Skip to content

Latest commit

 

History

History
150 lines (128 loc) · 10.3 KB

authentication.md

File metadata and controls

150 lines (128 loc) · 10.3 KB

Authentication

Ithibati is pinned to 0.11.0. On an empty database, /setup first asks for a setup code. The operator issues it on the host with bin/setup-code. Then it asks for a username and a passkey. Only the code's digest is stored. A valid code yields a session authorization for ten minutes. Creating the first account consumes the code. Issuing a new code invalidates the previous code and its authorizations. Sikio.Claim stops the boot for any other initial_claim setting. See Operations for the operator's steps.

Later registrations require a valid invitation. Every signed-in member can create invitation links on /invitations. A link is shown once, expires, and can be accepted once. The account that claimed the instance is an administrator. bin/grant-admin makes another account one.

The same page lists pending invitations with invitee, inviter, creation time and expiry. A member withdraws the pending invitations they sent, an administrator any. A withdrawn link stops working immediately. An administrator removes an account on /accounts after confirming the passkey within five minutes. Removing revokes the account's sessions in the same transaction and disconnects their LiveViews after the commit. Invitations created before the inviter was recorded show no inviter, and the list states that. An invitation from a removed account names a former member.

The account identifier is a username or an email address. Sikio.Identity reads the mode from ACCOUNT_IDENTITY, and the default is username. In username mode, the inviter shares the link by any channel. In email mode, the invitee's identifier is an email address. The link is mailed to that address, and accepting it proves control of the address. Both schemas pass Sikio.Identity to Ithibati's :format and :format_message options instead of literals. Ithibati calls them on every changeset, so the format follows the runtime mode. The identifier field itself is fixed at compile time. An instance in email mode without mail configuration stops at boot. A failed delivery shows an error, and the link can still be shared by hand. See Operations for the environment variables.

In email mode a member changes their address under Settings → Email address. Asking needs a reauthentication within the last five minutes. Sikio mails a link to the new address and keeps the request beside the account. The link carries its token after #, like an invitation link, so no request or access log contains it. Only the same account, signed in, confirms it with a button on the link's page. Signed out, the page asks for a sign-in and the tab keeps the token, so the page confirms after the sign-in. Asking for an address allows 5 requests per account a day. A request is stored only once its link is mailed, so a failed mail keeps an earlier link. Until the confirmation the old address stays in force. The request expires after seven days, and a new request replaces the earlier link. An address of another account or of an open invitation is refused. After the change the old address gets a notice. Sign-in uses passkeys without an identifier, so sessions and passkeys stay valid. A password manager keeps showing the old address as the passkey's name. Sikio cannot rename a stored passkey.

Sessions are revocable. The session cookie is encrypted, because it carries new recovery codes until they are displayed. A session lasts sixty days from sign-in. The cookie's max_age matches that lifetime, so the session persists after the browser closes. Recovery codes are displayed once after registration.

The public auth pages are /login (passkey), /recover (recovery code) and /setup (first account only). /setup posts the operator's code to /setup/code before it asks for a username. Signed-in users are redirected to /. Layouts.auth/1 holds the project name and the layout of the auth pages. The recovery-code page has a copy button. If the clipboard write fails, the page asks you to select and copy the codes manually.

The member header in Layouts.member/1 takes current_account and shows the username menu. /account/passkeys supports adding, renaming and removing passkeys. The last passkey cannot be removed. /account/recovery-codes shows the number of unused codes. Replacing all codes requires an explicit confirmation. New codes use the same one-time page and copy button as registration.

Renaming and removing passkeys and regenerating codes are controller requests. Each request validates the session and checks the CSRF token. Adding a passkey binds the challenge to the signed-in account. The account is checked again when registration completes. Adding a passkey or generating new recovery codes also requires a reauthentication within the last five minutes. The reauthentication uses a passkey or recovery code of the current account. Adding a passkey checks it both when the challenge is created and when registration completes. All settings pages and messages are available in English and German.

Browser extensions

A browser extension connects to an account through an authorization code with PKCE (RFC 7636, S256). The extension opens /extension/connect with redirect_uri, state, code_challenge and client through identity.launchWebAuthFlow. Only an extension's own redirect URI is accepted: https://<id>.extensions.allizom.org/ for Firefox and https://<id>.chromiumapp.org/ for Chrome. Any other address gets no code.

