Login and logout for web applications
The complete lifecycle for an external web application: from the first redirect to ScaiKey, through token handling, to a logout that actually logs the user out.
Read this end to end before integrating. Most integration problems we see come from one of two places — the token exchange, or treating logout as a client-side concern.
The mental model: there are two sessions#
This is the single most important idea on the page.
| Session | Owned by | Ended by |
|---|---|---|
| Your application session | You (cookie, server session, JWT in storage) | Your logout handler |
| The ScaiKey SSO session | ScaiKey (an HttpOnly cookie on scaikey.scailabs.ai) |
Only by calling our end-session endpoint |
If you clear only your own session and redirect to your login page, the browser still
holds the ScaiKey SSO cookie. Your login redirect hits /oauth/authorize, ScaiKey sees
a valid session, and issues a fresh authorization code without prompting. The user is
back where they started, apparently never logged out.
A logout that does not call ScaiKey's end-session endpoint is not a logout.
Before you start#
Your application must be registered with ScaiKey. Ask the platform team for:
| Field | What it is |
|---|---|
client_id |
Public identifier for your application. |
client_secret |
Confidential clients only. Never ship this to a browser. |
redirect_uris |
Exact URLs we will return authorization codes to. Exact-matched. |
logout_uris |
Optional. Where we may send users after logout. See Post-logout redirects. |
| Scope | GLOBAL (usable across tenants) or tenant-scoped. Decides which endpoints you use. |
Which endpoint family do you use?#
| Your application is | Use | Discovery document |
|---|---|---|
GLOBAL-scoped |
/api/v1/platform/oauth/* |
$SCAIKEY/api/v1/platform/oauth/.well-known/openid-configuration |
| Tenant-scoped | /api/v1/auth/tenants/{slug}/oauth/* |
$SCAIKEY/api/v1/auth/tenants/{slug}/.well-known/openid-configuration |
Pair them consistently: if you authorize against the platform endpoint, log out against the platform endpoint. Mixing families works but makes debugging harder.
Throughout this page, $SCAIKEY is https://scaikey.scailabs.ai.
Always read endpoints from discovery#
1 | |
1 2 3 4 5 6 7 8 | |
Note that issuer and the endpoint URLs have different base paths. That is
deliberate: issuer must equal the iss claim we mint into tokens, while the endpoints
live under the API prefix. Validate iss against issuer; send requests to the
endpoint URLs. Do not construct endpoint URLs by appending to issuer.
Part 1 — Login#
Step 1: Generate PKCE values#
Required for public clients (SPAs, mobile), recommended for everyone.
1 2 3 4 5 6 | |
Store verifier in the user's pre-login session. You need it at Step 4.
Step 2: Redirect to the authorize endpoint#
1 2 3 4 5 6 7 8 | |
| Parameter | Required | Notes |
|---|---|---|
response_type |
yes | Always code. |
client_id |
yes | |
redirect_uri |
yes | Must exactly match a registered value. |
scope |
yes | Include openid to receive an ID token. Add offline_access for a refresh token. |
state |
yes | Opaque CSRF value. Verify it on the callback. |
code_challenge |
public clients | Base64url SHA-256 of the verifier. |
nonce |
recommended | Echoed into the ID token; bind it to the user's session. |
prompt=login |
optional | Forces re-authentication even if an SSO session exists. |
login_hint |
optional | Pre-fills the username field. |
If the user already has a valid SSO session, ScaiKey returns a code immediately without showing a login screen. That is single sign-on working as intended.
Step 3: Handle the callback#
We redirect to your redirect_uri with code and state.
Verify state matches the value you stored. If it does not, abort — do not exchange
the code.
Step 4: Exchange the code for tokens#
Server-side, within about 60 seconds. Codes are single-use.
1 2 3 4 5 6 7 8 | |
1 2 3 4 5 6 7 8 | |
redirect_uri must be identical to the one from Step 2 — it is verified, not decorative.
Step 5: Validate the ID token#
Before trusting any claim:
- Verify the signature against
jwks_uri(cache the keys; honourkid). - Check
issequals theissuerfrom discovery — exactly, no normalisation. - Check
audcontains yourclient_id. - Check
expis in the future. - If you sent
nonce, check it matches.
Claims you can expect:
| Claim | Meaning |
|---|---|
sub |
Stable user ID. Use this as your foreign key, never the email. |
sid |
The SSO session this login belongs to. Keep it — see Part 3. |
tenant_id |
The user's tenant. |
email, name, given_name, family_name |
Identity claims, subject to scope. |
groups |
Group memberships, if the groups scope was granted. |
auth_time |
When the user actually authenticated. |
Step 6: Establish your own session — and store the ID token#
Create your application session now. Persist the raw id_token alongside it. You
need it at logout as id_token_hint, and it is the most common thing teams forget.
1 2 3 | |
Part 2 — Using and refreshing tokens#
Send the access token to resource servers:
1 | |
When it expires, use the refresh token:
1 2 3 4 5 6 | |
Refresh tokens rotate: each refresh returns a new one and retires the old. Store the new value immediately.
A refresh token is bound to the SSO session that created it. When that session ends —
because the user logged out, an administrator revoked it, or it expired — the refresh
token is revoked with it and further refreshes fail with invalid_grant. Treat that as
"the user must log in again", not as an error to retry.
Part 3 — Logout#
The complete sequence#
- Clear your own application session.
- Redirect the browser to the end-session endpoint, with
id_token_hint. - We terminate the SSO session, revoke refresh tokens, and clear our cookie.
- We redirect the browser back to your
post_logout_redirect_uri.
1 2 3 4 | |
| Parameter | Required | Notes |
|---|---|---|
id_token_hint |
strongly recommended | Identifies the session (sid) and client (aud). Send it even if expired — we verify the signature, not the expiry. |
post_logout_redirect_uri |
optional | Must pass validation below, or it is ignored. |
state |
optional | Echoed back on the redirect. |
client_id |
optional | Fallback identification when you cannot send id_token_hint. |
Make sure you have the right endpoint#
ScaiKey has several routes with "logout" in the path. Only two are OIDC end-session endpoints, and only those accept a browser redirect:
| Route | Method | Purpose |
|---|---|---|
/api/v1/platform/oauth/logout |
GET | OIDC end-session — GLOBAL apps. Use this. |
/api/v1/auth/tenants/{slug}/oauth/logout |
GET | OIDC end-session — tenant-scoped apps. Use this. |
/api/v1/auth/logout |
POST | Portal session API. Not an end-session endpoint. |
/api/v1/auth/tenants/{slug}/logout |
POST | Portal session API. Not an end-session endpoint. |
/api/v1/admin/auth/logout |
POST | Admin console. Not for integrating applications. |
/api/v1/auth/tenants/{slug}/backchannel-logout |
POST | Back-channel logout we call on you. |
The distinguishing feature is the oauth/ segment. If you send a browser to one of the
POST routes you will get 405 Method Not Allowed with Allow: POST, and — because the
navigation fails quietly — a logout that appears to do nothing.
Take the URL from end_session_endpoint in the discovery document and this cannot
happen.
Two rules people get wrong#
Redirect the browser. Do not call this from your server. The endpoint identifies the session from the browser's SSO cookie. A back-channel HTTP call from your backend carries no cookie, ends nothing, and returns a redirect your server then discards. The user stays signed in.
Clearing your session is not enough on its own. See The mental model at the top.
What logout does and does not do#
| Effect | Behaviour |
|---|---|
| ScaiKey SSO session | Terminated. Next authorize forces a fresh login. |
| SSO cookie | Cleared. |
| Refresh tokens | Revoked — those bound to the session, plus those of the client identified by id_token_hint. |
| Access tokens | Not revoked. They are self-contained JWTs and remain valid until exp. |
| Your application session | Your responsibility. |
Because access tokens survive, keep their lifetime short if logout needs to take effect quickly, and use introspection at resource servers when you need certainty:
1 2 | |
Post-logout redirects#
post_logout_redirect_uri is validated. An unaccepted value is ignored and the user
lands on ScaiKey's generic signed-out page instead of returning to your application.
A value is accepted when it either:
- exactly matches one of your registered
logout_uris, or - shares an origin (scheme + host + port) with one of your registered
redirect_uris.
Matching is exact. https://app.example.com does not authorise:
| Rejected | Why |
|---|---|
http://app.example.com/... |
scheme differs |
https://app.example.com.attacker.net/... |
host differs |
https://app.example.com:8443/... |
port differs |
If you send id_token_hint or client_id, we check against that application alone. If
you send neither, we check against every active application in scope — the redirect still
works, but we cannot revoke your client's tokens by client. Send id_token_hint.
Register an explicit logout_uri only when your signed-out page lives on a different
origin than your OAuth callback.
Worked example (Flask)#
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | |
Federated logins#
When a user signs in through an external identity provider (OIDC or SAML), ScaiKey still creates its own SSO session, and everything above applies unchanged.
One caveat: logging out of ScaiKey does not log the user out of the upstream IdP. If that IdP still holds a session, clicking its button again may sign the user straight back in without a prompt. When you need both ended, call the IdP's own end-session endpoint after ours.
Integration checklist#
- Endpoints read from the discovery document, not hardcoded
-
issvalidated againstissuerfrom discovery,audagainst yourclient_id -
stateverified on the callback - PKCE used (mandatory for public clients)
-
subused as the user key, not email -
id_tokenstored at login for use at logout - Logout clears your session and redirects to the end-session endpoint
- Logout is a browser redirect, not a server-side call
-
post_logout_redirect_urion a registered origin - Rotated refresh tokens persisted on every refresh
-
invalid_granton refresh handled as "log in again"
Troubleshooting#
| Symptom | Cause |
|---|---|
| Logout returns the user, still signed in | You are not calling the end-session endpoint, or you are calling it server-side. |
| Logout redirects to ScaiKey's signed-out page, not your app | post_logout_redirect_uri failed validation. Check scheme, host and port against your registered URIs. |
| 404 on a logout or token URL | The URL is missing the /api/v1/... prefix. Read it from discovery. |
405 Method Not Allowed, Allow: POST on logout |
You are on a portal-session route, not the OIDC end-session endpoint. The end-session paths contain oauth/. |
404 on /api/v1/platform/.well-known/openid-configuration |
Fixed 2026-08-22 — both this path and the older /api/v1/platform/oauth/.well-known/... now serve the same document. |
invalid_grant on refresh |
The SSO session ended, or the refresh token was already rotated. Send the user through login. |
invalid_grant on the code exchange |
Code expired, already used, or redirect_uri/code_verifier mismatch. |
| ID token rejected by your library | iss compared against the endpoint base rather than the issuer from discovery. |
| User is signed straight back in after logging out | The SSO session survived — the end-session call did not happen or did not carry the cookie. |