Skip to main content

Single sign-on with Authentik

The intranet signs users in through OAuth2/OIDC and maps their provider groups to app roles. The reference provider is Authentik; the configuration is provider-agnostic, so Keycloak, Entra, Google, Okta, and others work the same way.

Two people are involved: an Authentik administrator who creates the client, and the intranet deployer who sets the environment variables. The callback path is always <origin>/api/auth/callback/<OAUTH_PROVIDER_ID>; with the recommended OAUTH_PROVIDER_ID=authentik that is /api/auth/callback/authentik. The origin is derived from the request Host when ORIGIN is unset.

1. Choose the origin first​

The redirect URI must match the origin the browser actually opens (scheme + host

  • port + path). ORIGIN is optional; when unset the app derives the origin from the request Host, so register each origin users will hit:
DeploymentPublic originRedirect URI to register
Local composehttp://localhost:3000http://localhost:3000/api/auth/callback/authentik
Staging / productionhttps://intranet.examplehttps://intranet.example/api/auth/callback/authentik

Authentik accepts a newline-separated list; extras are harmless, a missing one causes invalid_redirect_uri. To pin a single origin, set ORIGIN (compose: PUBLIC_ORIGIN).

2. Create the provider (Authentik admin)​

Applications → Providers → Create → OAuth2/OpenID Provider.

FieldValue
NameCloud Exit Intranet
Authorization flowdefault-provider-authorization-explicit-consent (or implicit)
Client typeConfidential
Client IDcloud-exit-intranet (or generated)
Client Secretgenerate; hand the value to the deployer
Redirect URIsthe URI(s) from step 1, one per line
Signing Keyselect a certificate (required for the OIDC id_token)
Scopesopenid, profile, email
PKCEkeep enabled

The profile scope already emits the groups claim — no custom scope mapping is needed for role mapping.

3. Create the application​

Applications → Applications → Create with a name and the slug cloud-exit-intranet (the slug appears in the discovery URL), provider set to step 2. Then, under the application's Policy / Group / User bindings, bind the group(s) allowed to authenticate. Without a binding, users see a "not authorized" page even though the client is valid.

4. Create role groups​

Create Authentik groups whose names map to app roles and add users:

Authentik groupApp role
cloud-exit-adminsadmin
cloud-exit-financefinance
cloud-exit-reviewersreviewer
(any other group)engineer (default)

The role is re-synced from the groups claim on every login, so group membership changes take effect the next time the user signs in.

5. Configure the container (deployer)​

Set these in .env (never commit the secret) and rebuild/recreate:

OAUTH_PROVIDER_ID="authentik"
OAUTH_PROVIDER_NAME="Authentik"
OAUTH_DISCOVERY_URL="https://<AUTHENTIK_HOST>/application/o/cloud-exit-intranet/.well-known/openid-configuration"
OAUTH_CLIENT_ID="cloud-exit-intranet"
OAUTH_CLIENT_SECRET="<client secret>"
OAUTH_SCOPES="openid profile email"
OAUTH_PKCE="true"
OAUTH_GROUPS_CLAIM="groups"
OAUTH_ROLE_MAP='{"cloud-exit-admins":"admin","cloud-exit-finance":"finance","cloud-exit-reviewers":"reviewer"}'
OAUTH_DEFAULT_ROLE="engineer"

Leave OAUTH_AUTHORIZATION_URL/OAUTH_TOKEN_URL/OAUTH_USERINFO_URL blank because the discovery document is used. Then:

docker compose up -d --build --force-recreate app worker
Container reachability

The app container must be able to reach AUTHENTIK_HOST. A hostname that resolves only on your workstation (e.g. a LAN IP or localhost) may not resolve inside the container. Use an address the container can reach, and verify:

docker compose exec app node -e "fetch(process.env.OAUTH_DISCOVERY_URL).then(r=>console.log(r.status))"

6. Values to exchange​

The Authentik admin sends the deployer: the discovery URL, client ID, client secret, confirmation of the registered redirect URI(s), and the group names that should grant admin/finance/reviewer.

Offboarding​

Group membership syncing fixes granting, not revoking: an existing session is not re-checked against Authentik, and removing a user from a group does not sign them out. Until session enforcement is added, deprovision by removing the user from the Authentik application binding and having them sign out (or awaiting session expiry).

Troubleshooting​

SymptomCause / fix
invalid_redirect_uriReceived URI doesn't exactly match a registered one. Compare scheme, host, port, path.
"not authorized" after loginThe user's group isn't bound to the application (step 3).
Provider skipped, log shows discovery failureAuthentik unreachable from the container, or wrong discovery URL. Verify with the command above.
Everyone lands as engineerGroups aren't in OAUTH_ROLE_MAP, OAUTH_GROUPS_CLAIM/OAUTH_ROLE_MAP didn't reach the container, or the profile scope is off.
CSRF / redirect errors after loginThe request Origin differs from the origin derived from Host (forward Host/X-Forwarded-Proto, or set ORIGIN).