Browser-direct sign-in, tokens and member projection

Use the SSO base URL, context, handles and proof rules. The operations below have no confidential client authentication and require browser-direct authentication enabled. Only registered scopes/audiences can be issued. Unknown fields are rejected.

Password and provider sign-in

Method / relative path Exact input Success and lifecycle
POST /challenge Authorization context transaction, challenge, expires_in: 300; no DPoP JWT yet
POST /login transaction, username (≤320), password (≤4096), dpop (≤16384); optional signup_transaction code, echoed state, expires_in: 120, or outcome: signup_required with signup transaction/status
GET /authorize Context as query parameters plus provider: github, discord or m7 303 provider navigation; validated callback receives code,state or error,error_description,state
POST /token Grant-specific body below and fresh header proof Token package, or outcome: two_factor_required
POST /token/ack Session handles, activation_id; optional selection_id Activation receipt described below
POST /me Session handles and fresh endpoint proof Scope-limited tenant profile

For example, send this body to /challenge (replace every placeholder with a fresh valid value; the challenge and thumbprint must each be 43 base64url characters):

{
  "redirect_uri":"https://app.example.org/auth/callback",
  "state":"<fresh unpredictable state>","nonce":"<fresh nonce>",
  "code_challenge":"<S256 challenge>","code_challenge_method":"S256",
  "dpop_jkt":"<signing-key thumbprint>",
  "scope":"openid profile email","aud":"https://api.example.org"
}

The example audience must first be registered for the application. After the challenge response, /login receives:

{"transaction":"<password transaction>","username":"sample.member","password":"<password>","dpop":"<signed login proof including returned challenge>"}

For password login, sign a fresh /login proof containing the returned challenge and send it in JSON dpop. The password challenge is consumed before credentials are checked; a wrong password or lost response requires a new challenge. A username or unambiguous verified email can identify the member. Credentials and groups are checked within the application's organization. Consumer-linked members use their linked credentials but keep tenant ownership, projection and tenant-factor policy. A disabled account does not become active merely because its password is correct. Only a proved incomplete signup can return the signup continuation.

Provider context expires after ten minutes. Validate callback state and preserve the original key/verifier. A login-only miss is access_denied; it never creates or links an account by matching email. Use explicit provider signup or linking for those operations. A provider callback is one-use. Current federation, member and group policy is rechecked before issuance. Brokered/federated sign-in never requires local tenant 2FA and does not assert that the broker performed MFA. Closing navigation discards local state; there is no password/provider-login cancel endpoint. Restart after expiry, denial or lost one-use completion.

Token pickup and two-factor continuation

{
  "grant_type":"authorization_code",
  "code":"<one-use code>",
  "code_verifier":"<original 43–128 character PKCE verifier>",
  "redirect_uri":"https://app.example.org/auth/callback"
}

These four fields are required for code pickup. An optional pair bd_carrier,bd_key attaches a newly authenticated session to an existing carrier; do not include bd_session on this grant. Retain the original callback and S256 verifier (43–128 characters from letters, digits, ., _, ~, -). The code expires after 120 seconds and is consumed before upstream issuance. It cannot be redeemed again to recover a lost initial token response.

When local tenant 2FA is required, pickup consumes this code and returns:

{"outcome":"two_factor_required","continuation":"<opaque continuation>","expires_in":300}

No usable token or new member session is released. Submit POST /two-factor/login with continuation, code and optional boolean recovery (default false), using the original signing key. Successful proof returns a new code, original state, expires_in: 120 and boolean two_factor_disabled. Redeem that new code with the original PKCE verifier. See tenant two-factor for attempts, expiry and backup-code behavior. Lost continuation or consumed proof requires fresh sign-in.

Token package and custody

{
  "access_token":"<provisional access token>",
  "token_type":"DPoP",
  "expires_in":300,
  "scope":"openid profile email",
  "token_state":"pending",
  "bd_carrier":"<carrier>",
  "bd_session":"<member session>",
  "bd_key":"<carrier secret>",
  "dpop_jkt":"<key thumbprint>",
  "activation":{
    "id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "expires_in":60,
    "ack_endpoint":"https://id.m7.org/api/v2/oauth/token/ack"
  }
}

The example lifetimes are illustrative; use returned values. Public fields are access_token, token_type, token_state (active or pending), the three handles, dpop_jkt, and optional expires_in, scope, activation, selection_id, session_cleanup_pending. activation appears for pending tokens and contains only id, expires_in, ack_endpoint. A pending switch can also carry a selection_id. No refresh token, ID token, activation secret, binding chain or private root data is returned. Rely on /me for browser profile display and the protected resource's token validator for authorization.

Save the package before ACK. Pending access is unusable. Send the HTTP request to this app's /token/ack, even though the proof's htu is the fixed Identity ACK URL shown above. The SSO service supplies its held activation secret.

{
  "bd_carrier":"<carrier>","bd_session":"<member session>","bd_key":"<carrier secret>",
  "activation_id":"aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
}

Include the returned selection_id when activating a pending selection. Successful ACK returns token_state: active, normalized activation_id and boolean idempotent, optionally session_cleanup_pending. It does not return a replacement access token: use the saved package after confirmed activation. Repeated ACK with a fresh proof can return an idempotent receipt. Never copy the ordinary confidential ACK request into the browser.

