Configuring Authentik OIDC for an app¶
Most of the time you don't need this guide. The scaffold script writes a per-app blueprint to
services/auth/blueprints/apps/<slug>.yaml, and Authentik's worker reconciles it within ~60s (or instantly withcd services/auth && docker compose restart worker). The blueprint creates the Provider, Application, and group binding with all the right values. This guide is the manual UI fallback — useful for diagnosing blueprint failures, making one-off tweaks, or understanding what the blueprint sets.
Every SCALR app authenticates users via Authentik. The default flow is fully declarative: the scaffold script writes a committed .env.app (structural config) and a blueprint YAML (OIDC config); secrets like POSTGRES_PASSWORD come from <repo>/.env.shared — see ADR 0008. If a blueprint fails or you need to inspect/edit a Provider in the UI, the click-by-click below is what the blueprint encodes.
This guide walks through it for template-app (slug template-app, frontend port 5174). Substitute your own slug and port for any other app.
0. Prerequisites¶
- Authentik is running:
cd services/auth && docker compose up -d. Wait ~30 seconds. - You can sign in to http://auth.localhost as
akadminwithAUTHENTIK_BOOTSTRAP_PASSWORDfromservices/auth/.env.- First-boot trap: the bootstrap password is read only on the first boot against a fresh Postgres. If you change
.envafter that, the database keeps the old password. Eitherdocker compose down -v && docker compose up -d(wipes the volumes), or reset in place withdocker compose exec worker ak change_password akadmin.
- First-boot trap: the bootstrap password is read only on the first boot against a fresh Postgres. If you change
Open the admin UI: click your avatar (top right) → Admin interface, or go directly to http://auth.localhost/if/admin/.
1. Create the OAuth2/OpenID Provider¶
- Left sidebar: Applications → Providers.
- Click Create (top-right of the providers list).
- In the modal, pick OAuth2/OpenID Provider and click Next.
- Fill in:
- Name: any human-readable label, e.g.
template-app-provider. Used only as a display name inside Authentik. - Authentication flow:
default-authentication-flow. (Click into the dropdown and start typing — pasting alone often doesn't trigger the filter.) - Authorization flow:
default-provider-authorization-implicit-consentfor dev (auto-approves the consent screen). For production, switch to…-explicit-consentso users see a "this app wants access to your profile" page once.- Gotcha: these are two different dropdowns with different lists of options.
default-authentication-flowwill not appear in the Authorization-flow dropdown — that's by design. - If both dropdowns are empty: Authentik's default flows ship as blueprints that load on first boot. Visit Customisation → Blueprints in the admin UI; if any of the default blueprints are in
failedstate, the dropdowns won't populate. Restart the worker and wait a minute.
- Gotcha: these are two different dropdowns with different lists of options.
- Name: any human-readable label, e.g.
- Scroll to Protocol settings:
- Client type: Public. (Critical. Single-page apps can't safely keep a client secret. Picking "Confidential" will break the SPA login.)
- Client ID: the app's slug — for example,
template-app. Authentik prefills this with a random string; replace it with the slug. The whole template wiresAUTH_AUDIENCE,VITE_AUTH_CLIENT_ID, the manifest'sauth.authentik_app_slug, and the OIDC issuer URL all to this same value, so keeping them aligned saves debugging. - Client Secret: leave empty.
- Redirect URIs/Origins: one per line. For dev, list all four forms — both the gateway hostname and the direct-port URL, each with and without trailing slash:
Substitute your slug + port. Why all four:
http://localhost:5174 http://localhost:5174/ http://template-app.localhost http://template-app.localhost/<slug>.localhostis the canonical URL through the Traefik gateway (what users actually click).localhost:<port>is needed for direct-port standalone mode (rare, but useful when bringing one app up in isolation).- The trailing-slash variants exist because OIDC libraries differ on whether they include the slash in
redirect_uri— register both, ship. For prod, replacelocalhostwithscalr.com(HTTPS) and drop the direct-port URLs.
- Signing Key:
authentik Self-signed Certificate(the only one present after a fresh boot — fine for dev).
- Expand Advanced protocol settings:
- Subject mode:
Based on the User's hashed IDis safest. (Based on the User's usernameis fine for dev — it makes thesubclaim more readable in logs.) - Include claims in id_token: leave checked.
- Issuer mode: leave on the default ("Each provider has a different issuer, based on the application slug"). This is what makes the issuer URL
http://auth.localhost/application/o/<slug>/, which is what each app's.env.appexpects (AUTH_ISSUER,VITE_AUTH_AUTHORITY).
- Subject mode:
- Click Create.
2. Create the Application¶
- Left sidebar: Applications → Applications.
- Click Create.
- Fill in:
- Name: human-readable, e.g.
Template App. This is what the SCALR portal will eventually show in the app hub. - Slug: must equal
auth.authentik_app_slugfrom your manifest. For the template,template-app. The portal's auto-discover keys off this. - Provider: pick the one you just made (
template-app-provider). - Backchannel providers: leave empty.
- UI settings → Launch URL:
http://template-app.localhost(the gateway URL — substitute your slug). This is what users click from the SCALR portal and from Authentik's launchpad. Don't uselocalhost:<port>— that bypasses Traefik and breaks/apisame-origin routing. - Policy engine mode:
anyis fine for dev.
- Name: human-readable, e.g.
- Click Create.
3. Grant access¶
By default an Application has no policy binding, which means every signed-in Authentik user can access it. That works for dev — the test user can log in immediately.
To restrict access (closer to production behaviour):
- Open the application you just created (click its row).
- Switch to the Policy / Group / User Bindings tab.
- Click Create binding → choose Group.
- Pick
scalr-users(created byservices/auth/blueprints/scalr-test-user.yaml) → Save.
The test user is already a member of scalr-users, so it can sign in. Add akadmin to the group too if you want to test as admin.
4. Verify the OIDC discovery endpoint¶
This is the single best sanity check. Open in a browser:
http://auth.localhost/application/o/template-app/.well-known/openid-configuration
(Substitute your slug.) You should get a JSON response. The fields map directly to your app's .env.app:
| JSON field | env var |
|---|---|
issuer |
AUTH_ISSUER and VITE_AUTH_AUTHORITY (drop the trailing slash for the latter) |
jwks_uri |
AUTH_JWKS_URL |
token_endpoint |
what scripts/smoke-test.sh POSTs to |
authorization_endpoint |
where the frontend redirects users to sign in |
AUTH_AUDIENCE and VITE_AUTH_CLIENT_ID are both just the slug.
If this URL returns 404, the Application slug doesn't match the URL. Fix the slug in Authentik or in the URL.
5. The smoke test's password-grant requirement¶
The third check in scripts/smoke-test.sh exchanges the test user's password for a token using the OIDC password grant (grant_type=password). For this to work:
- The provider's Authentication flow must be
default-authentication-flow(set in step 1.4 above). - Client type must be
Publicand Client ID must equal the audience (set in step 1.5). - The test user must be a member of any group bound to the application (see step 3).
If the smoke test fails with invalid_grant or unauthorized_client, check those three first. Authentik's logs (docker compose logs server -f) will show the exact reason.
Summary checklist¶
For an app with slug <slug> and frontend port <port>:
- [ ] Authentik is up at http://auth.localhost, you can sign in as
akadmin. - [ ] Provider created with: Client type=Public, Client ID=
<slug>, Redirect URIs include all four ofhttp://localhost:<port>,http://localhost:<port>/,http://<slug>.localhost,http://<slug>.localhost/. Default authentication and implicit-consent authorization flows. - [ ] Application created with: slug=
<slug>, provider=the one you just made, launch URL=http://<slug>.localhost. - [ ] (Optional) Application bound to the
scalr-usersgroup. - [ ]
http://auth.localhost/application/o/<slug>/.well-known/openid-configurationreturns JSON. - [ ] App's
.env.apphasAUTH_AUDIENCE=<slug>andVITE_AUTH_CLIENT_ID=<slug>.
You're then ready to make up (from the repo root) and ./scripts/smoke-test.sh from the app's directory.