feat(auth): OIDC/Keycloak SSO-Login (additiv) — v1.2.91
SSO per OpenID Connect (Authorization Code + PKCE) zusätzlich zum lokalen Login.
- Regeln: kein Auto-Provisioning (E-Mail muss als User existieren), Rolle aus DB (nie aus Token), lokaler Login+TOTP unangetastet.
- Migration 0040: oidc_settings (Singleton, client_secret_enc via secrets.Box) + users.oidc_subject.
- internal/services/oidc: Settings-Repo (write-only Secret) + lazy go-oidc Client (testbarer Authenticator-Seam).
- internal/handlers/oidc.go: GET/PUT /oidc/settings (admin), GET /auth/oidc/{settings,login,callback}. Flow-State (state/PKCE/nonce) stateless im 5-min signierten HttpOnly-Cookie (SameSite=Lax). email_verified erzwungen, opportunistisches sub-Linking, Session via setSessionCookie+Signer.
- session.SignBlob/VerifyBlob; users.Get/SetOIDCSubject; main.go-Wiring.
- Frontend: App.tsx /auth/me-Bootstrap (für Cookie-Session nach Callback), Login-SSO-Button + sso_error, Settings OIDC-Card, i18n de/en.
- Tests (guarded EG_FWTEST_DSN): Secret-Roundtrip + Callback (Rolle-aus-DB, no_account, disabled, unverified, nonce, state).
Deps: go-oidc/v3, x/oauth2. Scope v1: nur Login (kein SLO/Refresh).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
35
internal/database/migrations/0040_oidc_settings.sql
Normal file
35
internal/database/migrations/0040_oidc_settings.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
|
||||
-- OIDC / Keycloak SSO — Singleton-Settings (analog forward_proxy_settings).
|
||||
-- client_secret_enc: secrets.Box.Seal-Output (AES-256-GCM), NULL = nicht gesetzt.
|
||||
-- Rolle kommt bewusst NICHT aus dem Token, daher keine group/role-claim-Spalten.
|
||||
CREATE TABLE IF NOT EXISTS oidc_settings (
|
||||
id INTEGER PRIMARY KEY DEFAULT 1,
|
||||
enabled BOOLEAN NOT NULL DEFAULT false,
|
||||
issuer_url TEXT NOT NULL DEFAULT '',
|
||||
client_id TEXT NOT NULL DEFAULT '',
|
||||
client_secret_enc BYTEA,
|
||||
scopes TEXT NOT NULL DEFAULT 'openid email profile',
|
||||
email_claim TEXT NOT NULL DEFAULT 'email',
|
||||
button_label TEXT NOT NULL DEFAULT 'Sign in with SSO',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT oidc_settings_singleton CHECK (id = 1)
|
||||
);
|
||||
INSERT INTO oidc_settings (id) VALUES (1) ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Opportunistisches Linking: beim ersten SSO-Login wird der OIDC-'sub'
|
||||
-- gespeichert; weicht er später ab, wird der Login abgelehnt. Nullable,
|
||||
-- kein Backfill (Match-Schlüssel bleibt die verifizierte E-Mail).
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS oidc_subject TEXT;
|
||||
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS oidc_subject;
|
||||
DROP TABLE IF EXISTS oidc_settings;
|
||||
|
||||
-- +goose StatementEnd
|
||||
Reference in New Issue
Block a user