Docs
  • 0.1.0latest

Signing in

The sign-in pages, passwords and passkeys, sessions, throttling that never locks an account, and the mails that reset a password, enrol a user, verify an address or change it.

The pages a user signs in with are served by the binary itself, at the company's issuer:

PageAddress
sign-in<issuer>/login
password reset<issuer>/login/reset
first password from an enrolment link<issuer>/login/enrol
address verification<issuer>/login/verify
change of sign-in address<issuer>/login/email
consent<issuer>/consent
account portal<issuer>/account

They load nothing from anywhere else, talk only to this server, submit no form, cannot be framed, follow the system's light or dark theme and work from the keyboard alone; a passkey is offered wherever the browser supports one. A company may brand them with its name, colour, logo and links (see Branding and mail).

How a sign-in goes

A sign-in is a few steps: the address, then the first factor, then a second factor when the account has one or the company requires one.

  • A password. A wrong password and an address with no account get the same 401.
  • A passkey answers its sign-in's challenge once. One that verified its user (a PIN or a biometric) is a whole sign-in and names the account itself; one after a password stands as the second factor. A passkey of another account, a foreign origin or a signature counter that did not move forward gets that same 401 and is counted.
  • An authenticator app code or a recovery code as the second factor.

The step that completes a sign-in opens the user's session under a cookie of its own, __Host-tessari-session, drawn at that moment, and clears the sign-in cookie. A session lasts a working day without use and a week at most.

Holds, never lockouts

Each sign-in step is counted per client address. A wrong password is also counted against the address being signed in to, the client address, and the pair of the two (a wrong one-time or recovery code against the first two). Past a few failures that key is held for a short, growing time that ends by itself. No account is ever locked.

Passwords

A new password, whether set at enrolment, by a reset link or in the account portal, is refused with 400 password_rejected when:

  • it is shorter than its use requires: at least fifteen characters, or eight when the account has an authenticator app its company's policy asks for (a sign-in then always asks for its code);
  • it equals the account's address, its local part or the company id;
  • it was seen in a breach. The lookup sends the first five hex digits of the password's SHA-1 to BREACHED_PASSWORDS_URL and matches the rest here. When the lookup does not answer within 3 seconds the password is judged without it and the miss is logged as breach_check_unchecked.

Passwords are hashed with Argon2id under the server's pepper.

A forgotten password

A user asks from the sign-in page. POST <issuer>/login/api/reset with an address answers 202 for every address, with or without an account, and does the same store work for each, so the answer says nothing about who has an account. Without a mail relay it answers 503 mail_unavailable for every address.

An address with an account gets a link to <issuer>/login/reset#token=<token>. It works once, for 30 minutes, and stops working when a newer link is asked for or the account signs in. The token travels in the fragment, which reaches neither the server's logs nor a Referer, and the page takes it out of the address before it sends anything. Setting the new password changes the password only: the authenticator app and passkeys stay, so the next sign-in still asks for them. Every session ends, every refresh token is revoked, the change is recorded and a notice goes to the address.

First password

A user an operator created sets a first password with the one-time code the operator handed over, or from an enrolment link mailed to them: POST <issuer>/login/api/enroll with {"code": "<code>", "password": "<password>"}.

  • A code is kept only as its digest, lasts 72 hours, and a newer code for the same user replaces it.
  • A code that was never issued, was spent, was replaced or has lapsed gets 400 code_rejected; a refused password does not spend the code.
  • An account that already has a password keeps it (409 already_enrolled).
  • The answer is 204. No session is opened; the user then signs in.

Opening an account

A company can let people open their own account. It is closed until an operator opens it in the console, optionally for a list of domains. GET <issuer>/login/api/registration answers {"open": true} or {"open": false}. While it is open, the sign-in page offers "Create an account", which sends POST <issuer>/login/api/register with the typed address.

Every acceptable address gets 202 and the same body, and the mailbox learns the rest: a new address gets an account and an enrolment link, an account that never set a password gets a new link, an account that signs in gets a notice and no link, and a disabled account gets nothing. The requester is never told that an address is taken. Each address may be asked for 3 times an hour and each client address 10 times. A closed company answers 404, a server with no relay 503, an address that cannot be one 400 email_rejected, and one at a domain the company does not list 400 address_not_accepted.

Verifying an address

An account's address stays unverified (email_verified is false) until a link sent to it is opened. With a relay set, a link to <issuer>/login/verify#token=<token> is mailed when an account sets its first password and whenever the user asks for one in the account portal. It works once, for 24 hours, and a newer link replaces it.

Changing the sign-in address

The user changes the address from the account portal, which needs a sign-in from the last ten minutes. The new address gets a link that confirms, the current address one that cancels, and the change waits a day from the request even when confirmed at once, so the current address can still cancel it. When it takes effect the new address signs in, already verified, and both addresses get a notice. An address another account already uses gets the same 202 and a notice with no link, so asking says nothing about who has an account. A cancellation is sent only when the user presses its button, so a mail filter that opens links cannot cancel a change.

Notices

With a relay set, the account's address gets a notice whenever its sign-in changes: password changed or reset, a passkey or authenticator app added or removed, new recovery codes, a recovery code used, every session signed out. A notice carries no link.