Skip to content

Auth

CairnCMS handles authentication directly. It supports password login, single sign-on through OpenID Connect, OAuth 2.0, LDAP, and SAML, two-factor authentication, and long-lived static tokens for service accounts. This page covers how those pieces fit together and how to configure them.

Authentication is configured per CairnCMS instance, partly through environment variables, partly through user and role settings under Settings > Roles & Permissions, and partly through each user’s own profile (for two-factor enrollment).

A user can authenticate against a CairnCMS instance in three ways:

  • Password — the user logs in with email and password through the app or the API.
  • Single sign-on (SSO) — the user logs in through an external identity provider such as Google, Okta, Keycloak, or an internal SAML IdP.
  • Static token — a service or script presents a long-lived token for API calls. Static tokens are not used for app login.

Password and SSO logins produce a session represented by a short-lived access token and a longer-lived refresh token. The refresh token is delivered as a cookie when logging in through the app and as part of the response payload when logging in through the API.

Static tokens work differently. The token itself is sent as a bearer token on each API request. There is no separate access token, JWT verification, or refresh flow.

Password login is enabled by default. Each user has an email and password set on their record under User Directory in the app. Passwords are hashed with argon2 before storage.

The LOGIN_STALL_TIME environment variable controls how long failed login attempts wait before responding, mitigating timing attacks against the login endpoint.

The email-based password reset and user invite flows use URL allow lists. Any URL passed to the reset or invite endpoints must match an entry in the corresponding allow list:

Terminal window
PASSWORD_RESET_URL_ALLOW_LIST="https://app.example.com/reset"
USER_INVITE_URL_ALLOW_LIST="https://app.example.com/invite"

Without these allow lists, the corresponding flows reject any URL. This prevents email-based open-redirect attacks.

CairnCMS supports time-based one-time passwords (TOTP) for two-factor authentication. Users enroll from their profile by scanning a QR code with an authenticator app such as 1Password, Authy, or Google Authenticator.

To require two-factor enrollment for everyone in a role, open the role under Settings > Roles & Permissions and enable Enforce 2FA. Users in that role cannot complete login until they have enrolled.

When two-factor is enrolled, the login flow becomes:

  1. The user submits email and password.
  2. CairnCMS responds requesting an OTP.
  3. The user submits the current OTP.
  4. CairnCMS issues access and refresh tokens.

API consumers performing login on behalf of users with 2FA enabled must include the otp field on the login payload.

SSO lets CairnCMS delegate authentication to an external identity provider. CairnCMS supports four mechanisms:

  • OpenID Connect — most modern providers (Google, Microsoft, Okta, Auth0, Keycloak, and similar)
  • OAuth 2.0 — providers that have not adopted OpenID Connect (GitHub, Facebook, Discord, and similar)
  • SAML — enterprise identity providers (AWS IAM Identity Center, Azure AD, and similar)
  • LDAP — directory services such as Active Directory

Several providers can be enabled at once. The login page shows a button for each.

SSO is configured through environment variables. The general pattern:

Terminal window
AUTH_PROVIDERS="<provider1>,<provider2>"
AUTH_<PROVIDER>_DRIVER="<openid|oauth2|saml|ldap>"
AUTH_<PROVIDER>_CLIENT_ID="..."
AUTH_<PROVIDER>_CLIENT_SECRET="..."
AUTH_<PROVIDER>_DEFAULT_ROLE_ID="..."
AUTH_<PROVIDER>_ALLOW_PUBLIC_REGISTRATION="true"
AUTH_<PROVIDER>_IDENTIFIER_KEY="email"

AUTH_PROVIDERS is a comma-separated list of provider keys you want enabled. Each enabled provider needs its own block of environment variables prefixed with AUTH_<KEY>_.

The redirect URL the provider needs to be configured with is:

<your-cairncms-url>/auth/login/<provider>/callback

For example, with AUTH_PROVIDERS="google" and a CairnCMS instance at https://cms.example.com, the redirect URL configured with Google would be https://cms.example.com/auth/login/google/callback.

ALLOW_PUBLIC_REGISTRATION controls whether new users are auto-created on first login. With it set to true, anyone the IdP authenticates gets a CairnCMS user record assigned to DEFAULT_ROLE_ID. With it set to false, only users who already exist with a matching identifier can sign in.

  1. In the Google Cloud Console, create or select a project.
  2. Configure the OAuth consent screen with the scopes .../auth/userinfo.email, .../auth/userinfo.profile, and openid.
  3. Create an OAuth Client ID under Credentials, choosing Web Application.
  4. Set the authorized redirect URI to https://<your-cairncms-url>/auth/login/google/callback. For local testing, also add http://localhost:8055/auth/login/google/callback.
  5. Copy the Client ID and Client Secret.
  6. Add the following to .env:
