Skip to main content

Refresh Tokens & Session Lifecycle

ZewstID issues refresh tokens to OAuth clients that request the

offline_access
scope. This page documents the lifecycle, rotation policy, and reuse-detection behaviour so you can build robust integrations without surprises.


Lifetimes

TokenDefault lifetimeNotes
Access token15 minutesRS256-signed JWT. Use until expiry, then refresh.
Refresh token30 days (sliding)Each successful refresh issues a new refresh token and resets the 30-day clock.
ID token15 minutesIssued once at login; not refreshed automatically. Re-authenticate the user if you need a fresh
id_token
.
Browser session at
auth.zewstid.com
30 days (sliding)Backs cross-portal SSO. Independent of the OAuth refresh token lifetime.

These values are realm-wide defaults. Specific applications can request different values during onboarding if there's a compelling reason (e.g., regulated industries needing shorter sessions).


Rotation policy

ZewstID rotates refresh tokens on every use. After a successful refresh:

  1. The old refresh token is invalidated and added to a revocation list.
  2. A new refresh token is returned in the response.
  3. Your client must persist the new token and discard the old one.

Reuse detection: if the same refresh token is presented twice (e.g., an attacker captured one and the legitimate user already used it), the entire session is terminated. Both the user's active access tokens and the browser session at

auth.zewstid.com
are revoked. The user must re-authenticate.

This is the standard mitigation for refresh-token theft and follows the OAuth 2.1 best practices recommendation.


Refresh request

curl -X POST https://api.zewstid.com/oauth/token \ -d 'grant_type=refresh_token' \ -d 'client_id=app_xxx' \ -d 'client_secret=xxx' \ -d 'refresh_token=eyJhbGc...'

Response:

{ "access_token": "eyJhbGc...", "refresh_token": "eyJhbGc...", // ← NEW token, persist this "expires_in": 900, "refresh_expires_in": 2592000, "token_type": "Bearer", "scope": "openid profile email offline_access" }

Always read

refresh_token
from the refresh response and overwrite your stored token. Forgetting to do this is the #1 cause of "users get logged out unexpectedly after a few days" bugs.


Implementation patterns

Server-side (Node)

Persist refresh tokens in encrypted storage (your session DB, not localStorage). Refresh proactively when an access token has < 60 seconds left:

async function getValidAccessToken(session) { if (Date.now() < session.accessTokenExpiresAt - 60_000) { return session.accessToken; } const fresh = await fetchTokenRefresh(session.refreshToken); await saveSession({ ...session, accessToken: fresh.access_token, refreshToken: fresh.refresh_token, // ← new rotated value accessTokenExpiresAt: Date.now() + fresh.expires_in * 1000, }); return fresh.access_token; }

NextAuth

Use the standard refresh-token rotation callback pattern in

jwt
:

async jwt({ token, account }) { // Initial sign-in if (account) { return { accessToken: account.access_token, refreshToken: account.refresh_token, accessTokenExpires: account.expires_at * 1000, }; } // Token still valid if (Date.now() < token.accessTokenExpires - 60_000) { return token; } // Refresh const refreshed = await refreshAccessToken(token.refreshToken); return { ...token, accessToken: refreshed.access_token, refreshToken: refreshed.refresh_token, accessTokenExpires: Date.now() + refreshed.expires_in * 1000, }; }

Mobile (React Native)

@zewstid/id-react-native
handles rotation automatically. Just call
getAccessToken()
whenever you need to make an authenticated API call — it refreshes silently in the background.


Revocation

To explicitly invalidate a refresh token (e.g., on user logout):

curl -X POST https://api.zewstid.com/oauth/revoke \ -d 'client_id=app_xxx' \ -d 'client_secret=xxx' \ -d 'token=eyJhbGc...' \ -d 'token_type_hint=refresh_token'

Revoking a refresh token also invalidates every access token issued from it.


Common error responses

ErrorMeaningAction
invalid_grant
(refresh)
Refresh token expired, revoked, or already used (reuse detection)Send the user back through the full OAuth login flow
invalid_client
Wrong
client_id
/
client_secret
, or the client was deleted
Check your env vars
invalid_scope
Requested scope wasn't granted at original authorizationDon't request scopes during refresh that weren't granted originally
invalid_request
Malformed request body or missing required parameterInspect the request — usually a typo in the form-encoded body

The error response body always includes a human-readable

error_description
field with details — log it during development.


See also

Was this page helpful?

Let us know how we can improve our documentation