OIDC login via Authentik: PKCE flow, JIT provisioning, email linking #5

Closed
opened 2026-07-18 18:15:46 +00:00 by steve · 0 comments
Owner

Part of epic #20 · Design: DESIGN.md

Context

OIDC is pansy's primary login path; Authentik is the IdP it's built against (any spec-compliant issuer should work). Users authenticated by the IdP are provisioned on first login; a matching local account (by email) is linked rather than duplicated. Local auth (#4) remains as fallback and both paths issue the same pansy session cookie.

Scope

  • Deps: github.com/coreos/go-oidc/v3 + golang.org/x/oauth2 (both pure Go — keep CGO_ENABLED=0 working).
  • Issuer discovery from PANSY_OIDC_ISSUER (for Authentik: the application's OIDC provider URL, e.g. https://auth.example.com/application/o/pansy/). Initialize lazily/with retry and log clearly if unreachable — don't crash a server that also serves local auth.
  • GET /api/v1/auth/oidc/login: build the authorization-code URL with PKCE (S256) + random state; stash verifier+state in a short-lived HttpOnly cookie; redirect.
  • GET /api/v1/auth/oidc/callback: verify state, exchange code (redirect URI derived from PANSY_BASE_URL + callback path), verify the ID token, extract sub, email, email_verified, name/preferred_username.
  • Provisioning in internal/service/auth.go:
    1. (oidc_issuer, oidc_subject) matches a user → log them in.
    2. Else email matches an existing user → stamp oidc_issuer/oidc_subject on that user (link) → log in.
    3. Else create a new user (JIT — the IdP gates access, so PANSY_REGISTRATION does not apply) → log in.
  • Issue the same session as #4; redirect to /gardens on success, /login?error=... on failure.
  • GET /auth/providers now reports oidc: true + PANSY_OIDC_BUTTON_LABEL (default "Sign in with Authentik") when configured.
  • PANSY_LOCAL_AUTH=false: hide local login/register from providers and reject those endpoints — pure-Authentik deployments.

Out of scope

  • Frontend button (#6). IdP-initiated logout / back-channel logout, refresh tokens (session lifetime is pansy's own), Authentik group→role mapping (post-v1 idea).

Key files & patterns

  • Flow/config summary: DESIGN.md § Auth. Users table already has the oidc_issuer/oidc_subject unique pair (#1).
  • Steve runs Authentik — test against a real application entry there (redirect URI: PANSY_BASE_URL + /api/v1/auth/oidc/callback).

Dependencies

Blocked by #4.

Acceptance criteria

  • Full round-trip against Authentik: unauthenticated → /auth/oidc/login → Authentik → callback → session cookie set → /auth/me shows the user with issuer/subject stamped.
  • Second OIDC login reuses the same user; a pre-existing local user with the same email gets linked (one row, both auth methods work).
  • Tampered/missing state → 4xx, no session. Unset PANSY_OIDC_ISSUER → providers reports local only and OIDC endpoints 404.
  • PANSY_LOCAL_AUTH=false + OIDC configured: local endpoints rejected, OIDC works.
Part of epic #20 · Design: [DESIGN.md](https://gitea.stevedudenhoeffer.com/steve/pansy/src/branch/main/DESIGN.md) ## Context OIDC is pansy's primary login path; **Authentik** is the IdP it's built against (any spec-compliant issuer should work). Users authenticated by the IdP are provisioned on first login; a matching local account (by email) is linked rather than duplicated. Local auth (#4) remains as fallback and both paths issue the same pansy session cookie. ## Scope - [ ] Deps: `github.com/coreos/go-oidc/v3` + `golang.org/x/oauth2` (both pure Go — keep `CGO_ENABLED=0` working). - [ ] Issuer discovery from `PANSY_OIDC_ISSUER` (for Authentik: the application's OIDC provider URL, e.g. `https://auth.example.com/application/o/pansy/`). Initialize lazily/with retry and log clearly if unreachable — don't crash a server that also serves local auth. - [ ] `GET /api/v1/auth/oidc/login`: build the authorization-code URL with **PKCE (S256)** + random `state`; stash verifier+state in a short-lived HttpOnly cookie; redirect. - [ ] `GET /api/v1/auth/oidc/callback`: verify state, exchange code (redirect URI derived from `PANSY_BASE_URL` + callback path), verify the ID token, extract `sub`, `email`, `email_verified`, `name`/`preferred_username`. - [ ] Provisioning in `internal/service/auth.go`: 1. `(oidc_issuer, oidc_subject)` matches a user → log them in. 2. Else email matches an existing user → stamp `oidc_issuer`/`oidc_subject` on that user (link) → log in. 3. Else create a new user (JIT — the IdP gates access, so `PANSY_REGISTRATION` does **not** apply) → log in. - [ ] Issue the same session as #4; redirect to `/gardens` on success, `/login?error=...` on failure. - [ ] `GET /auth/providers` now reports `oidc: true` + `PANSY_OIDC_BUTTON_LABEL` (default "Sign in with Authentik") when configured. - [ ] `PANSY_LOCAL_AUTH=false`: hide local login/register from providers and reject those endpoints — pure-Authentik deployments. ## Out of scope - Frontend button (#6). IdP-initiated logout / back-channel logout, refresh tokens (session lifetime is pansy's own), Authentik group→role mapping (post-v1 idea). ## Key files & patterns - Flow/config summary: `DESIGN.md` § Auth. Users table already has the `oidc_issuer`/`oidc_subject` unique pair (#1). - Steve runs Authentik — test against a real application entry there (redirect URI: `PANSY_BASE_URL` + `/api/v1/auth/oidc/callback`). ## Dependencies Blocked by #4. ## Acceptance criteria - Full round-trip against Authentik: unauthenticated → `/auth/oidc/login` → Authentik → callback → session cookie set → `/auth/me` shows the user with issuer/subject stamped. - Second OIDC login reuses the same user; a pre-existing local user with the same email gets linked (one row, both auth methods work). - Tampered/missing `state` → 4xx, no session. Unset `PANSY_OIDC_ISSUER` → providers reports local only and OIDC endpoints 404. - `PANSY_LOCAL_AUTH=false` + OIDC configured: local endpoints rejected, OIDC works.
steve added the backendauth labels 2026-07-18 18:15:46 +00:00
steve closed this issue 2026-07-18 21:34:37 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: steve/pansy#5