Terminal window
AUTH_PROVIDERS="google"
AUTH_GOOGLE_DRIVER="openid"
AUTH_GOOGLE_CLIENT_ID="<from-step-5>"
AUTH_GOOGLE_CLIENT_SECRET="<from-step-5>"
AUTH_GOOGLE_ISSUER_URL="https://accounts.google.com"
AUTH_GOOGLE_IDENTIFIER_KEY="email"
AUTH_GOOGLE_LABEL="Google"
AUTH_GOOGLE_ICON="google"
AUTH_GOOGLE_ALLOW_PUBLIC_REGISTRATION="true"
AUTH_GOOGLE_DEFAULT_ROLE_ID="<role-uuid>"
  1. Restart the CairnCMS container.

A “Login with Google” button appears on the login page. New users matched by email get a CairnCMS user record automatically when AUTH_GOOGLE_ALLOW_PUBLIC_REGISTRATION is true.

Common providers and their driver configurations:

Google (OpenID)

Terminal window
AUTH_GOOGLE_DRIVER="openid"
AUTH_GOOGLE_CLIENT_ID="..."
AUTH_GOOGLE_CLIENT_SECRET="..."
AUTH_GOOGLE_ISSUER_URL="https://accounts.google.com/.well-known/openid-configuration"
AUTH_GOOGLE_IDENTIFIER_KEY="email"

Microsoft Azure (OpenID)

Terminal window
AUTH_MICROSOFT_DRIVER="openid"
AUTH_MICROSOFT_CLIENT_ID="..."
AUTH_MICROSOFT_CLIENT_SECRET="..."
AUTH_MICROSOFT_ISSUER_URL="https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration"
AUTH_MICROSOFT_IDENTIFIER_KEY="email"

Okta (OpenID)

Terminal window
AUTH_OKTA_DRIVER="openid"
AUTH_OKTA_CLIENT_ID="..."
AUTH_OKTA_CLIENT_SECRET="..."
AUTH_OKTA_ISSUER_URL="https://<okta-domain>/.well-known/openid-configuration"
AUTH_OKTA_IDENTIFIER_KEY="email"

Auth0 (OpenID)

Terminal window
AUTH_AUTH0_DRIVER="openid"
AUTH_AUTH0_CLIENT_ID="..."
AUTH_AUTH0_CLIENT_SECRET="..."
AUTH_AUTH0_ISSUER_URL="https://<auth0-domain>/.well-known/openid-configuration"
AUTH_AUTH0_IDENTIFIER_KEY="email"

Keycloak (OpenID)

Terminal window
AUTH_KEYCLOAK_DRIVER="openid"
AUTH_KEYCLOAK_CLIENT_ID="..."
AUTH_KEYCLOAK_CLIENT_SECRET="..."
AUTH_KEYCLOAK_ISSUER_URL="https://<keycloak-domain>/realms/<realm>/.well-known/openid-configuration"
AUTH_KEYCLOAK_IDENTIFIER_KEY="email"

GitHub (OAuth 2.0)

Terminal window
AUTH_GITHUB_DRIVER="oauth2"
AUTH_GITHUB_CLIENT_ID="..."
AUTH_GITHUB_CLIENT_SECRET="..."
AUTH_GITHUB_AUTHORIZE_URL="https://github.com/login/oauth/authorize"
AUTH_GITHUB_ACCESS_URL="https://github.com/login/oauth/access_token"
AUTH_GITHUB_PROFILE_URL="https://api.github.com/user"

If a GitHub user has not marked their email as public, CairnCMS cannot read it and the login will not match an existing user.

AWS IAM Identity Center (SAML)

Terminal window
AUTH_PROVIDERS="awssso"
AUTH_AWSSSO_DRIVER="saml"
AUTH_AWSSSO_idp_metadata="<your IAM Identity Center SAML metadata XML>"
AUTH_AWSSSO_sp_metadata=""
AUTH_AWSSSO_ALLOW_PUBLIC_REGISTRATION="true"
AUTH_AWSSSO_DEFAULT_ROLE_ID="<role-uuid>"
AUTH_AWSSSO_IDENTIFIER_KEY="email"
AUTH_AWSSSO_EMAIL_KEY="email"