sequenceDiagram
  participant B as Browser
  participant S as SSO
  participant I as Identity
  B->>S: Code + verifier + endpoint proof
  S->>I: Bound token request
  I-->>S: Pending token package
  S-->>B: Public pending package + handles
  Note over B: Save package; do not use token yet
  B->>S: ACK + proof targeting Identity ACK URL
  S->>I: ACK with server-held activation secret
  I-->>S: Active receipt
  S-->>B: Active receipt; commit selection

Refresh and interrupted activation

POST /token refresh accepts only grant_type: refresh_token, session handles and optional scope narrowing. It takes no browser refresh-token value. It renews the currently selected session; use /sessions/switch for another one. The server checks current member/app policy, scope/audience admission, IP and token binding. Refresh and switching do not make primary authentication fresh.

{"grant_type":"refresh_token","bd_carrier":"<carrier>","bd_session":"<selected session>","bd_key":"<carrier secret>"}

Browser-direct uses the server's ACK/supersession lifecycle. A confirmed predecessor refresh credential remains authoritative until its successor ACK commits. If a pending package was already saved, refresh/switch returns that package instead of rotating again; its scope cannot be changed while pending.

Interruption Required response
Pending package received, ACK response lost Retry ACK with a new proof and the same activation/selection receipt
Renewal response lost but handles retained Refresh/switch with fresh proof to recover server-saved pending package; then ACK
Wrong proof/key, network error, 5xx or malformed ACK response Preserve pending state and predecessor; do not declare activation or discard recovery authority
Terminal ACK error: invalid_grant, unknown_activation, unknown_activation_lineage, activation_not_pending, activation_not_current, activation_parent_not_current, activation_parent_invalid, activation_expired Discard terminal pending state, preserve confirmed predecessor, and attempt one fresh renewal; sign in if that fails
Initial pickup lost before any handles were received Fresh sign-in; authorization-code replay is not a recovery mechanism
session_changed for a stale selection Reload carrier list and selected account; never install an obsolete selection

Do not claim every upstream interruption is automatically recoverable. Selected account changes commit only with an active token; denial leaves the previous selection intact and never falls back to a different remembered account.

These five POSTs require endpoint DPoP, an admitted origin and the email-sign-in allowance. They do not require a selected session or email scope merely to prove ownership; scopes for the eventual token come from the context.

Relative path Exact input Output
/email-login/start Context plus email (valid address, ≤254; trimmed/lowercased) transaction, status, expires_in, request_id, resend_after, entered email, generic message
/email-login/resend transaction Current view with new request_id and cooldown
/email-login/verify transaction, request_id UUID, method: code or link, proof Updated view; completed includes one-use code and state
/email-login/status transaction Current view; completed receipt can recover its code while still valid
/email-login/cancel transaction Cancelled view before completion starts

proof is a six-digit string for code or the 43-character base64url fragment token for link. The transaction stays bound to app, org, origin, original browser key and authorization context. Unknown, unverified, ambiguous, ineligible or member-throttled addresses receive the same generic send view and no usable email. The email echo is the entered address, not an ownership oracle. Current unique verified ownership, admission and policy are checked again before completion. Native, federated-only and consumer-linked tenant members can qualify; email proof neither links providers nor merges accounts.

{"transaction":"<email transaction>","request_id":"bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb","method":"code","proof":"123456"}

One email offers both alternatives; first successful consumption invalidates both. Magic links return to the registered callback with fragment fields email_signin=1, state, request_id, token. Remove the fragment immediately, hold it only in memory, validate original state/request, and require an explicit confirmation POST. A GET or preview must not consume it. Forward confirmation only to the original browser/tab with its key and transaction. If the link opens elsewhere, enter the emailed code in the original browser; do not adopt the other browser's authority or silently begin a different account session.

States are verification_required → completing → completed, or cancelled. The ceremony and proof last ten minutes; resending does not extend the original ceremony. Limits: five failed proofs per request, ten failed proofs and five sends per member per 15 minutes, 60 seconds between sends; IP limits are ten sends per 15 minutes and 60 verification requests per five minutes. A new transaction does not reset member budgets. IP throttling can return slow_down (429). Resend replaces previous proofs. Wrong bound proof is invalid_grant; malformed input is invalid_request.

Use status after a lost verify response; a completed code remains one-use and expires 120 seconds after issuance. In this family's current response projection, expires_in remains the ceremony's remaining lifetime even when completed; it is not a fresh code lifetime, and a status read does not renew the code. Redeem promptly; an expired code requires new sign-in. completing without a recoverable completion is ambiguous: start fresh rather than replay issuance. Cancel is rejected with 409 once issuance began. After email proof, token pickup may require the tenant email 2FA preference; email proof alone never resets a factor.

me projection

POST /me requires selected active session handles and endpoint proof. sub is the tenant member UUID. With profile scope, nonempty fields may include name (system login name), display_name (editable name), picture, locale, zoneinfo, website. preferred_username is intentionally absent. Hosted userinfo has its own mapping and is not changed by this projection.

With email scope, a unique active primary address supplies email and an explicit email_verified boolean. Unverified primary email is not silently verified. Missing or ambiguous primary rows omit both fields. Never select an arbitrary email from an ambiguous result.

{"sub":"cccccccc-cccc-4ccc-8ccc-cccccccccccc","name":"sample.member","display_name":"Sample Member","email":"member@example.org","email_verified":true}

No profile/email scope means those optional fields are absent. Expired active access can produce invalid_token (401): refresh/ACK once then retry. Revoked membership, groups or session admission require resolving that condition or fresh sign-in, not a hidden account switch.