Headless Authentication
Headless authentication lets single-page apps and other front ends sign users in with your IdP, then call dotCMS APIs as those users. dotCMS permissions decide what each user can see. You don't need a server-side proxy, a shared API key, or custom user-mapping code.
How It Works#
- Your front end signs the user in with the IdP using a standard OIDC library (Authorization Code + PKCE). It generates a
noncefor that request. - The front end sends the resulting
id_tokenand thenoncetoPOST /api/v1/dotauth/oauth/exchange. - dotCMS checks that the token comes from a trusted IdP, and verifies its signature, issuer, audience, expiry, and nonce, plus the browser origin. It then finds or provisions the user, applies role mapping, and returns a
sessionRef. - The front end sends
Authorization: Bearer <sessionRef>on dotCMS REST and GraphQL requests.
dotCMS never sees the user's password or the authorization code. Each id_token can be exchanged only once.
For every dotAuth endpoint, see Endpoints on the dotAuth reference page.
Headless Settings#
Headless settings apply instance-wide (System scope). Open them with Configure headless on the overview screen. Saving requires an administrator.
Enable token exchange for this scope (default off): "Accept OIDC tokens from trusted IdPs and issue dotCMS sessionRefs for API calls." While this is off, the exchange endpoint returns 503.
SessionRef Lifetime#
| Setting | Default | Description |
|---|---|---|
| SessionRef lifetime (minutes) | 60 | How long an issued sessionRef stays valid. When it expires, the app exchanges a fresh IdP token. The lifetime is fixed from when the sessionRef was issued; using it doesn't extend it. |
| Clamp to IdP exp | On | Never lets a sessionRef outlive the exp claim of the token it came from. Recommended. |
Trusted IdPs#
"Each IdP has its own connection, claim mapping, and provisioning rules." You need at least one enabled trusted IdP. Click Add IdP to add one, and use each IdP's switch to enable or disable it.
| Field | Description |
|---|---|
| Discovery URL (optional) | Your IdP's base URL or full .well-known URL. Click Discover to fill in the issuer and signing details. |
| Display name | A label shown in this list and in audit logs, for example Auth0 — Production. |
| Issuer URL | Must exactly match the iss claim in incoming tokens. |
| JWKS URL | Shown for reference. dotCMS gets signing keys from the issuer's discovery document. |
| Expected audience | The aud claim in your app's tokens, usually the SPA's client ID or API identifier. Discovery doesn't provide this. Set it manually; tokens without a matching audience are rejected. |
| Allowed signing algorithms | Default RS256. dotCMS accepts only asymmetric algorithms (RS/PS/ES 256/384/512) that the IdP advertises in discovery. |
| Claim mapping | Email, Groups / roles claim, First name, and Last name, with the same defaults as SSO. Headless exchange reads these from the id_token. |
| Provisioning & role mapping | Auto-provision, Default roles, Role behavior, and Group → role mappings, as described in User provisioning and role mapping. |
Users signed in through headless exchange are treated as front-end users: their system access role is DOTCMS_FRONT_END_USER.
Allowed Origins#
"Browser origins allowed to call /api/v1/dotauth/oauth/exchange. Wildcards not allowed."
Click Add origin and enter each origin exactly as the browser sends it in the Origin header, for example https://app.example.com. Don't include a trailing slash or path. Matching is exact.
- Browser requests are rejected until at least one origin is added.
- Server-to-server requests (no
Originheader) are allowed. - This list covers only the exchange endpoint. CORS for your subsequent Content API calls comes from dotCMS's standard
api.cors.*configuration.
Emergency Controls#
Revoke all sessionRefs invalidates every issued sessionRef immediately. Every app then has to exchange a fresh token. Use it if a token or IdP configuration may be compromised. Browser (admin) sessions are not affected, and the action is written to the security log.
Example: Exchanging a Token From a Front End#
// 1. After your OIDC library completes sign-in, you have the id_token
// and the nonce you generated for the authorization request.
const res = await fetch('https://cms.example.com/api/v1/dotauth/oauth/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken, nonce }),
});
if (!res.ok) throw new Error(`Exchange failed: ${res.status}`);
const { entity } = await res.json();
// entity = {
// sessionRef: "dsr_…",
// expiresAt: "2026-10-05T18:00:00Z",
// expirationDays: 0,
// user: { userId, email, firstName, lastName, fullName, roles: [...] }
// }
// 2. Call dotCMS APIs as the signed-in user.
const page = await fetch('https://cms.example.com/api/v1/page/json/intranet/home', {
headers: { Authorization: `Bearer ${entity.sessionRef}` },
});
// 3. Sign out: discard the sessionRef on the server.
await fetch('https://cms.example.com/api/v1/dotauth/oauth/session', {
method: 'DELETE',
headers: { Authorization: `Bearer ${entity.sessionRef}` },
});The sign-out call at the end of the example discards the sessionRef on the server:
Exchange Responses#
| Status | Message | Cause |
|---|---|---|
200 | — | Success. |
400 | idToken and nonce are both required | Missing request fields. |
400 | Could not decode iss claim from id_token | The token is malformed. |
401 | Untrusted token issuer | No enabled trusted IdP has a matching issuer. |
401 | id_token validation failed | Bad signature, audience, expiry, or nonce. |
401 | id_token has already been exchanged | Replay: this token was already used. |
403 | allowedOrigins must be configured for browser-based token exchange | No allowed origins are configured. |
403 | Origin not allowed | The browser origin isn't in the list. |
403 | User provisioning is not enabled for this site | The user doesn't exist and auto-provisioning is off. |
403 | User account is not active | The dotCMS user is deactivated. |
503 | OAuth is not configured for this site | Headless exchange isn't enabled. |
Requests that use an unknown or expired sessionRef receive 401 Invalid or expired session.