跳到主要内容

Sessions and sign-in

Endpoints that turn a sign-in into a session, report on that session, end it, and produce a credential for methods that cannot sign in directly.

MethodPathCredential
POST/auth/verifyID token in the body
POST/auth/session/refreshSession cookie
POST/auth/logoutNone
POST/auth/project-accessSession cookie
POST/auth/exchangeSingle-use code in the body
POST/auth/otp/sendNone
POST/auth/otp/verifyNone
POST/auth/otp/sign-inNone
GET/auth/bridge/None
GET/auth/bridge/:provider/startNone
GET/auth/bridge/:provider/callbackState cookie set at start

POST /auth/verify

Verifies an ID token, resolves the person behind it, checks approval and the project grant, and sets the session cookie.

Request body.

FieldTypeRequiredDetail
idTokenstringYesID token from akn ID sign-in
projectstringYesProject the caller is gating on

Approved response, 200. The session cookie is set on this response.

{
"approved": true,
"uid": "…",
"personId": "…",
"email": "[email protected]",
"name": "…",
"role": "user",
"projects": ["startpage"],
"groups": []
}

Refused responses.

ConditionStatusBody
Account not approved200{"approved":false,"reason":"pending"}
Approved, no grant for project200{"approved":false,"reason":"no_project_access"}
A field is missing400{"error":"idToken and project required"}
Sign-in proved a phone number only, and that number is on no account403{"approved":false,"reason":"phone_not_linked","error":"…"}
Token does not verify401{"error":"Invalid token"}

A 200 with approved: false sets no cookie. Test the field, not the status code.

The first successful verification for an unrecognised sign-in creates a person record with status pending. Phone is the exception: it resolves an account that already carries the number and refuses otherwise.

POST /auth/session/refresh

Reports whether the session cookie is still valid, and returns the person behind it. Takes no body.

Response, 200.

{
"authenticated": true,
"uid": "…",
"personId": "…",
"email": "[email protected]",
"name": "…",
"role": "user",
"projects": ["startpage"],
"groups": []
}

An X-Remote-User response header carries the email address when one is known.

ConditionStatusBody
No cookie, or the cookie does not verify401{"authenticated":false}

The cookie lifetime is unchanged by this call.

POST /auth/logout

Clears the session cookie. Takes no body and no credential. Always answers 200 with {"ok":true}.

The cookie is cleared in the calling browser. Sessions held elsewhere continue until they expire.

POST /auth/project-access

Records that the signed-in person reached a project.

FieldTypeRequired
projectstringYes
ConditionStatusBody
Recorded200{"ok":true}
Cookie or project missing400{"error":"missing params"}
Cookie does not verify401{"error":"Unauthorized"}

POST /auth/exchange

Redeems a single-use code for the person it was issued to. The code is consumed whether or not the person turns out to be approved.

FieldTypeRequired
codestringYes

Response, 200.

{
"uid": "…",
"personId": "…",
"email": "[email protected]",
"name": "…",
"role": "user",
"projects": ["startpage"]
}
ConditionStatusBody
code missing400{"error":"code required"}
Code unknown401{"error":"Invalid code"}
Code already redeemed401{"error":"Code already used"}
Code past its expiry401{"error":"Code expired"}
Code resolves to nothing401{"error":"User not found"} or {"error":"Person not found"}
Person not approved403{"error":"Account not approved","reason":"pending"}

Email codes

Three endpoints implement sign-in by a six-digit code sent to an email address. The purpose field selects between them.

PurposeMeaning
signinDefault. An existing person proves control of an address already on their record
request-accessA stranger proves an address is real so an account request can be reviewed
PropertyValue
Code formatSix digits
Lifetime10 minutes
AttemptsFive per code, after which the code is destroyed
ReuseSingle use. Sign-in consumes it
ResendOne code per address per 60 seconds
VolumeFive codes per address per hour

POST /auth/otp/send

FieldTypeRequiredDetail
emailstringYesAddress to send to
purposestringNosignin or request-access. Defaults to signin
ConditionStatusBody
Sent200{"ok":true}
purpose is signin and no account holds the address200{"ok":true}
Address is malformed400{"error":"A valid email is required"}
Resent inside 60 seconds429{"error":"A code was just sent. Please wait a minute."}
Hourly limit reached429{"error":"Too many codes requested. Try again later."}
Mail delivery not configured503{"error":"Email is not configured"}
Delivery failed500{"error":"Could not send a code"}

A sign-in request for an address with no account is answered identically to a successful send. A caller cannot use this endpoint to test whether an address has an account.

POST /auth/otp/verify

Checks a code without consuming it.

FieldTypeRequired
emailstringYes
otpstringYes
purposestringNo
ConditionStatusBody
Code correct200{"valid":true}
Code wrong, or the purpose does not match400{"valid":false,"error":"Invalid code"}
Code expired400{"valid":false,"error":"That code expired. Request a new one."}
Attempt limit reached400{"valid":false,"error":"Too many attempts. Request a new code."}

A wrong code consumes an attempt. An expired code or an exhausted attempt count destroys the code.

POST /auth/otp/sign-in

Consumes the code and returns a custom token for the browser to exchange for an ID token. The purpose is always signin.

FieldTypeRequired
emailstringYes
otpstringYes
ConditionStatusBody
Signed in200{"customToken":"…"}
Code wrong, expired or exhausted400{"error":"…"}
Address holds no account403{"error":"No account for that address"}
Account not approved403{"error":"Your account is pending"}
Sign-in failed500{"error":"Sign-in failed"}

Exchange the custom token for an ID token, then call POST /auth/verify to open a session.

Bridged providers

A bridge carries a sign-in method that cannot be used directly, and finishes by handing a custom token to https://id.akn.me.uk.

ProviderIdentifierState
akn ID, and the providers behind itzitadelAvailable when configured. Select one with the idp parameter
SteamsteamAvailable
InstagraminstagramNot available. Registered and permanently disabled

Accepted idp values on the zitadel bridge are google, linkedin, github, apple and aknid. The browser is sent straight to the provider.

GET /auth/bridge/

Lists the bridges that are usable. Disabled bridges are omitted.

{ "providers": [ { "id": "steam", "label": "Steam", "start": "/auth/bridge/steam/start" } ] }

GET /auth/bridge/:provider/start

ParameterInRequiredDetail
providerPathYesIdentifier from the table above
redirectQueryNoWhere to send the browser afterwards. Defaults to https://id.akn.me.uk/account
idpQueryNoProvider to select on the zitadel bridge

Answers 302 to the provider. A state cookie scoped to /auth/bridge is set and is valid for 10 minutes.

redirect must be https on one of the akn hostnames. Any other destination is refused.

GET /auth/bridge/:provider/callback

Called by the provider. Verifies the assertion, resolves or creates the person, and answers 302 to https://id.akn.me.uk/ carrying customToken and redirect query parameters.

ConditionStatusResponse
Unknown provider404HTML page
Provider disabled503HTML page
redirect not allowed400HTML page
State missing, altered or older than 10 minutes400HTML page
Provider did not confirm the sign-in401HTML page
Provider returned no stable account identifier502HTML page
Anything else500HTML page

Bridge errors are HTML, not JSON. A caller parsing the response as JSON fails on every error path.

A bridged sign-in that matches no existing person creates one with status pending. It never self-approves.