Refresh Tokens & Session Lifecycle
ZewstID issues refresh tokens to OAuth clients that request the
offline_accessLifetimes
| Token | Default lifetime | Notes |
|---|---|---|
| Access token | 15 minutes | RS256-signed JWT. Use until expiry, then refresh. |
| Refresh token | 30 days (sliding) | Each successful refresh issues a new refresh token and resets the 30-day clock. |
| ID token | 15 minutes | Issued 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:
- The old refresh token is invalidated and added to a revocation list.
- A new refresh token is returned in the response.
- 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.comThis 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
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.refresh_token
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
jwtasync 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-nativegetAccessToken()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
| Error | Meaning | Action |
|---|---|---|
invalid_grant | Refresh token expired, revoked, or already used (reuse detection) | Send the user back through the full OAuth login flow |
invalid_client | Wrong client_idclient_secret | Check your env vars |
invalid_scope | Requested scope wasn't granted at original authorization | Don't request scopes during refresh that weren't granted originally |
invalid_request | Malformed request body or missing required parameter | Inspect the request — usually a typo in the form-encoded body |
The error response body always includes a human-readable
error_descriptionSee also
Was this page helpful?
Let us know how we can improve our documentation