Backend Phase B (2/2): OIDC auth + config validation

- internal/auth/oidc.go: OIDCService mit go-oidc v3
  - id_token-Verifikation via JWKS (Signatur, iss, aud, exp)
  - Provider-Caching (sync.Map, lazy init per Issuer-URL)
  - Unterstützt Google + Generic OIDC
- internal/auth/user.go: GetByOIDCSubject + CreateOIDCUser
  (find-or-create via (oidc_issuer, oidc_subject))
- internal/httpapi/auth.go: POST /auth/oidc Handler
  (id_token verifiziern → find-or-create User → issueSession)
- internal/httpapi/api.go: /auth/oidc Route verdrahtet
- internal/config/config.go: OIDC-Validierung
  (enabled → client_id + issuer Pflicht)
- go.mod/go.sum: go-oidc/v3 + oauth2 Abhängigkeiten
- AGENTS.md: Phase B vollständig als erledigt markiert

Verifiziert: E2E gegen lokalen Mock-IdP (Discovery → JWKS →
signiertes id_token → User angelegt → 2. Login gleicher User →
tampered Token → 401). Alle Fehlerpfade geprüft.

go build ./... && go vet ./... && go test ./internal/auth/... 
This commit is contained in:
Tronax 2026-08-05 19:45:00 +02:00
parent 7b1c18590e
commit a5ef8cf3ba
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
8 changed files with 322 additions and 23 deletions

View file

@ -26,14 +26,33 @@ func NewAPI(cfg *config.Config, pool *pgxpool.Pool) *API {
TokenBytes: cfg.SessionTokenBytes,
TTL: cfg.SessionTokenTTL,
})
oidcSvc := auth.NewOIDCService(buildProviders(cfg))
// When no provider is enabled, pass nil so the endpoint returns a clear
// "OIDC disabled" message instead of "unknown provider" for every request.
if len(oidcSvc.Providers()) == 0 {
oidcSvc = nil
}
return &API{
cfg: cfg,
pool: pool,
health: &HealthHandler{Pool: pool},
auth: NewAuthHandler(users, sessions, cfg),
auth: NewAuthHandler(users, sessions, oidcSvc, cfg),
}
}
// buildProviders assembles the OIDC provider map from config. Only enabled
// providers with a non-empty issuer and client_id are included.
func buildProviders(cfg *config.Config) map[string]auth.ProviderConfig {
p := map[string]auth.ProviderConfig{}
if cfg.GoogleOIDC.Enabled && cfg.GoogleOIDC.Issuer != "" && cfg.GoogleOIDC.ClientID != "" {
p["google"] = auth.ProviderConfig{Issuer: cfg.GoogleOIDC.Issuer, ClientID: cfg.GoogleOIDC.ClientID}
}
if cfg.GenericOIDC.Enabled && cfg.GenericOIDC.Issuer != "" && cfg.GenericOIDC.ClientID != "" {
p["generic"] = auth.ProviderConfig{Issuer: cfg.GenericOIDC.Issuer, ClientID: cfg.GenericOIDC.ClientID}
}
return p
}
// Handler returns the fully wired http.Handler with all middleware applied.
func (a *API) Handler() http.Handler {
mux := http.NewServeMux()
@ -45,8 +64,8 @@ func (a *API) Handler() http.Handler {
// --- auth endpoints ---
mux.HandleFunc("POST /auth/register", a.auth.Register)
mux.HandleFunc("POST /auth/login", a.auth.Login)
mux.HandleFunc("POST /auth/oidc", a.auth.OIDC)
mux.HandleFunc("POST /auth/logout", a.auth.Logout)
// mux.HandleFunc("POST /auth/oidc", a.auth.OIDC) // Phase B part 2
// --- authenticated API endpoints (added in Phase C) ---
// mux.HandleFunc("GET /api/lists", a.RequireAuth(http.HandlerFunc(a.lists.List)))