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:
(oidc_issuer, oidc_subject) matches a user → log them in.
Else email matches an existing user → stamp oidc_issuer/oidc_subject on that user (link) → log in.
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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
github.com/coreos/go-oidc/v3+golang.org/x/oauth2(both pure Go — keepCGO_ENABLED=0working).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) + randomstate; stash verifier+state in a short-lived HttpOnly cookie; redirect.GET /api/v1/auth/oidc/callback: verify state, exchange code (redirect URI derived fromPANSY_BASE_URL+ callback path), verify the ID token, extractsub,email,email_verified,name/preferred_username.internal/service/auth.go:(oidc_issuer, oidc_subject)matches a user → log them in.oidc_issuer/oidc_subjecton that user (link) → log in.PANSY_REGISTRATIONdoes not apply) → log in./gardenson success,/login?error=...on failure.GET /auth/providersnow reportsoidc: 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
Key files & patterns
DESIGN.md§ Auth. Users table already has theoidc_issuer/oidc_subjectunique pair (#1).PANSY_BASE_URL+/api/v1/auth/oidc/callback).Dependencies
Blocked by #4.
Acceptance criteria
/auth/oidc/login→ Authentik → callback → session cookie set →/auth/meshows the user with issuer/subject stamped.state→ 4xx, no session. UnsetPANSY_OIDC_ISSUER→ providers reports local only and OIDC endpoints 404.PANSY_LOCAL_AUTH=false+ OIDC configured: local endpoints rejected, OIDC works.