diff --git a/docusaurus/docs/cms/api/session-manager.md b/docusaurus/docs/cms/api/session-manager.md new file mode 100644 index 0000000000..31a66d2481 --- /dev/null +++ b/docusaurus/docs/cms/api/session-manager.md @@ -0,0 +1,686 @@ +--- +title: Session Manager API +description: Programmatic API for issuing, rotating, validating, and revoking origin-scoped authentication sessions from the Strapi backend. +displayed_sidebar: cmsSidebar +tags: + - API + - Session Manager + - authentication + - JWT +--- + +# Session Manager API + + + +The Session Manager API is available at `strapi.sessionManager`. Use it from the backend or a plugin to issue short-lived access tokens and longer-lived refresh tokens, scoped to an origin such as `admin` or `users-permissions`. + + + +The Session Manager powers [admin panel session management](/cms/configurations/admin-panel#session-management) and [Users & Permissions refresh-token mode](/cms/features/users-permissions#jwt-management-modes). HTTP endpoints for those products are documented on their own pages. This page describes the JavaScript API for custom controllers, services, and plugins. + +Access tokens are JWTs. Send them as `Authorization: Bearer `. Refresh tokens are also JWTs. Admin stores refresh tokens in an HTTP-only cookie. Users & Permissions can return them in the response body or in a cookie, depending on configuration. + +:::note Built-in HTTP APIs +End-user session list and revoke flows use the [Users & Permissions REST API](/cms/features/users-permissions/rest-api#session-management). Admin users manage devices from the admin profile, not from this API. +::: + +## Access the API + +`strapi.sessionManager` is registered at startup. Call it with an origin name to get an origin-scoped manager: + +```js +const adminSessions = strapi.sessionManager('admin'); +const upSessions = strapi.sessionManager('users-permissions'); +``` + +The origin string must already be registered with [`defineOrigin()`](#defineorigin). Strapi registers `admin` and `users-permissions` during bootstrap. A missing origin throws: + +``` +SessionManager: Origin '' is not defined. Please define it using defineOrigin('', config). +``` + +## Origins and configuration + +Each origin has its own JWT keys and lifespans. Strapi stores every origin's rows in the hidden `admin::session` content-type (database table `strapi_sessions`). Rows are isolated by the `origin` field. + +| Origin | Registered by | Typical config | +| --- | --- | --- | +| `admin` | Admin server bootstrap | [`admin.auth.sessions`](/cms/configurations/admin-panel#session-management) and `admin.auth.secret` | +| `users-permissions` | Users & Permissions bootstrap | [`plugin::users-permissions` session keys](/cms/features/users-permissions#jwt-management-modes) | + +`defineOrigin()` accepts the following fields: + +| Field | Type | Description | +| --- | --- | --- | +| `jwtSecret` | string | Symmetric signing secret. Required for `HS256` (the default algorithm). | +| `accessTokenLifespan` | number | Access token lifetime, in seconds. | +| `maxRefreshTokenLifespan` | number | Maximum lifetime of a refresh-token family, in seconds. | +| `idleRefreshTokenLifespan` | number | Idle timeout for `type: 'refresh'` tokens, in seconds. | +| `maxSessionLifespan` | number | Maximum lifetime of a `type: 'session'` family, in seconds. | +| `idleSessionLifespan` | number | Idle timeout for `type: 'session'` tokens, in seconds. | +| `algorithm` | string | JWT algorithm. Default: `HS256`. | +| `jwtOptions` | object | Extra options passed to `jsonwebtoken`. For `RS*`, `ES*`, and `PS*` algorithms, set `privateKey` (signing) and `publicKey` (verification). | + +Asymmetric algorithms read keys from `jwtOptions.privateKey` and `jwtOptions.publicKey`. They do not use `jwtSecret`. + +### Token types + +`generateRefreshToken()` accepts `type: 'refresh'` (default) or `type: 'session'`. The type selects which idle and max lifespans apply: + +| `type` | Idle lifespan | Max lifespan | +| --- | --- | --- | +| `refresh` | `idleRefreshTokenLifespan` | `maxRefreshTokenLifespan` | +| `session` | `idleSessionLifespan` | `maxSessionLifespan` | + +Admin uses `refresh` when `rememberMe` is true and `session` otherwise. Both types still issue a JWT whose payload `type` is `'refresh'`. Access tokens use payload `type: 'access'`. + +### Session records + +Active sessions are database rows. Typical fields include: + +| Field | Description | +| --- | --- | +| `userId` | User identifier stored as a string. | +| `sessionId` | Opaque id embedded in the refresh JWT. | +| `deviceId` | Optional device family. Used for targeted invalidation. | +| `origin` | Origin that created the row. | +| `type` | `'refresh'` or `'session'`. | +| `status` | `'active'`, `'rotated'`, or `'revoked'`. | +| `metadata` | Origin-defined object. The Session Manager stores it as-is and does not interpret it. | +| `expiresAt` | Idle expiry. | +| `absoluteExpiresAt` | Family expiry. Rotation copies this value to the child row. | +| `childId` | Session id of the rotated successor, when present. | + +[`rotateRefreshToken()`](#rotaterefreshtoken) marks the previous row as `rotated` and creates a child. [`listSessions()`](#listsessions) returns only `status: 'active'` rows, so each login family appears once. + +Expired rows are deleted in batches about every 50 Session Manager calls. [`isSessionActive()`](#issessionactive) also deletes a row that has already expired. + +## Method overview + +Call methods on an origin-scoped manager, except `defineOrigin()`, `hasOrigin()`, and `generateSessionId()`, which live on `strapi.sessionManager` itself. + +| Method | Purpose | +| --- | --- | +| [`generateRefreshToken()`](#generaterefreshtoken) | Create a session row and a refresh JWT. | +| [`generateAccessToken()`](#generateaccesstoken) | Issue an access JWT from a valid refresh JWT. | +| [`rotateRefreshToken()`](#rotaterefreshtoken) | Replace a refresh JWT and keep the same family expiry. | +| [`validateAccessToken()`](#validateaccesstoken) | Verify an access JWT (synchronous). | +| [`validateRefreshToken()`](#validaterefreshtoken) | Verify a refresh JWT and the backing session row. | +| [`invalidateRefreshToken()`](#invalidaterefreshtoken) | Delete sessions for a user, optionally limited to one device. | +| [`listSessions()`](#listsessions) | List active sessions for a user. | +| [`revokeSessionById()`](#revokesessionbyid) | Delete one session owned by the user and origin. | +| [`isSessionActive()`](#issessionactive) | Return whether a session exists and is not expired. | +| [`defineOrigin()`](#defineorigin) | Register origin configuration (root API). | +| [`hasOrigin()`](#hasorigin) | Check whether an origin is registered (root API). | +| [`generateSessionId()`](#generatesessionid) | Generate a random session id (root API). | + +## Origin methods + +The following methods are called on `strapi.sessionManager('')`. + +### `generateRefreshToken()` + +Creates a session row, then signs a refresh JWT that includes `userId`, `sessionId`, `type: 'refresh'`, `iat`, and `exp`. + +undefined when the origin does not track devices.' }, + { name: 'options.type', type: "'refresh' | 'session'", required: false, description: 'Selects idle and max lifespans. Default: refresh.' }, + { name: 'options.metadata', type: 'object', required: false, description: 'Free-form data persisted on the row (for example device label).' }, + ]} +> + + + + +```js +const sessions = strapi.sessionManager('users-permissions'); + +const { token, sessionId, absoluteExpiresAt } = await sessions.generateRefreshToken( + String(user.id), + deviceId, + { + type: 'refresh', + metadata: { deviceName: 'CLI' }, + } +); +``` + + + + + + + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "sessionId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", + "absoluteExpiresAt": "2026-09-26T12:00:00.000Z" +} +``` + + + + + + +### `generateAccessToken()` + +Validates the refresh JWT and the active session row, then signs a short-lived access JWT. On failure the return value is `{ error: 'invalid_refresh_token' }` instead of throwing. + +generateRefreshToken() or rotateRefreshToken().' }, + ]} +> + + + + +```js +const result = await strapi.sessionManager('admin').generateAccessToken(refreshToken); + +if ('error' in result) { + throw new Error(result.error); +} + +const accessToken = result.token; +``` + + + + + + + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + + + + +```json +{ + "error": "invalid_refresh_token" +} +``` + + + + + + +### `rotateRefreshToken()` + +Creates a child session, marks the current row as `rotated`, and returns a new refresh JWT. Idle and max windows are enforced against the current row. If the parent already has a `childId`, the same child token is returned again. + + + + + + +```js +const rotated = await strapi.sessionManager('users-permissions').rotateRefreshToken( + refreshToken +); + +if ('error' in rotated) { + throw new Error(rotated.error); +} +``` + + + + + + + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "sessionId": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5", + "absoluteExpiresAt": "2026-09-26T12:00:00.000Z", + "type": "refresh" +} +``` + + + + +```json +{ + "error": "invalid_refresh_token" +} +``` + + + + +:::note Rotation errors +`rotateRefreshToken()` can also return `{ error: 'idle_window_elapsed' }` or `{ error: 'max_window_elapsed' }`. +::: + + + +### `validateAccessToken()` + +Verifies the JWT signature, algorithm, and payload `type: 'access'`. This method is synchronous and does not read the database. A revoked session can still present a valid access token until that token expires. Pair it with [`isSessionActive()`](#issessionactive) when you need the row to still exist. + + + + + + +```js +const result = strapi.sessionManager('admin').validateAccessToken(accessToken); + +if (!result.isValid) { + return; +} + +const { userId, sessionId } = result.payload; +``` + + + + + + + +```json +{ + "isValid": true, + "payload": { + "userId": "1", + "sessionId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", + "type": "access", + "iat": 1756281600, + "exp": 1756283400 + } +} +``` + + + + +```json +{ + "isValid": false, + "payload": null +} +``` + + + + + + +### `validateRefreshToken()` + +Verifies the refresh JWT, then loads the session row. The row must exist, belong to the same `userId`, have `status: 'active'`, and be within `expiresAt` and `absoluteExpiresAt`. + + + + + + +```js +const validation = await strapi.sessionManager('admin').validateRefreshToken( + refreshToken +); + +if (!validation.isValid) { + return; +} +``` + + + + + + + +```json +{ + "isValid": true, + "userId": "1", + "sessionId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4" +} +``` + + + + +```json +{ + "isValid": false +} +``` + + + + + + +### `invalidateRefreshToken()` + +Deletes session rows for the origin and user. Pass `deviceId` to limit deletion to that device family. Omit it to delete every session for the user on this origin. + + + + + + +```js +await strapi.sessionManager('users-permissions').invalidateRefreshToken( + String(user.id) +); + +await strapi.sessionManager('users-permissions').invalidateRefreshToken( + String(user.id), + deviceId +); +``` + + + + + + + +```json +{} +``` + + + + + + +### `listSessions()` + +Returns active sessions for the user and origin, newest first. + + + + + + +```js +const sessions = await strapi.sessionManager('admin').listSessions(String(user.id)); +``` + + + + + + + +```json +[ + { + "userId": "1", + "sessionId": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", + "deviceId": "3f2e1d0c-9b8a-7c6d-5e4f-3210fedcba98", + "origin": "admin", + "type": "refresh", + "status": "active", + "metadata": { "deviceName": "Chrome on macOS" }, + "expiresAt": "2026-09-09T12:00:00.000Z", + "absoluteExpiresAt": "2026-09-26T12:00:00.000Z" + } +] +``` + + + + + + +### `revokeSessionById()` + +Deletes one session when the row belongs to the given user and origin. Returns `true` if a matching row was deleted. + + + + + + +```js +const revoked = await strapi.sessionManager('admin').revokeSessionById( + String(user.id), + sessionId +); +``` + + + + + + + +```json +true +``` + + + + + + +### `isSessionActive()` + +Returns `true` when a row exists for the origin and `expiresAt` is still in the future. If the row exists but has expired, the method deletes it and returns `false`. + + + + + + +```js +const access = strapi.sessionManager('admin').validateAccessToken(accessToken); + +if (!access.isValid) { + return; +} + +const active = await strapi.sessionManager('admin').isSessionActive( + access.payload.sessionId +); +``` + + + + + + + +```json +true +``` + + + + + + +## Root methods + +The following methods are called on `strapi.sessionManager` without an origin argument. + +### `defineOrigin()` + +Registers JWT and lifespan configuration for an origin. Call this during plugin bootstrap before issuing tokens. Calling it again with the same origin name replaces the previous configuration. + +```js +strapi.sessionManager.defineOrigin('my-plugin', { + jwtSecret: strapi.config.get('plugin::my-plugin.jwtSecret'), + accessTokenLifespan: 30 * 60, + maxRefreshTokenLifespan: 30 * 24 * 60 * 60, + idleRefreshTokenLifespan: 14 * 24 * 60 * 60, + maxSessionLifespan: 24 * 60 * 60, + idleSessionLifespan: 2 * 60 * 60, + algorithm: 'HS256', +}); +``` + +### `hasOrigin()` + +Returns whether `defineOrigin()` has been called for the name. + +```js +if (!strapi.sessionManager.hasOrigin('my-plugin')) { + throw new Error('Session origin my-plugin is not configured'); +} +``` + +### `generateSessionId()` + +Returns a 32-character hex string. `generateRefreshToken()` already calls this internally. + +```js +const sessionId = strapi.sessionManager.generateSessionId(); +``` + +## Custom origin example + +The following plugin bootstrap registers an origin and issues tokens for a custom user id: + +```js +module.exports = { + async bootstrap({ strapi }) { + strapi.sessionManager.defineOrigin('my-plugin', { + jwtSecret: strapi.config.get('plugin::my-plugin.jwtSecret'), + accessTokenLifespan: 30 * 60, + maxRefreshTokenLifespan: 30 * 24 * 60 * 60, + idleRefreshTokenLifespan: 14 * 24 * 60 * 60, + maxSessionLifespan: 24 * 60 * 60, + idleSessionLifespan: 2 * 60 * 60, + }); + }, +}; +``` + +```js +const origin = strapi.sessionManager('my-plugin'); + +const { token: refreshToken } = await origin.generateRefreshToken( + userId, + deviceId, + { type: 'refresh' } +); + +const access = await origin.generateAccessToken(refreshToken); +``` + +`admin.auth.secret` is still required at startup when the admin panel is served. API-only apps can set [`serveAdminPanel: false`](/cms/configurations/admin-panel#admin-panel-behavior) so that check is skipped. Users & Permissions can reuse `admin.auth.secret` when `jwtSecret` is unset. + +## What's next? + + + + + + diff --git a/docusaurus/docs/cms/backend-customization/examples/authentication.md b/docusaurus/docs/cms/backend-customization/examples/authentication.md index c8651e5ff1..1d03ad5908 100644 --- a/docusaurus/docs/cms/backend-customization/examples/authentication.md +++ b/docusaurus/docs/cms/backend-customization/examples/authentication.md @@ -151,7 +151,7 @@ export default Login; ## Enhanced authentication with session management -The above example uses the traditional JWT approach. For enhanced security, you can enable session management mode in your Users & Permissions configuration, which provides shorter-lived access tokens and refresh token functionality. +The above example uses the traditional JWT approach. You can enable session management mode in your Users & Permissions configuration to use shorter-lived access tokens and refresh tokens. Custom backend code can also call the [Session Manager API](/cms/api/session-manager) directly. ### Configuration diff --git a/docusaurus/docs/cms/configurations/admin-panel.md b/docusaurus/docs/cms/configurations/admin-panel.md index 9a1000783e..12c059992c 100644 --- a/docusaurus/docs/cms/configurations/admin-panel.md +++ b/docusaurus/docs/cms/configurations/admin-panel.md @@ -289,9 +289,9 @@ Additional configuration parameters are available for [session management](#sess ### Session management -Admin authentication uses session management by default for enhanced security. +Admin authentication uses session management by default. -Session management provides enhanced security for authentication in Strapi applications by using short-lived access tokens paired with longer-lived refresh tokens. This approach reduces the risk of token theft and allows for more granular control over user sessions. +Session management uses short-lived access tokens paired with longer-lived refresh tokens. This reduces the risk of token theft and allows more granular control over user sessions. The same core service is exposed to plugins and custom backend code as the [Session Manager API](/cms/api/session-manager). :::caution Serve the admin panel over HTTPS Since v5.24.0, Strapi stores admin authentication data in secure, HTTP-only cookies. Browsers only accept and send these cookies over HTTPS connections, so attempting to access the admin panel via plain HTTP prevents the session cookie from being set and results in failed logins. Always expose the admin panel through HTTPS in production (for example, by placing Strapi behind a TLS-terminating proxy or load balancer). Local development continues to work with the default configuration because cookies are not marked as secure in that environment. @@ -315,9 +315,9 @@ To configure session lifespans and behavior, use the following parameters: | `auth.sessions` | Session management configuration | object | `{}` | | `auth.sessions.accessTokenLifespan` | Access token lifespan in seconds | number | `1800` (30 minutes) | | `auth.sessions.maxRefreshTokenLifespan` | Maximum refresh token lifespan in seconds | number | `2592000` (30 days, or legacy `expiresIn` value) | -| `auth.sessions.idleRefreshTokenLifespan` | Idle refresh token timeout in seconds | number | `604800` (7 days) | -| `auth.sessions.maxSessionLifespan` | Maximum session duration in seconds | number | `2592000` (30 days, or legacy `expiresIn` value) | -| `auth.sessions.idleSessionLifespan` | Session idle timeout in seconds | number | `3600` (1 hour) | +| `auth.sessions.idleRefreshTokenLifespan` | Idle refresh token timeout in seconds | number | `1209600` (14 days) | +| `auth.sessions.maxSessionLifespan` | Maximum session duration in seconds | number | `86400` (1 day, or legacy `expiresIn` value) | +| `auth.sessions.idleSessionLifespan` | Session idle timeout in seconds | number | `7200` (2 hours) | ### Cookie configuration diff --git a/docusaurus/docs/cms/features/users-permissions.md b/docusaurus/docs/cms/features/users-permissions.md index 3f5537b41c..76c79898d1 100644 --- a/docusaurus/docs/cms/features/users-permissions.md +++ b/docusaurus/docs/cms/features/users-permissions.md @@ -311,7 +311,7 @@ export default ({ env }) => ({ -In `refresh` mode, authenticated end users can [list their active sessions](/cms/features/users-permissions/rest-api#list-sessions) and [revoke a session](/cms/features/users-permissions/rest-api#revoke-a-session) through the REST API. +In `refresh` mode, authenticated end users can [list their active sessions](/cms/features/users-permissions/rest-api#list-sessions) and [revoke a session](/cms/features/users-permissions/rest-api#revoke-a-session) through the REST API. Plugins and custom backend code can use the same origin through the [Session Manager API](/cms/api/session-manager) (`users-permissions`). ### Registration configuration diff --git a/docusaurus/docs/cms/features/users-permissions/rest-api.md b/docusaurus/docs/cms/features/users-permissions/rest-api.md index d45ae2b97f..08f81b5634 100644 --- a/docusaurus/docs/cms/features/users-permissions/rest-api.md +++ b/docusaurus/docs/cms/features/users-permissions/rest-api.md @@ -361,7 +361,7 @@ If the username derived from the provider profile already exists, a unique usern ## Session management -When session management is enabled (`jwtManagement: 'refresh'` in the plugin configuration), these additional endpoints become available. They return 404 when the default legacy JWT mode is active. +When session management is enabled (`jwtManagement: 'refresh'` in the plugin configuration), these additional endpoints become available. They return 404 when the default legacy JWT mode is active. The endpoints call the `users-permissions` origin of the [Session Manager API](/cms/api/session-manager). ### Refresh token diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index 3f9bb0a4d6..92dd23bdde 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -241,6 +241,7 @@ const sidebars = { 'cms/api/document-service/publication-filter', ], }, + 'cms/api/session-manager', ], }, { // Configurations