Scopes & Tokens
The scopes SSO understands, what each unlocks, and the exact shape and lifetime of the tokens it issues.
Scopes & Tokens
Scopes
A requested scope must be (a) registered on the client and (b) known to
the server. Anything else is rejected with invalid_scope — including a scope
that is valid globally but was never granted to that client.
| Scope | What it does |
|---|---|
openid | Marks the sign-in as OpenID-style. No id_token is issued today, so this is currently informational. |
profile | /api/oauth/userinfo returns the user's name. |
email | /api/oauth/userinfo returns email and email_verified. |
read | Resource scope — see the warning below. |
write | Resource scope. |
admin | Resource scope, retained for a legacy first-party client. |
Where scopes are checked
/authorize,/consent-info,/approve— unknown or unregistered scope →invalid_scope/token— the granted scope is copied onto the issued access and refresh tokens/userinfo— claims are gated per scope
Defaults: omit scope and the whole registered set is used. A client with
no scopes registered falls back to openid profile email.
Resource scopes are recorded, not enforced
read, write and admin appear on the token, but SSO does not use them to
gate its own endpoints. Authorization in this ecosystem is RBAC: read
roles and permissions from /api/auth/me and
enforce them in your own API.
Access tokens
An HS256 JWT signed with the shared JWT_ACCESS_SECRET:
| Property | Value |
|---|---|
| Lifetime | 24 hours |
iss | aetherlabs-sso |
aud | aetherlabs |
| Claim for the user id | userId (not sub — sub appears only in /userinfo) |
{
"userId": "uuid",
"email": "user@example.com",
"name": "Jane Doe",
"roles": ["admin"],
"clientId": "your-client-id",
"scope": "openid profile email",
"iat": 1735689600,
"exp": 1735776000,
"iss": "aetherlabs-sso",
"aud": "aetherlabs"
}clientId and scope are present on OAuth-issued tokens only. A token from a
first-party cookie session has neither.
The token response reports expires_in as the actual remaining lifetime
(86400), not a fixed hour.
Refresh tokens
Also a JWT, signed with JWT_REFRESH_SECRET, and bound to the client that
obtained it — the clientId claim is checked at every refresh, so client A
can never redeem client B's token, nor a first-party session token.
| Property | Value |
|---|---|
| Lifetime | 30 days |
| Rotation | Every refresh issues a new refresh token and revokes the old row |
| Reuse | Presenting an already-rotated token is treated as theft: that client's whole session family for the user is revoked |
| Revocation | Via /api/oauth/revoke, "sign out other sessions", or disconnecting the app |
Always replace the stored refresh token with the one from the response. Using the old value triggers reuse detection and signs the user out everywhere.
First-party cookie sessions
Pattern A does not use these endpoints. Its cookies are shorter-lived than the tokens they carry:
| Cookie | Contains | Cookie lifetime | Token lifetime |
|---|---|---|---|
accessToken | access JWT | 1 hour | 24 hours |
refreshToken | refresh JWT | 7 days | 30 days |
Drive your own expiry from the token's exp claim, not the cookie.
See also
- Errors — what each failure means.
- API reference — endpoint-by-endpoint detail.