AETHERLABS

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.

ScopeWhat it does
openidMarks 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.
readResource scope — see the warning below.
writeResource scope.
adminResource 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:

PropertyValue
Lifetime24 hours
issaetherlabs-sso
audaetherlabs
Claim for the user iduserId (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.

PropertyValue
Lifetime30 days
RotationEvery refresh issues a new refresh token and revokes the old row
ReusePresenting an already-rotated token is treated as theft: that client's whole session family for the user is revoked
RevocationVia /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.

Pattern A does not use these endpoints. Its cookies are shorter-lived than the tokens they carry:

CookieContainsCookie lifetimeToken lifetime
accessTokenaccess JWT1 hour24 hours
refreshTokenrefresh JWT7 days30 days

Drive your own expiry from the token's exp claim, not the cookie.

See also

On this page