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.
Email-code and magic-link sign-in
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.