IAM-06: sso-anmeldung-ueber-oidc
internal/oidc: JWKS-Parser (RSA-Schluessel, RFC 7517) + Verifier prueft ID-Tokens gegen den geparsten Schluesselsatz (Signatur, Ablauf ueber die jwt-Bibliothek, Aussteller) — Akzeptanzkriterium 3. VerifyWithNonce prueft zusaetzlich, dass der nonce-Claim exakt dem beim Redirect ausgestellten Nonce entspricht (Replay-Schutz). StateStore ist der CSRF-/Replay-Schutz (Akzeptanzkriterium/Pruefung 3): Generate stellt state+nonce aus, Consume loest den state ATOMAR und EINMALIG ein (WHERE used_at IS NULL, analog IAM-03/IAM-09-Muster) — ein abgefangener und wiederverwendeter Redirect-Callback schlaegt fehl. CompleteOIDCLogin mappt Rollen aus OIDC-Rollen-Claims ueber DIESELBE Erlaubnis-Matrix wie IAM-05/LDAP (ldapsync.RoleMappingStore.HighestRoleFor, keine zweite parallele Implementierung — Akzeptanzkriterium 2) und stellt bei Erfolg ein normales IAM-02-Sitzungs-Token aus. Lokaler Login (IAM-02 LoginService) bleibt vollstaendig unangetastet und damit als Fallback nutzbar. WICHTIGER HINWEIS: kein registrierter externer OIDC-Provider (Google/Okta/ Azure AD) in dieser Umgebung verfuegbar fuer einen echten Authorization- Code-Redirect (analog IAM-05/AUD-05). ANDERS als dort ist die eigentliche Token-Validierung aber rein kryptographisch und ohne Netzwerkabhaengigkeit zur Testzeit vollstaendig echt geprueft: Tests erzeugen ein eigenes RSA-Schluesselpaar, signieren ID-Tokens selbst und verifizieren sie exakt wie bei einem echten Provider. Nur der Live-Redirect zu einem realen externen IdP bleibt ungeprueft. Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS): 1. Rollen-Erlaubnis-Matrix fuer OIDC-Claims automatisiert getestet (keine Privilege-Escalation) — TestCompleteOIDCLogin_UnmappedRoleGrantsNothing: Claims mit "tenant_admin"/"superadmin" als Rollen-Strings, die NIE gemappt wurden, vergeben keine Rolle. PASS. 2. Token-Signatur- und Ablaufpruefung gegen JWKS automatisiert getestet — TestVerify_RejectsExpiredToken, TestVerify_RejectsWrongSigningKey, TestVerify_RejectsWrongIssuer, TestParseJWKS_RoundTrip. PASS. 3. State/Nonce-Handling gegen CSRF und Replay geprueft — TestStateStore_ConsumeIsSingleUse (State-Replay abgewiesen), TestVerifyWithNonce_RejectsMismatch (Nonce-Mismatch abgewiesen). PASS. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1a82da211d
commit
e4856dfc9d
@@ -0,0 +1,69 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/auth"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/ldapsync"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/rbac"
|
||||
"gitea.perlbach24.de/scripte/nexarch/internal/user"
|
||||
)
|
||||
|
||||
// CompleteOIDCLogin verifiziert das ID-Token (Signatur, Ablauf, Aussteller,
|
||||
// Nonce — Akzeptanzkriterium 3), loest den State einmalig ein
|
||||
// (Akzeptanzkriterium/Pruefung 3), legt bei Bedarf einen lokalen Benutzer an
|
||||
// und mapped Rollen aus den OIDC-Claims ueber DIESELBE Erlaubnis-Matrix wie
|
||||
// IAM-05/LDAP (ldapsync.RoleMappingStore — kein zweiter, paralleler
|
||||
// Mapping-Mechanismus, Akzeptanzkriterium 2). Bei Erfolg wird ein normales
|
||||
// IAM-02-Sitzungs-Token ausgestellt.
|
||||
func CompleteOIDCLogin(
|
||||
ctx context.Context,
|
||||
verifier *Verifier,
|
||||
states *StateStore,
|
||||
users *user.TenantUserStore,
|
||||
roles *rbac.Store,
|
||||
roleMapping *ldapsync.RoleMappingStore,
|
||||
issuer *auth.TokenIssuer,
|
||||
tenantSlug, state, idToken string,
|
||||
) (token string, err error) {
|
||||
nonce, err := states.Consume(ctx, state)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims, err := verifier.VerifyWithNonce(idToken, nonce)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if claims.Email == "" {
|
||||
return "", fmt.Errorf("%w: id-token ohne e-mail-claim", ErrInvalidToken)
|
||||
}
|
||||
|
||||
u, err := findOrCreateUser(ctx, users, claims.Email)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if role, ok, err := roleMapping.HighestRoleFor(ctx, claims.Roles); err != nil {
|
||||
return "", err
|
||||
} else if ok {
|
||||
if _, err := roles.Assign(ctx, u.ID, role, "oidc-sso"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return issuer.Issue(u.ID, tenantSlug)
|
||||
}
|
||||
|
||||
func findOrCreateUser(ctx context.Context, users *user.TenantUserStore, email string) (user.User, error) {
|
||||
creds, err := users.GetByEmailForAuth(ctx, email)
|
||||
if err == nil {
|
||||
return creds.User, nil
|
||||
}
|
||||
if !errors.Is(err, user.ErrNotFound) {
|
||||
return user.User{}, err
|
||||
}
|
||||
return users.Create(ctx, email, email)
|
||||
}
|
||||
Reference in New Issue
Block a user