# Sessions and Tokens

From [Descope Fundamentals](https://learn.descope.io/c/foundations) — Learn the platform by configuring your own project — every module ends in work the app checks for real.

Take this course interactively at https://learn.descope.io/c/foundations/sessions-and-tokens

---

![Refresh, Rotation, and Inactivity. 3 minutes.](video:sessions-and-tokens-refresh-rotation-and-inactivity)

Timeouts are only half the picture. Two further controls — refresh token rotation and session inactivity — tighten security beyond lifetime alone, and a settings hierarchy lets you apply different policies per tenant.

With Refresh Token Rotation enabled, every use of a refresh token replaces it with a new one, and the old token is invalidated immediately rather than eventually. That turns a stolen refresh token into a self-reporting problem: Descope's automatic reuse detection treats the presentation of an already-rotated token as a strong signal it was copied, and invalidates the associated tokens. Without rotation, a leaked refresh token stays usable by whoever holds it until it naturally expires.

Session inactivity detects idle sessions and closes them automatically, protecting data left open on an unattended device. Enabling it changes client SDK behavior: they must periodically refresh in the background to keep a genuinely active session alive. The SDK schedules a refresh heartbeat at 10% of the configured inactivity timeout — a 30-minute window produces a heartbeat roughly every 3 minutes. When an app returns to the foreground the SDK refreshes if the session expired; if the refresh token expired while backgrounded, the user authenticates again. With inactivity disabled, refresh cadence follows the session token's own timeout instead.

Every timeout you shorten trades convenience for a smaller window of exposure. A short Session Token Timeout limits how long a leaked session token is useful, at the cost of more frequent silent refreshes. A short Refresh Token Timeout forces fuller re-authentication: safer, rougher for infrequent users.

## Rough starting points

- Administrative or highly sensitive sessions — session tokens 15–60 minutes, refresh tokens around 1–7 days, inactivity 15–30 minutes.
- Standard end-user sessions — session tokens 60–120 minutes, refresh tokens 7–30 days.
- Refresh Token Rotation is worth enabling broadly, since it detects theft rather than merely limiting its window.

> [!WARNING]
> A tenant's session policy can only be more restrictive than the project default, never looser. If one flow needs a different refresh duration — a step-up path, or a partner-specific login — override it per-flow with the Custom Claims flow action rather than loosening the project-wide setting.

## Key terms

- Refresh Token Rotation — issues a new refresh token on every use and invalidates the previous one.
- Reuse detection — flags and invalidates tokens when an already-rotated refresh token is presented again.
- Session inactivity — closes sessions after a configured period with no user activity.
- Refresh heartbeat — the periodic background refresh at 10% of the inactivity timeout.

![Project → Session Management, where every lifetime in this lesson is set. Four weeks of refresh token against ten minutes of session token is the usual shape: the long one is revocable, the short one is not. Refresh token rotation and session inactivity detection are both unticked here — neither is on until you turn it on, and the Step Up Token Timeout on this same page is what bounds the `su` claim.](sessions-and-tokens-refresh-rotation-and-inactivity)

## Summary

- What rotation buys beyond a shorter lifetime, and how reuse detection responds to theft
- How enabling inactivity changes SDK refresh behavior
- How to choose timeouts for sensitive versus ordinary sessions
- That tenant policies may only tighten, never loosen

---

![Session Fundamentals. 4 minutes.](video:sessions-and-tokens-session-fundamentals)

> [!NOTE]
> If you use Descope as an OIDC provider, the session token is called an access token in OAuth terminology. Same artifact, different name for the same role.

## Lifetimes — Project Settings → Session Management

- Session Token Timeout — how long the session token stays valid. Must be at least 3 minutes, and can never exceed the Refresh Token Timeout.
- Refresh Token Timeout — how long the refresh token stays valid before the user must fully re-authenticate.

Where tokens live depends on the platform. On web, the Client SDK stores them in memory, browser storage, or a secure cookie: persistTokens controls whether the session token is kept in localStorage (on by default; set false to reduce XSS exposure), and sessionTokenViaCookie stores it in a cookie instead. The refresh token is commonly kept in an HttpOnly cookie so it survives reloads and restarts. On mobile, the Swift, Kotlin, Flutter, and React Native SDKs use the device's secure storage — Keychain on iOS, EncryptedSharedPreferences on Android — so you never handle raw token storage yourself.

The client's job is simple: send the session token with each request and let the SDK handle refreshing. With autoRefresh enabled — the default — the SDK silently exchanges the refresh token for a new session token when the current one expires, and the user notices nothing. If the refresh token has itself expired or been revoked, the exchange fails and the user returns to sign-in. On the backend the same pattern is a single call: validateAndRefreshSession validates the session token and, if expired, transparently refreshes it in one step. That is the version most backend integrations should reach for.

## The four ways a session ends

- The user logs out, revoking the refresh token server-side — logout() for the current session, logoutAll() for every session that user has.
- The refresh token expires on its own, per the Refresh Token Timeout.
- Session inactivity closes it after a configured period with no activity.
- It is revoked server-side — an admin disables the user, or your backend calls the logout API.

## Key terms

- Session — the authenticated period between sign-in and logout.
- Session token — the short-lived JWT sent with API requests (an access token under OIDC).
- Refresh token — the longer-lived credential exchanged for a new session token.
- HttpOnly cookie — a cookie JavaScript cannot read, commonly used for the refresh token on web.

![Project Settings then Session Management, where every lifetime in this module is set. Refresh Token Timeout governs how long a session can be renewed for, Session Token Timeout how long each issued token lasts, and the two cannot cross -- the session token may never outlive the refresh token. Refresh token rotation and session inactivity are switches on this same page.](sessions-and-tokens-session-fundamentals)

## Summary

- Why Descope uses two tokens rather than one, and what each is for
- That Session Token Timeout is at least 3 minutes and never exceeds the refresh timeout
- That storage is platform-specific and the SDKs manage it for you
- The four ways a session ends — and that there is no fifth

---

![Step-Up Sessions. 3 minutes.](video:sessions-and-tokens-step-up-sessions)

A session says the user signed in. It does not say they signed in *just now*, and for some actions that difference matters: a session established this morning on a laptop somebody then walked away from is a perfectly valid session and a poor reason to move money.

Step-up is asking the user to prove themselves again, mid-session, before one particular action. They do not sign in again — they already are signed in. They re-authenticate, and the session they already hold is upgraded for a short window.

The proof is a claim on the session token called `su`. When a step-up succeeds, Descope reissues the token carrying `su`, and that claim expires on its own after the project's Step Up Token Timeout — independently of the session, which keeps running as normal. So the elevated state is a property of the next few minutes, not of the user.

Authentication Methods and Risk covers the other half of this: when to demand it, and how risk signals decide that for you. This lesson is about what the token does.

## What people gate behind it

- Financial transactions — making a purchase, moving money
- Accessing sensitive personal information
- Administrative operations
- Changing account settings, such as an email address or password
- Any high-risk action that should not rest on a session established hours ago

There are three ways to implement it, and they are not alternatives so much as different places to put the trigger. Descope ships a pre-built step-up flow, which loads the user from their refresh token, marks the run as a step-up, presents authentication options — magic link, passkeys, social login — and updates the session token on success. Or you trigger it from the Client SDK, or from the Backend SDK, when the decision belongs in code rather than in a journey.

> [!WARNING]
> Checking that the session is valid is not the same as checking that it is elevated. An ordinary, non-stepped-up session validates perfectly well — that is the whole point of it. Validate the session and then read `su` explicitly, or the gate does nothing.

Because the claim expires on its own, the check belongs per sensitive action rather than once per login. That is a different habit from ordinary authorization: a role is true until revoked, whereas an elevated session is true for a few minutes. Treat `su` as a fact about right now, not about this user.

![The Step Up action's settings, with its own description of what it does: load the user from the incoming refresh token and mark the run as a step-up. That marking is the whole mechanism — it is what makes the session token this flow reissues carry `su`. The action's one output, Step Up Initiated, is what the rest of the journey hangs off.](sessions-and-tokens-step-up-sessions)

## Summary

- Why step-up beats making every session more restrictive
- That the proof is the `su` claim, and that it expires on the Step Up Token Timeout
- The three places the trigger can live, and what the pre-built flow does
- That a valid session and an elevated session are different questions
- Why the check belongs per action rather than per login

---

![Token Validation. 2 minutes.](video:sessions-and-tokens-token-validation)

Client and mobile apps never authorize API access themselves; they only hold tokens and forward them. The authorization decision has to happen somewhere users cannot tamper with it — your backend, or a gateway in front of it.

> [!WARNING]
> Client-side helpers like isJwtExpired() are for UI decisions, such as showing a login screen instead of a dashboard. They are a convenience, never a security boundary: a user fully controls their own browser and could return true from that check regardless of the actual token. Only signature validation on your server or gateway counts.

## The mistakes that cause most validation bugs

- Treating a client-side expiry check as authorization.
- Skipping audience validation — without an aud check, a token issued for one application can be replayed against another that trusts the same project.
- Assuming roles are a flat list. Descope nests roles and permissions under a tenants object in multi-tenant JWTs; some gateway policies cannot evaluate nested JSON, so switch the JWT Template's authorization claim format to "Current tenant, no tenant reference" if you need gateway-level role checks.
- Trusting nsec claims on the backend. Claims submitted by the client SDK land there unverified — never use them for authorization decisions.
- Mismanaging the JWKS cache. Fetching it per request adds needless latency; never refreshing it means a key rotation silently breaks validation the moment the old key retires.

Gateway JWT authorizers and the Backend SDK are complementary rather than competing. Use the gateway for edge-level rejection of anything obviously invalid, and the SDK for role and permission logic that needs to understand your domain.

## Key terms

- JWKS — a JSON Web Key Set: the published document containing an issuer's public signing keys.
- JWK — a single public key inside a JWKS, identified by a kid (key ID).
- aud claim — the audience claim, identifying which application a token was issued for.
- JWT authorizer — a gateway feature validating a JWT's signature and standard claims at the edge.

![A JWT template's preview pane, which is the clearest view of what your backend is actually validating. `iss` is your project ID, `sub` the user, `exp` and `iat` the window the token is good for, and `amr` how they authenticated. Note that `roles` and `permissions` appear twice — once at the root for project level, once nested under a tenant ID — so reading the wrong one is a real way to get authorization wrong.](sessions-and-tokens-token-validation)

## Summary

- Why a frontend expiry check cannot protect an API route
- What audience validation actually prevents
- That JWKS enables stateless validation, cached on the token's kid
- Where a gateway ends and the Backend SDK begins

---