The SAML metadata needs the leading <?xml version="1.0" encoding="UTF-8"?> declaration removed before being passed to CairnCMS. Map the user’s email attribute as both the Subject (emailAddress type) and as the email attribute (unspecified type). The application ACS URL is https://<your-cairncms-url>/auth/login/awssso/acs.

For other SAML providers, replace awssso with your chosen key and supply the IdP metadata XML.

For LDAP, Facebook, Twitter, Discord, Twitch, Apple, and any other options not covered here, see the configuration reference under Manage > Configuration.

When CairnCMS is the auth backend for a frontend on a different domain, for example, a static site that needs to read content as a logged-in user, configure refresh-token cookies for cross-domain delivery:

Terminal window
REFRESH_TOKEN_COOKIE_DOMAIN="cms.example.com"
REFRESH_TOKEN_COOKIE_SECURE="true"
REFRESH_TOKEN_COOKIE_SAME_SITE="None"

The frontend directs users to a CairnCMS-hosted SSO login URL with a same-origin redirect target that completes the flow on the CairnCMS side:

<a href="https://cms.example.com/auth/login/google?redirect=/admin">Login</a>

After the provider authenticates and the redirect value passes the same-origin check, CairnCMS sets the refresh-token cookie scoped to cms.example.com and redirects the user to the local target (here, the admin app). The cross-domain frontend at https://app.example.com then calls POST https://cms.example.com/auth/refresh with credentials: 'include' to exchange the cookie for an access token.

The redirect query parameter accepts only same-origin local paths. A cross-domain frontend cannot pass an external URL as the redirect target. If redirect is omitted or fails the same-origin check, the callback returns the access and refresh tokens in a JSON response body and the refresh cookie is not set. That shape suits programmatic clients but not a cross-domain browser frontend, which needs the cookie present on cms.example.com to call /auth/refresh from app.example.com. Practical patterns for completing the round-trip back to your frontend include opening the SSO login in a popup window with a same-origin completion page that closes after authentication and then having the parent frame call /auth/refresh, or directing users to a CairnCMS landing page that prompts them to return to your application.

Cross-domain cookies require HTTPS in production. For local testing only, you can use REFRESH_TOKEN_COOKIE_SECURE="false" and REFRESH_TOKEN_COOKIE_SAME_SITE="lax". Never run production with these settings. They expose the instance to CSRF attacks.

Static tokens are long-lived access tokens attached to a specific user. They are used for service accounts, scripts, and integrations that authenticate non-interactively.

To create one:

  1. Open the user under User Directory.
  2. Expand the Token section in the sidebar.
  3. Generate a token, save it somewhere safe, and use it in the Authorization: Bearer <token> header on API requests.

Static tokens carry the role and permissions of the user they belong to. They do not expire on their own. They remain valid until the token field on the user is regenerated or cleared.

For interactive users, prefer the access/refresh-token flow over static tokens.

A successful login produces two tokens:

  • an access token, short-lived (default 15 minutes)
  • a refresh token, longer-lived (default 7 days)

The access token is sent as a Bearer token in the Authorization header. When it expires, the client calls POST /auth/refresh with the refresh token to obtain a new access token.

Defaults can be tuned through environment variables. The shipped defaults are:

Terminal window
ACCESS_TOKEN_TTL="15m"
REFRESH_TOKEN_TTL="7d"
REFRESH_TOKEN_COOKIE_NAME="cairncms_refresh_token"
REFRESH_TOKEN_COOKIE_SECURE="false"
REFRESH_TOKEN_COOKIE_SAME_SITE="lax"

These defaults favor local development. For production over HTTPS, set REFRESH_TOKEN_COOKIE_SECURE to true and consider tightening REFRESH_TOKEN_COOKIE_SAME_SITE to strict.

For app logins, the refresh token is delivered as an httpOnly cookie. For API logins, both tokens are returned in the JSON response body.

Two role-level flags control what authenticated users can do:

  • App access — required to use the admin app in a browser. Users without app access can still authenticate against the API but cannot open the admin UI.
  • Admin access — bypasses all permission checks. Reserve this for administrators; do not give it to roles that should respect access control.

Both flags are set on the role under Settings > Roles & Permissions, not on individual users. To create an API-only service account, make a role with neither flag set, assign the user to that role, and create a static token for them.

A role can restrict access to specific source IPs. Configure this on the role under Settings > Roles & Permissions by setting IP Access to a comma-separated list of allowed addresses. Requests from users in that role whose source IP is not in the list are rejected.

The check runs on every authenticated request, not only at login. The match is exact string comparison; CIDR ranges are not supported, so list each allowed address explicitly.

This is useful for tightly scoped admin or service accounts where the legitimate caller comes from a known network.