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/verifywith{}mails a link that verifies the address:202once queued,204when it is verified already,503 mail_unavailablewithout a relay.POST <issuer>/account/api/me/emailwith{"email": "<new address>"}asks to change the sign-in address (see Signing in). Past five requests in an hour the answer is429withRetry-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.
| Route | Does |
|---|---|
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/register | spends 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/endwith{"sid": "<sid>"}ends one of the user's own sessions; another user's id ends nothing.POST <issuer>/account/api/sessions/end-allwith{}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).