Sessions and tokens
Understand session lifetimes, code exchange, token verification, and revocation.
A session represents one signed-in device. Revoke it to sign that device out, or revoke all of a user’s sessions to sign them out everywhere.
Signing in creates a session and short-lived access tokens. Your backend can verify an access token locally, without making a request to Lepsto for each authenticated request.
The session
A session issues a refresh token that your backend uses to obtain new access tokens. Keep the refresh token out of browser JavaScript. A laptop and a phone have separate sessions; revoking one leaves the other signed in.
A session ends when it is revoked, exceeds its inactivity window, or reaches its absolute age limit. Access tokens last ten minutes by default; the default refresh inactivity window is thirty days and the absolute limit is one year. See Configure authentication for all lifetime settings.
Getting the tokens in the first place
In a callback flow, completion delivers a single-use code to an allowed callback address. The redirecting component sends it by form POST, keeping it out of browser history and referrer headers. The code expires in less than a minute and is bound to the client, the exact callback address, and the PKCE challenge supplied when the flow started.
Your backend exchanges the code using a product public key (lpk_) scoped to POST /code/exchange. This backend credential is separate from the browser’s Users publishable key (upk_). Never include lpk_ in a browser bundle. With @lepsto/sdk/users/server, your application owns these steps:
- Receive the callback and validate
state. CallexchangeCode(code, codeVerifier, { redirectUri })with the saved verifier and exact callback address. The result containsaccessToken,refreshToken,expiresIn, andsession. The exchange checks all three bindings and consumes the code atomically. Reusing it revokes the session it created. - Store the returned tokens and set your first-party session cookie. The library does not set cookies. Keep the refresh token in backend-controlled storage; the quickstart shows a callback handler.
- Authenticate subsequent requests using that session. First-party cookies work independently of third-party cookie support.
Use a __Host- cookie with Secure, Path=/, and no Domain attribute. Add HttpOnly to prevent script access, and choose SameSite for your application’s callback and CSRF design. The quickstart uses Lax for its same-site setup. Do not assume a cross-site POST will carry a SameSite=Lax cookie. Set the cookie lifetime from the returned session.
The two browser handoffs keep the verifier in different places. A headless hook generates it in the browser and exposes handle.pkceVerifier; your code sends that verifier and the returned code to your backend. A redirecting prebuilt SignIn receives a backend-generated challenge, while the backend retains the verifier. Native applications use the exchange without cookies and store tokens in platform secure storage.
The access token
Access tokens are JSON Web Tokens signed with ES256 and a keypair unique to your product. Verification uses the product’s public keys; it does not require a shared signing secret.
Claim | Carries |
|---|---|
iss | Your product’s issuer URL |
aud | Your product |
sub | The stable end-user id — the value you store in your own tables |
sid | The session, so you can tell a user’s devices apart |
iat, exp | Issued and expiry; the lifetime defaults to ten minutes |
aal | How strongly this session is authenticated — a second factor raises it, and so does a passkey the device verified |
amr | Which methods were used, and when — including webauthn for a passkey |
email | The primary address, present only when it is verified |
act | Present only while an operator is impersonating the user |
Tokens also contain a size-limited projection of public user metadata. Private metadata stays out of the token. The claim set is capped at about 1.2 KB. Management writes enforce that limit when metadata is saved, so oversized metadata cannot later prevent sign-in.
Verifying it
Your backend verifies the signature and claims against your product’s published key set. It caches those keys, so ordinary verification does not need a network request.
Create a server client, then call its verifyToken method. The configured public key is used for code exchange; local token verification uses the public key set:
import { createUsersClient } from '@lepsto/sdk/users/server'
const users = createUsersClient({
productId: process.env.LEPSTO_PRODUCT_ID!,
publicKey: process.env.LEPSTO_USERS_PUBLIC_KEY!,
})
const claims = await users.verifyToken(token)
// claims.sub → the user id
// claims.sid → the session
// claims.aal → 'aal2' once a second factor has been used, or after a
// verified passkey sign-in, which is two factors in one gestureexpressMiddleware verifies the bearer token and attaches the claims to the request. For a cookie containing an access token, use a token extractor such as requireAuth(users, (ctx) => ctx.cookies.session). Neither guard exchanges codes, refreshes tokens, or manages cookies. If you implement verification in another language, follow these rules:
- Configure the issuer and key-set URLs yourself. Require an exact
issmatch and look up keys only in that configured set. Never derive trusted URLs from an incoming token. - Ignore
jku,x5u, and embedded header keys. - Accept only
ES256, require the expectedaud, and allow one minute of clock skew. - Cache the key set for at most ten minutes. Refetch when a token has an unknown key ID so signing-key rotation can take effect.
For sensitive application routes, also check aal and the authentication time in amr. These claims let you require a second factor or recent authentication. Lepsto enforces those checks on its own sensitive routes; your application must enforce them on yours.
Refresh and rotation
Call users.refresh(refreshToken) before the access token expires. Each successful refresh replaces the refresh token, so persist the returned token before using it again. The SDK coalesces concurrent refresh calls with the same token.
Reuse detection revokes the affected session and emits an event explaining why. There is one concurrency allowance: reusing the immediately previous refresh token within about ten seconds returns the same successor. An older token, or reuse after that window, revokes the session.
Revoking
Revocation prevents further refreshes. With local verification, an already issued access token remains valid until it expires: ten minutes by default. Lepsto endpoints check the session and reject a revoked session immediately.
Choose the verification mode according to how quickly the route must recognize revocation:
Local verification (default) | Checked verification | |
|---|---|---|
Call | users.verifyToken(token) | users.verifyToken(token, { checkRevoked: true }) |
Network cost per request | none | one call to us |
A revoked session stops working | within the access token lifetime — ten minutes by default | immediately |
Use it on | every ordinary route | the routes that deserve it: admin actions, payments, anything irreversible |
You can also reduce the access-token lifetime to shorten the local-verification window. The minimum is five minutes; a shorter lifetime requires more frequent refreshes.
Users can sign out this device, every other device, or all devices. Signing out other devices or all devices requires fresh authentication, which prevents someone with an unattended session from locking the owner out.
These events revoke all of a user’s sessions regardless of lifetime settings:
- a password change or a password reset
- an email change, and the revert of an email change
- a reset of a second factor
- a ban
- an erasure
After a password change, for example, expect the user to sign in again on their other devices.
Impersonation
An authorized operator can start impersonation from the Impersonation card on a user’s record or through the product API available to an agent. The resulting session includes act, lasts at most thirty minutes, and has no refresh token. It cannot change credentials, factors, addresses, or sessions.
The audit trail records the operator. If impersonation notifications are enabled, the user also receives an email. Check session.isImpersonated before allowing actions your application should withhold from a support operator.
Stability
The token and flow contract supports additive changes: new claims, statuses, and optional fields can appear without changing existing meanings. Read the claims your application needs and ignore unknown fields.
Next steps
- Run a sign-in flow to create a session.
- Configure authentication to set lifetimes and credentials.
- Add passkeys for a verified-device sign-in that can reach
aal2. - Receive user events to react to revocation.
- Use the client libraries for server verification options.