# Configure authentication

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.

Edit it on **Configuration** in the management App, or ask an agent to. That screen is one form with six sections — Signup, Password policy, Sessions, Email, OAuth providers and Waitlist widget — a **Save changes** at the foot that sends only the fields you touched, and a **Discard** beside it. It is not the whole configuration: several fields below have no screen at all, and each one says so where it comes up.

## 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.

### UI

In the management App (`app.lessly.com`), open **Configuration**. **Email** carries the sender address and the switches for six-digit codes and magic links, with the lifetime both share. **OAuth providers** carries a switch, a client id and a write-only client secret for Google and for GitHub. Password sign-in has no switch on either surface: what governs it is the policy below.

### MCP

Ask your agent:

```text
Turn on email one-time codes for Acme, and leave password sign-in on.
```

The agent needs write access to the product's authentication configuration.

What each method is:

**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 the **Sender** box in the same section, and the screen says as much under it.

**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.

**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 configuration, where the secret is held encrypted.

> **WARNING**
> A provider account is linked to an existing user automatically only when the provider states the email is verified **and** it matches a verified address of exactly one of your users. Anything less unambiguous than that is never linked silently.

**A second factor.** 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 Google or GitHub alike — and a password reset does not bypass it. If a user loses both their factor and their backup codes, an operator resets it from **MFA factors → Reset MFA** on that user's record; the reset is recorded, delayed, and the user is emailed so they can stop it. The second-factor settings themselves — the backup-code batch size, how long that reset is delayed, how fresh an authentication has to be to enrol, the second-factor lockout ceilings — are `mfa` fields on the configuration, and **Configuration** carries no section for them.

## Choose who may sign up

| Mode | Who creates the account | Where they come from |
|---|---|---|
| Public | Anyone with an email address. The default. | Your sign-up form. |
| Invite-only | You do, from the management App or an import. | The invitation email; the user sets their first credential from it. |
| Waitlist | Nobody yet. A public form collects people without creating a way in. | You invite them in batches later — see [Run a waitlist](/ship/users/waitlist). |

### UI

**Configuration → Signup** is three buttons — **Public**, **Invite only**, **Waitlist** — with a line under them saying what the chosen one means.

### MCP

Ask your agent:

```text
Set Acme to invite-only sign-up.
```

The agent needs write access to the product's authentication configuration.

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. 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. None of it is on **Configuration**. The CAPTCHA is the `captcha` field of `users_config_upsert`; the address rules are not a section of that screen either.

## Set the password policy

The policy applies at sign-up and at every password change.

| Setting | Where |
|---|---|
| Minimum length | **Configuration → Password policy**, 1 to 256 |
| Require a digit, an uppercase letter, a symbol | **Configuration → Password policy**, one checkbox each |
| Breached-password check | the `protection` field of `users_config_upsert`; no screen |

Length is the control that matters, and the composition checkboxes are there for a policy you are required to keep rather than one worth adopting: each one shrinks the space of passwords a person will actually choose.

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. That check and the lockout ceilings are `protection` fields, so they are set through the tool rather than on a screen. 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.

## 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.

> **WARNING**
> **Neither list has a screen.** **Configuration** has no origins section and no redirect-URI section; both are `allowedOrigins` and `redirectUriAllowlist` on `users_config_upsert`, so an agent or your backend is the only way to change them today. Two things depend on that allowlist and will not tell you why: a browser call presenting your publishable key, and the **Impersonate** button on a user's record, which stays disabled while the list is empty.

Ask your agent:

<CodeBlock lang="text" code="Allow the origin https://app.example.com and the redirect URI https://app.example.com/auth/callback for Acme. Keep the entries that are already there." />

The agent needs write access to the product's authentication configuration.

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.

A product can hold more than one server key, so a rotation needs no downtime:

1. Create the new key and copy it while it is on screen.
2. Deploy it to your backend.
3. Revoke the old one.

### UI

Keys live on their own screen, not in the configuration form. **Keys → Issue API key** mints one; a server key comes back inside a red panel that says to copy it now, a publishable key inside a plain one because it is not a secret and stays readable in the list. **Revoke** is on each active row. Once your deploy is out, revoke the old key from there.

### MCP

Ask your agent:

```text
Create a new server key for Acme, then revoke the one created in March.
```

The agent needs write access to the product's authentication configuration.

> **WARNING**
> The server key is shown once, when it is created, and stored only as a hash afterwards. The platform cannot show it to you again.

## Set session lifetimes

All four are on **Configuration → Sessions**, as number boxes that refuse a value the tool would reject.

| Setting | Screen label | Default | Notes |
|---|---|---|---|
| Access token lifetime | Access token lifetime (minutes) | 10 minutes | 5 to 60 |
| Session inactivity window | Inactivity window (days) | 30 days | A session unused for this long ends; every refresh slides it forward |
| Session absolute lifetime | Absolute limit (days) | 365 days | Counted from sign-in and never extended |
| Session lifetime | Session lifetime (hours) | 720 hours | The session's own lifetime, in hours |

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](/ship/users/sessions-and-tokens) compares both modes.

Some events end a user's sessions regardless of these settings:

- a password change or a password reset
- an email change, and the revert of an email change
- a reset of a second factor
- a ban
- an erasure

## Next steps

- [Run a sign-in flow](/ship/users/auth-flows): what each method you just switched on looks like, step by step.
- [Read the token contract](/ship/users/sessions-and-tokens): verifying, refreshing and revoking.
- [Manage your end-users](/ship/users/user-management): the directory and the audit trail these settings govern.
- [Run a waitlist](/ship/users/waitlist): the sign-up mode for a product that is not open yet.
