Configure authentication
Switch on sign-in methods, set the password, second-factor and passkey policy, and manage keys and lifetimes.
Everything your product decides about authentication lives in one configuration. It applies to your product only, it takes effect on the next flow, and — apart from the origin and redirect lists — changing it never requires a code change on your side.
Users → Configuration is the screen. It reads the whole configuration and writes back only what you changed, in eight sections: Signup, Password policy, Multi-factor authentication, Passkeys, Sessions, Email, OAuth providers and Waitlist widget. Edits are held as a draft until Save changes, and Discard throws the draft away.
Two things below are part of the same configuration but have no field on that screen — the origin and redirect allowlists — and the keys are not part of it at all: they have a screen of their own, Users → Keys. Each says so where it is described. Everything here is available to an agent as well.
Turn on a sign-in method
A method is a way an end-user proves who they are. Turn on as many as you want; they combine on one sign-in screen, and a user who signed up with one can later add another.
Password. An email address and a password, hashed with argon2id. Subject to the password policy below.
Neither email method is offered to your users until this product has a sender address on a domain verified in its mail toolkit. That address is email.from, in the Email section of the same screen.
Email one-time code. A six-digit code is emailed and typed back into your sign-in form. No password is involved, so nothing can be reused or leaked from another site. A code is short-lived, may be attempted a handful of times, and a resend is rate-limited both by a cooldown of about a minute and by a daily cap.
Magic link. The same email, rendered as a link instead of a code. Opening the link never signs anyone in by itself: it lands on a page that asks for a click, which prevents mail scanners from consuming the link on the way. If the user opens the link on a different device from the one they started on, they confirm the waiting device and carry a short code back to it — the session is only ever created on the device that began the sign-in.
OAuth. Google and GitHub. In development you can use the shared Lessly credentials to try the flow; in production you register your own application with the provider and store its client id and secret in the OAuth providers section of Users → Configuration, where the secret is held encrypted.
Passkeys. A WebAuthn credential held on the user’s own device — the passwordless method, and on most devices an identifier-less one: the browser offers the passkeys it holds and the authenticator names the user. Off until you turn it on with the passkeys block below. A passkey is also a second factor over any other method. Add passkeys is the subject end to end.
Multi-factor authentication. An authenticator app (TOTP) and single-use backup codes. Enrolment must be confirmed before the factor counts. Once a user has a verified second factor, every first method is gated by it — password, email code, magic link and OAuth alike, and a password reset does not bypass it. If a user loses both their factor and their backup codes, an operator can reset MFA from that user’s record; the reset is recorded, delayed, and the user is emailed so they can stop it. Manage your end-users has the screen, and Add two-factor authentication is the screen your own product renders to enrol people. The settings below are the policy around it.
Choose who may sign up
The Signup section of Users → Configuration carries the choice of three modes:
| Mode | Who creates the account |
|---|---|
| Public | Anyone may create an account. The default. |
| Invite-only | Accounts are created by you, by inviting a person from the directory screen or by running an import, and the user sets their first credential from the invitation email. |
| Waitlist | A public sign-up form collects people without creating a way in; you invite them in batches later. See Run a waitlist. |
Independently of the mode you can restrict which addresses are accepted: an allowlist or a blocklist of domains, blocking of disposable-mailbox providers, and blocking of + sub-addresses. Those rules are set through the product’s API; the Signup section carries the mode — signupMode — and nothing else.
A CAPTCHA is on by default at sign-up and is also triggered at sign-in when a particular account or address starts failing repeatedly.
Set the password policy
The policy applies at sign-up and at every password change, and is edited in the Password policy section of Users → Configuration.
| Setting | Default |
|---|---|
| Minimum length | 8 characters |
| Maximum length | at least 64 characters, never silently truncated |
| Breached-password check | on |
Length is the control that matters, so there are no composition rules to configure — no required digit, symbol or mixed case. Submitted passwords are checked against a public database of breached credentials without the password or its full hash leaving the platform; if that check is unreachable it does not block a registration, and the same check runs again after sign-in so an affected user is flagged and asked to change their password.
Repeated failures lock an account temporarily, per account and per source. The lockout expires on its own and the user is emailed a way to unlock it sooner; a failed password is never a permanent lockout. The breached-password check and the lockout ceilings are protection fields on the configuration, alongside the rate limits.
Set the second-factor policy
The Multi-factor authentication section of Users → Configuration carries the mfa block, and an agent writes the same fields with users_config_upsert. Every field is merged on its own, so setting one leaves the rest alone.
| Field | Default | Range | What it decides |
|---|---|---|---|
required | false | — | Whether a second factor is mandatory for this product |
backupCodeCount | 10 | 1–20 | How many single-use backup codes a batch contains |
resetDelayHours | 72 | 0–720 | The delay on an operator-initiated factor reset — the window the user is emailed to abort it |
sensitiveAuthAgeMinutes | 10 | 1–1440 | How recently the caller must have authenticated to enrol or regenerate a factor |
maxSecondFactorAttempts | 5 | 1–50 | Wrong second-factor codes before the account is locked |
maxBackupCodeAttempts | 5 | 1–50 | Wrong backup codes before the account is locked, counted separately from the app’s codes |
stepUpGrantMinutes | 5 | 1–60 | How long a passed step-up challenge stays spendable |
A step-up grant is single-use whatever stepUpGrantMinutes says: the window only decides how long the user has to spend it, never how many times.
There is no on/off switch, and that is deliberate. A second factor belongs to the user who enrolled it, not to the product — turning the feature off would have to strip verified factors from accounts that rely on them, and silently weaken every one of those sessions. So enrolment is always offered, and your product decides only how far it goes: leave required at false and it is opt-in, or set it and it becomes a floor.
required is an assurance floor, not a migration. It does not enrol anybody and it does not lock existing users out; what it changes is that a user who has a verified factor may no longer unenrol their last one — the attempt is refused as last_factor_required, and the prebuilt screen says so in words. Independently of this setting, nobody may remove their last remaining way to sign in at all; that refusal is last_way_in.
Lowering sensitiveAuthAgeMinutes makes enrolling and regenerating ask for a fresh sign-in more often; raising it asks less often, and a stolen open session gets a wider opening. Ten minutes is the balance for most products.
Turn on passkeys
The Passkeys section of Users → Configuration carries the passkeys block, which turns your product into a WebAuthn relying party. It sits between Multi-factor authentication and Sessions, and an agent writes the same fields through the product’s API. Every field is merged on its own, so setting one leaves the rest alone.
| Field | On the screen | Default | What it decides |
|---|---|---|---|
enabled | Offer passkeys | false | Whether this product accepts passkeys at all. Off, the method is not offered and no credential can be enrolled |
rpId | Relying party ID | none | The domain credentials are bound to, when your allowed origins cannot decide it alone |
rpName | Relying party name | none | The product name the operating system’s passkey prompt shows. Defaults to the domain |
requireUserVerification | Require user verification | false | Refuse an assertion the authenticator did not verify. A verified one is aal2 either way |
rpId and rpName are the two text boxes, and an empty box means “derive it” rather than an empty value. rpId is a bare domain — no scheme, no port, no path — and the screen refuses a save that puts https://acme.com in the box before the call is made.
The domain is derived from your allowed origins, and refused rather than guessed. In order:
passkeys.enabledoff — no domain, and the method is not advertised.rpIdset — it must be the host of, or a parent domain of, every entry in your origin allowlist. A parent means a whole label:example.comis a parent ofapp.example.com, and is not a parent ofnotexample.com, which merely ends with the same letters.rpIdnot set — the single host your allowed origins share, when there is exactly one.- Anything else — two hosts with no
rpId, or anrpIdthat does not cover them — and passkeys stay off rather than binding credentials to a domain nobody meant.
Leave Require user verification at its default unless you have a reason. An unverified assertion is still a real factor and still goes through the second-factor challenge; refusing it outright turns some security keys away at the door for no assurance you were not already getting from aal.
There is deliberately no required flag and no per-user switch. A passkey is something an end-user adds to their own account, so the only product-level question is whether your product accepts them; whether one counted as two factors is decided per sign-in, by what the authenticator actually did.
Allow your origins and redirect URIs
Two allowlists guard the browser side, and both are matched exactly — scheme, host and port, no wildcards and no prefix matching.
Origins are the web origins your frontend runs on. A browser call presenting your publishable key is rejected unless it comes from one of them. This is what makes a public key in your bundle harmless.
Redirect URIs are the addresses an end-user can be returned to when a flow finishes — your sign-in callback, your OAuth return route, the page after a verification email. A flow that asks to return anywhere else is refused rather than followed.
Each list holds up to twenty entries, and a write replaces the whole list rather than appending to it — send every entry you want kept.
Keep development and production entries separate, and remove an entry as soon as the environment it belonged to is gone.
Rotate a key
Your product has two keys, and they are never mixed. A publishable key is refused on the endpoints that administer users, and a server key is refused on the public flow endpoints.
| Publishable key | Server key | |
|---|---|---|
| Prefix | upk_ | usk_ |
| Runs in | the browser, and any client library | your backend only |
| Secret | no | yes |
| Can do | start and advance sign-up and sign-in flows | exchange and refresh tokens, read and administer users |
| Protected by | the origin allowlist | secrecy |
The publishable key also tells the client libraries which address to talk to, so there is no base URL to configure and no way to point a client at the wrong product.
Users → Keys is where both kinds of key live, in two sections. API keys lists what the product has issued and what state each one is in, Issue an API key mints one — you name it, because the name is the only way to tell two apart, and you pick whether it is a server key or a publishable one — and each row revokes. A publishable key stays readable there; a server key is shown once, at the moment it is minted, and never again.
JWT signing keys is the second section: the keys Lessly Users mints access tokens with, listed by key id with the state each is in.
| State | What it means |
|---|---|
| Standby | Generated, not published, verifying nothing yet |
| Current | Signs every token from now on |
| Retired | No longer signs, but stays in JWKS until its overlap expires |
| Revoked | Evicted from JWKS, so tokens it signed no longer verify |
Rotate signing key mints a new current key, and a row revokes. Rotation invalidates nothing on its own — that is what the overlap is for. Everything on this screen is available to an agent as well.
Set session lifetimes
The Sessions section of Users → Configuration carries all three.
| Setting | Default | Notes |
|---|---|---|
| Access token lifetime | 10 minutes | accessTokenTtlMinutes, 5 to 60 |
| Session lifetime | 720 hours | sessionTtlHours — the session’s own lifetime |
| Session inactivity window | 30 days | refreshSlidingDays; every refresh slides it forward |
| Session absolute lifetime | 365 days | refreshAbsoluteDays, counted from sign-in and never extended |
Shorter access tokens make a revoked session stop working sooner, because your backend verifies tokens locally and does not ask us on each request; longer tokens mean fewer refreshes. Ten minutes is the balance for most products. When you need a session to stop working immediately — a ban, a sign-out from a stolen device — the server library can verify a token against the live session instead, at the cost of one call. Sessions and tokens covers that in full.
Some events end a user’s sessions regardless of these settings: a password change, an email change, an MFA reset, a ban, and an erasure.
Next steps
- Run a sign-in flow: what each method looks like step by step.
- Add two-factor authentication: the prebuilt two-step screen your product renders.
- Add passkeys: turning passkeys on, and the screens your product renders.
- Sessions and tokens: verifying, refreshing and revoking.
- Manage your end-users: the management App and the audit trail.