Connecting needs a reauthentication within the last five minutes, checked when the code is issued, since the token gives lasting access. The page is public. Signed out, it links to /extension/sign-in, which remembers the page, and the sign-in returns there within ten minutes. Only that link remembers the page, so a crafted link alone redirects no later sign-in. That sign-in used a passkey or a recovery code, so it counts as the reauthentication for connecting, and for nothing else. A sign-in that uses the last recovery code shows the new codes first and does not return. A request with a malformed challenge goes back to the extension with invalid_request. The page then redirects to the extension with a one-time code. The code expires after a minute and serves once, with its verifier and the same redirect URI. A failed exchange deletes it.

The extension exchanges the code at POST /api/v1/token with code, code_verifier and redirect_uri. The answer is {"token": ...}, or status 400 with {"error": "invalid_grant"}. The token serves the extension API only. Settings → Browser extensions lists connected extensions with their name and last use and disconnects them. A disconnected token stops working at its next request. The database keeps digests of codes and tokens only.

Authentication limits and maintenance

AuthRateLimit allows 10 recovery requests and 120 other ceremony requests per client address in a 60-second fixed window. IPv6 addresses count per /64 prefix. Exceeded requests get HTTP 429 with Retry-After, and the page shows a translated message. Exchanging a setup code allows 10 attempts per client address in a 60-second window. A rate-limited exchange redirects to /setup with Retry-After and a message naming the wait. Creating invitations allows 20 per signed-in account in a 24-hour window. Asking for a new address allows 5 per account in a 24-hour window, since each request mails a typed address too. The extension API allows 120 requests per account in a 60-second window, and 120 token exchanges and requests without a valid token per client address; see Extension API. Configure :auth_rate_limits on the application with positive counts and seconds. A group missing from the configured list keeps its default. The defaults are:

[recovery: {10, 60}, ceremony: {120, 60}, setup: {10, 60}, invite: {20, 86_400},
 address: {5, 86_400}, api: {120, 60}, api_address: {120, 60}]

The invitation limit counts per account, not per browser. Counting per session, username or address would let one account reset its count. The limit is checked before the form is validated, so invalid submissions also count. Otherwise invalid input would bypass the limit. A refused attempt creates no invitation and sends no mail. A link already on screen stays visible, because it is stored nowhere else. The limit is 20 per day, while Ithibati Starter uses 10. The limit targets a compromised account that sends mail through the operator's SMTP account at a steady rate. A burst is not the threat it addresses.

The counters are in memory, in a supervised process, and are atomic per node. Each limit group holds at most 10,000 keys per node. A restart resets them. SikioWeb.ClientIp resolves the client address. Behind a reverse proxy, every socket peer is the proxy. The address then comes from the X-Forwarded-For header. Sikio reads the header only from a trusted proxy: loopback addresses and the entries of TRUSTED_PROXIES. For any other peer, the socket peer address counts. Multiple nodes need a rate limit at the edge for a cluster-wide budget. This also applies to the invitation limit. Node-local counters do not enforce a cluster-wide quota. An edge rule keyed by address does not replace a limit keyed by account. These counters are not a distributed rate-limit service.

The passkey settings page has Sign out on all devices, which includes the current session. Ithibati revokes the stored sessions and disconnects their LiveView sockets. Passkeys remain valid for later sign-ins.

In development, run mix auth.cleanup to clean up immediately. It runs in a separate node, so it cannot disconnect the LiveViews of a running server. For a running release, call:

bin/sikio rpc 'Sikio.AuthCleanup.run()'

It returns deletion counts for expired sessions, expired challenges and expired, unaccepted invitations. It disconnects the LiveViews of each expired session. Valid credentials, recovery codes and accepted invitations remain. Sikio.Accounts.Cleanup runs it every fifteen minutes on Oban's maintenance queue, so a deployment needs no cron entry. The same job deletes expired address requests and extension codes.

Phoenix request logs filter parameters whose names contain password, secret, token, code or credential. The setting is :filter_parameters. This covers recovery codes and WebAuthn credentials. Keep this filtering when you add logging.