DocsDevelopers

Auth and mail

Passwords, codes by email, two factors, password reset, email change, sign in alerts, the rate limit and the mails, all declared in schema.ts.

An auth collection is a collection whose records sign in. Every option of it lives in schema.ts, beside its fields. The server checks the options at boot, as PocketBase does, and refuses a combination that cannot work.

Options in schema.ts#

import { auth, bool, mailTemplatesFr, text } from 'locosdk/schema'

const fr = mailTemplatesFr({ otpSeconds: 600 })

users: auth({
    fields: { name: text(), role: text(), disabled: bool() },
    indexes: ['CREATE UNIQUE INDEX idx_users_name ON users (name)'],
    authRule: 'disabled = false',
    options: {
        passwordAuth: { identityFields: ['email', 'name'] },
        otp: { enabled: true, duration: 600, length: 6, emailTemplate: fr.otp },
        mfa: { enabled: true, rule: "role = 'admin'" },
        authAlert: { enabled: true },
        authToken: { duration: 7 * 86400 },
        resetPasswordTemplate: fr.resetPassword,
    },
}),
  • Every key is optional. A key left out is the default, so removing a line turns the option back to its default.
  • A schema never holds a token secret: the type has no slot for one. Secrets are minted once in the database and survive every boot.
  • The boot refuses: two factors with fewer than two methods on, an identity field without a single column unique index, a mail template with an empty subject or body, a duration outside its range.

Defaults#

whatdefault
auth token12 hours, refreshable
file token180 seconds
verification link1 day
password reset link, email change link30 minutes
code by email8 digits, 180 seconds
two factor session600 seconds
password8 characters minimum, 71 maximum, bcrypt cost 10

Signing in#

  • Password: POST /api/collections/users/auth-with-password with identity and password. The identity fields are email by default.
  • Code by email: request-otp mails a code and answers an otpId, also for an unknown address, so nobody can probe accounts. auth-with-otp signs in with it. Wrong codes are limited to 5 per 180 seconds per account.
  • Two factors: with mfa.enabled, a first factor answers 401 { mfaId } when mfa.rule matches the record. The second factor sends that mfaId with a DIFFERENT method. The client's authWithPassword returns { mfaId } in its type, and Session opens the code step by itself (Client and data layer).
  • The auth rule runs after the factors: authRule: 'disabled = false' refuses a switched off account with 403.
  • Superusers sign in on _superusers. golo superuser upsert <email> creates one or sets its password, from stdin.

Changing a password or an email#

  • A user changing their own password sends oldPassword. Without it, or with a wrong one, the update fails: a stolen session cannot set a new password.
  • request-password-reset mails a link (at most one per 2 minutes) and answers the same whether the address exists or not. confirm-password-reset sets the password and ends every session of the account.
  • request-email-change mails a link to the NEW address. confirm-email-change takes the link's token and the account password, sets the email and verified, and ends every session.
  • A password or email change rotates the record's tokenKey. Every token of the record dies at once, codes and MCP access tokens included.

Sign in alerts#

With authAlert.enabled (the default), a password or code sign in from a new device mails the account an alert with the time, the IP and the browser. The first sign in only records its device. An account keeps its 5 newest devices. A failed alert is logged and never blocks the sign in.

The rate limit#

Password and code sign ins are limited to 5 per 3 seconds per client IP, then 429 Too Many Requests. Behind a proxy, set trustedProxy so the limit sees the real client and not the proxy (Files, backups and proxies). createApp({ rateLimits }) takes PocketBase's rate limit rules for other labels.

Mail#

import { createApp, smtpTransport } from 'locosdk/server'

const app = createApp({
    schema,
    meta: { appName: 'Orders', appURL: 'https://orders.example.com', senderName: 'Orders', senderAddress: 'no-reply@example.com' },
    mail: smtpTransport({ host: 'smtp.example.com', port: 587, username: 'orders', password: process.env.SMTP_PASSWORD ?? '' }),
})
  • The SMTP client is built in, with no dependency: STARTTLS when the server offers it, implicit TLS with tls: true, AUTH PLAIN or LOGIN. It refuses to send a password over plain text, except to the local machine.
  • smtpProbe(config) connects, authenticates and quits: { ok: true } or { ok: false, error }, never the password.
  • Five mails per auth collection: otp, authAlert, resetPassword, verification, confirmEmailChange. English is PocketBase's text. French comes from mailTemplatesFr({ otpSeconds }).
  • Placeholders: {APP_NAME}, {APP_URL}, {TOKEN}, {OTP}, {OTP_ID}, {ALERT_INFO}, and {RECORD:<field>} for any field that is not hidden.
  • Every mail goes through onMailerSend, where a hook can add a footer or change the recipients.

Not here yet#

This page as markdown