Docs
  • 0.1.0latest

The account portal

Where users manage themselves: their address, profile, password, authenticator app, recovery codes and passkeys, where they are signed in, and which applications hold access. The page and the API under it.

The account portal is the page at <issuer>/account. It shows the signed-in user's address and offers a link that verifies it and a change to another one, how the account signs in, where it is signed in and which applications hold access. It signs out one session or every session after a confirmation, takes an application's access back saying when the tokens it already holds stop working, changes the password, issues new recovery codes shown once, turns on an authenticator app from a key shown once, and adds or removes passkeys. Without a session it offers a sign-in that comes back to it, and it offers the same sign-in when a change needs a recent one.

Everything the page shows comes from the account API under <issuer>/account/api, described below. Every route acts for the session's own user and nobody else, needs the session cookie (401 session_required without it), takes JSON only, is counted under its own limit and is never cached.

Your account

GET <issuer>/account/api/me answers the user's own account: the address they sign in with and whether it is verified, their profile, and their sign-in factors as facts: whether a password is set, whether an authenticator app is enrolled, how many recovery codes are left, each passkey by its credential id with when it was registered and whether it may be synced, and the company's two-factor policy as two_factor. No secret, hash or key is part of it.

  • POST <issuer>/account/api/me/verify with {} mails a link that verifies the address: 202 once queued, 204 when it is verified already, 503 mail_unavailable without a relay.
  • POST <issuer>/account/api/me/email with {"email": "<new address>"} asks to change the sign-in address (see Signing in). Past five requests in an hour the answer is 429 with Retry-After.

Changing how the account signs in

Each of these needs a sign-in from the last ten minutes that reached the level the account's second factor gives (a password alone does not change an account that holds a passkey or an authenticator app), otherwise 401 reauthentication_required. Each signs out every other session of the user, revokes every refresh family the user holds, is recorded, and says when the access tokens already issued stop working. <api> below is <issuer>/account/api.

RouteDoes
POST <api>/password {"password"}sets a new password under the password rules
POST <api>/totp/begin {}answers a new authenticator secret once, in base32 and as an otpauth:// URI, kept sealed for ten minutes
POST <api>/totp/confirm {"code"}enrols the app when the code comes from that secret; a wrong code ends the attempt
POST <api>/totp/remove {}turns the app off and takes the unspent recovery codes with it
POST <api>/recovery-codes {}replaces every unspent recovery code with ten new ones, shown in that answer only
POST <api>/passkeys/options {}a registration challenge issued to this user alone
POST <api>/passkeys/registerspends that challenge once, checks the response against this issuer's relying party and origin, stores the passkey
POST <api>/passkeys/remove {"id"}removes one of the user's own passkeys

The account's last way to sign in is not removed (409 last_sign_in_method). Under a company policy of required, its last second factor is not removed either (409 second_factor_required); under off, no authenticator app is offered (409 not_offered).

Profile

POST <issuer>/account/api/profile with {"name": "...", "given_name": "...", "family_name": "..."} replaces the profile whole: each name is trimmed, at most 100 characters with no control character (otherwise 400 profile_rejected), and a name left out or empty is cleared. UserInfo releases the names to a token carrying profile, as they stand at that moment; they appear in no token.

Grants

GET <issuer>/account/api/grants lists, for each client still holding one, the scopes, the resources and when the grant was given. POST <issuer>/account/api/grants/revoke with {"client_id": "<client>"} withdraws the consent, which is recorded, and revokes every refresh family of the user at that client, so the client must ask again. The answer says how many families ended and the time by which the access tokens it already holds stop working, at most five minutes on.

Sessions

GET <issuer>/account/api/sessions lists the user's live sessions at this issuer by their public id, with when each signed in, when it was last used, the methods it signed in with, and which one is asking; never a cookie.

  • POST <issuer>/account/api/sessions/end with {"sid": "<sid>"} ends one of the user's own sessions; another user's id ends nothing.
  • POST <issuer>/account/api/sessions/end-all with {} signs the user out everywhere: every session, the asking one included, and every refresh family at every client. The answer says how many of each ended and when the access tokens already issued stop working.

Every ending is recorded in the audit trail, and clients that registered a back-channel URI are told (see Logout).