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#


  1. Your front end signs the user in with the IdP using a standard OIDC library (Authorization Code + PKCE). It generates a nonce for that request.
  2. The front end sends the resulting id_token and the nonce to POST /api/v1/dotauth/oauth/exchange.
  3. 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.
  4. 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.

Initializing...

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#

SettingDefaultDescription
SessionRef lifetime (minutes)60How 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 expOnNever 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.

Expanded trusted IdP card showing the Issuer URL and Expected audience, above the Allowed origins list

FieldDescription
Discovery URL (optional)Your IdP's base URL or full .well-known URL. Click Discover to fill in the issuer and signing details.
Display nameA label shown in this list and in audit logs, for example Auth0 — Production.
Issuer URLMust exactly match the iss claim in incoming tokens.
JWKS URLShown for reference. dotCMS gets signing keys from the issuer's discovery document.
Expected audienceThe 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 algorithmsDefault RS256. dotCMS accepts only asymmetric algorithms (RS/PS/ES 256/384/512) that the IdP advertises in discovery.
Claim mappingEmail, Groups / roles claim, First name, and Last name, with the same defaults as SSO. Headless exchange reads these from the id_token.
Provisioning & role mappingAuto-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 Origin header) 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:

Initializing...

Exchange Responses#

StatusMessageCause
200—Success.
400idToken and nonce are both requiredMissing request fields.
400Could not decode iss claim from id_tokenThe token is malformed.
401Untrusted token issuerNo enabled trusted IdP has a matching issuer.
401id_token validation failedBad signature, audience, expiry, or nonce.
401id_token has already been exchangedReplay: this token was already used.
403allowedOrigins must be configured for browser-based token exchangeNo allowed origins are configured.
403Origin not allowedThe browser origin isn't in the list.
403User provisioning is not enabled for this siteThe user doesn't exist and auto-provisioning is off.
403User account is not activeThe dotCMS user is deactivated.
503OAuth is not configured for this siteHeadless exchange isn't enabled.

Requests that use an unknown or expired sessionRef receive 401 Invalid or expired session.