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>
70 lines
1.9 KiB
Go
70 lines
1.9 KiB
Go
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)
|
|
}
|