The Authentik blueprint that provisions the OAuth2 Provider/Application only lived on CT121's filesystem via ad-hoc scp/pct push -- deploy/authentik/ is now the source of truth, with redeploy steps in docs/oidc-setup.md. Also documents two bugs found and fixed while implementing issue #19: the first RP-Initiated Logout attempt only ended the app-scoped session, and the provider had no property_mappings so the ID token's username claim was missing (fell back to a raw sub hash that looked like a leaked session token). Both are covered by new Playwright regression tests. The deep-check Fingerbank test now skips instead of failing when its target device (192.168.1.106) has since been manually labeled known via the dashboard, rather than assuming it stays unlabeled forever.
7.8 KiB
Authentik OIDC setup
Local auth (bcrypt + session, the saved Vaultwarden password) is always available.
When OIDC_ENABLED=true, the login page additionally shows a "Sign in with Authentik"
button — this is additive, not a replacement, so the existing admin login keeps working.
How the Authentik side was provisioned
No API token or admin credentials were needed. Authentik supports blueprints —
declarative YAML files it applies automatically — so the OAuth2 Provider + Application
were created via a blueprint deployed to /opt/authentik/blueprints-local/homelab-monitor-oidc.yaml
on CT121, picked up by the worker container on startup. This is purely additive: it
doesn't touch any existing user, group, or admin credential.
Source of truth is deploy/authentik/homelab-monitor-oidc.yaml in this repo — the
copy on CT121 was hand-edited via scp/pct push several times before this was
committed here, meaning CT121 was the only record of the current config for a while. If
CT121 is ever rebuilt, redeploy from this file (substituting the real client_secret
from Vaultwarden, since Authentik blueprints don't support secret references and the
repo copy uses a placeholder):
scp deploy/authentik/homelab-monitor-oidc.yaml pve:/tmp/
ssh pve "pct push 121 /tmp/homelab-monitor-oidc.yaml /opt/authentik/blueprints-local/homelab-monitor-oidc.yaml"
ssh pve "pct exec 121 -- docker restart authentik-worker-1"
- Blueprint volume mount added to
/opt/authentik/docker-compose.yml(backed up asdocker-compose.yml.bak-homelab-monitorbefore editing) for bothserverandworker. - Provider: confidential client,
default-provider-authorization-implicit-consentflow (auto-approve — reasonable for a single-user personal dashboard), signed with Authentik's existing self-signed cert. - Redirect URIs (both registered):
https://monitor.jerodrigged.com/api/auth/oidc/callback(primary, set in.env) andhttp://192.168.1.103:8090/api/auth/oidc/callback(LAN fallback, works if the tunnel/DNS is down). - Application slug:
homelab-monitor. property_mappings: the three default Authentik OpenID scope mappings (openid/profile/email) — see "Bug: username field showed a raw session-token-looking hash" below for why this matters.invalidation_flow:default-invalidation-flow(full Authentik logout), not the app-scopeddefault-provider-invalidation-flow— see "Bug: sign out didn't actually sign out of Authentik" below.
To change anything (redirect URI, flow, scopes), edit deploy/authentik/homelab-monitor-oidc.yaml,
redeploy via the commands above, and commit the change.
Bug: LAN-IP issuer sent browsers somewhere they couldn't reach
First deploy used OIDC_ISSUER_URL=https://192.168.1.208:9443/application/o/homelab-monitor/
(Authentik's LAN IP) for discovery. Authentik's discovery document echoes back whichever
host you queried it through — so authorization_endpoint came back as that same LAN IP.
That value gets handed straight to the browser as a redirect target. Anyone off the LAN
(or just not on wifi) got redirected to an address they couldn't reach at all — reported as
"signed in via Authentik, got sent to the local IP, and it failed."
Fix: Authentik was already publicly exposed at https://auth.jerodrigged.com (NPM proxy
host id 9, predates this project) with a real Let's Encrypt cert — just wasn't the one used
for OIDC_ISSUER_URL. Switched to it; OIDC_ALLOW_INSECURE_TLS could then go back to false
too, since it's not a self-signed cert. Always use the public issuer URL for anything a
browser is ever redirected through, even though the app's own server-to-server calls
(token exchange, userinfo, jwks — all made directly by the Node process, never by a browser)
would have worked fine against the LAN IP too.
API-side implementation
apps/api/src/auth/oidc.ts uses openid-client v6 with PKCE + state, with an optional
insecure-TLS fetch override (OIDC_ALLOW_INSECURE_TLS) for the rare case the issuer is
LAN-only with a self-signed cert — not needed now that the issuer is the public URL, but
kept since Authentik's LAN address still works as a fallback if auth.jerodrigged.com is
ever unreachable. Routes in apps/api/src/routes/oidc.ts:
GET /api/auth/oidc/login— redirects to Authentik's authorization endpointGET /api/auth/oidc/callback— exchanges the code, setsreq.session.usernamefrom thepreferred_username(falls back toemail, thensub) ID token claim, and storesauthMethod: "oidc"+ the rawid_tokenin the sessionGET /api/auth/oidc/logout— RP-Initiated Logout (issue #19). Only takes this path ifreq.session.authMethod === "oidc"(a locally-authenticated session has no Authentik session to end); redirects through Authentik'send_session_endpointwithid_token_hint+post_logout_redirect_uri(viaopenid-client'sbuildEndSessionUrl, not hand-rolled) before landing back on/. Must be a full-page navigation, not a fetch — the frontend doeswindow.location.href = ..., since Authentik needs to see a real browser request to clear its own session cookie onauth.jerodrigged.com. PlainPOST /api/auth/logoutstill exists for local sessions and just clears the local one.
Authentik has no dedicated post_logout_redirect_uri allowlist field (unlike
redirect_uris) as of the version this was built against — confirmed by checking the
provider's DB schema (\d authentik_providers_oauth2_oauth2provider) before
implementing, not assumed.
Bug: sign out didn't actually sign out of Authentik
Issue #19's premise. First implementation was spec-compliant OIDC RP-Initiated Logout —
redirected through Authentik's end_session_endpoint with correct id_token_hint +
post_logout_redirect_uri — but Authentik's default-provider-invalidation-flow
(the initial choice) only invalidates this application's session, leaving the
underlying Authentik browser cookie valid. A subsequent "Sign in with Authentik" click
would silently re-authenticate with no prompt at all — technically correct per spec
(RP-Initiated Logout is only supposed to end the requesting client's session), but
doesn't satisfy "sign out should sign out of Authentik too."
Fix: switched invalidation_flow to default-invalidation-flow (Authentik's actual
full-logout flow, designation Logout vs. the other's Logged out of application).
This is a per-provider setting — doesn't change any other app's own logout behavior.
Verified the difference with a full Playwright click-through both ways: app-scoped
logout → "Sign in with Authentik" silently re-authenticates; full logout → a real
Authentik login form ("Welcome to authentik! Login to continue.") is shown.
Bug: username field showed a raw session-token-looking hash
The OAuth2Provider was created without any property_mappings (scope-to-claims
mappings) attached. Requesting scope=openid profile email in the authorization
request doesn't matter if the provider itself isn't configured to release the
corresponding claims — the ID token only ever carried the bare required claims
(sub/iss/aud/exp/...), confirmed by decoding a real captured ID token rather
than guessing. The app's claim fallback chain (preferred_username ?? email ?? sub,
apps/api/src/auth/oidc.ts) landed on the raw sub value — with sub_mode: hashed_user_id, a long hash — which rendered next to the sign-out button looking like
a leaked session token.
Fix: attached Authentik's three default OpenID scope mappings (authentik default OAuth Mapping: OpenID 'openid'/'profile'/'email') via property_mappings in the blueprint.
preferred_username now populates correctly. No app-code change was needed — the fix
belonged entirely on the Authentik provisioning side.
Credentials
OIDC_CLIENT_ID/OIDC_CLIENT_SECRET are in CT122's .env and saved in Vaultwarden
alongside the local admin login.