IAM-18: strikte-claim-validierung-bei-oidc-sso
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 23s
Supply-Chain-Scan (Go) / govulncheck (push) Failing after 23s
Ergaenzt IAM-06 (SSO ueber OIDC) um strikte Issuer-/Audience-Pruefung, JWKS-Pinning gegen den konfigurierten Issuer und eine technisch erzwungene Sperre gegen Superadmin-Vergabe aus externen Claims. - internal/oidc/verify.go: Verifier.audience wird per Tenant fest konfiguriert (NewVerifier), niemals aus dem Token gelesen; Verify lehnt Tokens mit unerwarteter Audience zusaetzlich zum bestehenden Issuer-Abgleich ab. Signatur wird weiterhin ausschliesslich gegen die beim Verifier hinterlegte JWKS-Schluesselmenge des konfigurierten Issuers geprueft (kid waehlt nur INNERHALB dieser Menge aus). - internal/oidc/login.go: capToTenantScopedRole() kappt eine aus RoleMappingStore.HighestRoleFor gelieferte superadmin-Rolle unabhaengig von deren Quelle auf tenant_admin, statt sie zu vergeben oder den Login mit einem Fehler abzubrechen. Zusaetzliche, von der bestehenden Erlaubnis-Matrix (SetMapping, rbac.Store.Assign) unabhaengige Verteidigungsebene direkt im Login-Pfad. - Tests: TestVerify_RejectsWrongAudience (Pruefung 1), TestVerify_RejectsWrongSigningKey (Pruefung 2, bereits vorhanden), TestCapToTenantScopedRole_NeverGrantsSuperadmin und TestCompleteOIDCLogin_CapsSuperadminClaimToTenantAdmin (Pruefung 3, inkl. Integrationstest mit direkt an der Erlaubnis-Matrix vorbei manipulierter Zuordnungstabelle). - internal/pentest, internal/saml: bestehende Tests an neue NewVerifier-Signatur (audience-Parameter) angepasst. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPsyieCYwvbUBsnN598SbP
This commit is contained in:
1 parent
cc0dc21fe7
commit
9a41f3def9
6 files changed
+210
-28
No files matched your search
+21
-1
@@ -49,7 +49,7 @@ func CompleteOIDCLogin(
|
||||
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 {
|
||||
if _, err := roles.Assign(ctx, u.ID, capToTenantScopedRole(role), "oidc-sso"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,26 @@ func CompleteOIDCLogin(
|
||||
return issuer.Issue(u.ID, tenantSlug)
|
||||
}
|
||||
|
||||
// capToTenantScopedRole stellt technisch und unabhaengig von der
|
||||
// Erlaubnis-Matrix in ldapsync.RoleMappingStore sicher, dass aus externen
|
||||
// OIDC-Claims niemals eine Superadmin-Rolle vergeben werden kann
|
||||
// (Akzeptanzkriterium 3). RoleMappingStore.SetMapping laesst zwar bereits
|
||||
// nur user/tenant_admin als Ziel einer Zuordnung zu, und rbac.Store.Assign
|
||||
// weist superadmin-Zuweisungen ebenfalls zurueck (ErrRoleNotAssignableInTenantScope)
|
||||
// — diese Funktion ist die zusaetzliche, im OIDC-Login-Pfad selbst liegende
|
||||
// Sperre: selbst wenn HighestRoleFor durch eine fehlerhafte oder direkt an
|
||||
// der Datenbank vorbei veraenderte Zuordnungstabelle superadmin liefern
|
||||
// wuerde, kappt der Login-Pfad auf die hoechste zulaessige tenant-gescopte
|
||||
// Rolle, statt entweder superadmin zu vergeben oder den gesamten Login mit
|
||||
// einem Fehler abzubrechen. Superadmin bleibt ausschliesslich lokal ueber
|
||||
// internal/user.SuperadminStore (IAM-01) vergebbar.
|
||||
func capToTenantScopedRole(role rbac.Role) rbac.Role {
|
||||
if role == rbac.RoleSuperadmin {
|
||||
return rbac.RoleTenantAdmin
|
||||
}
|
||||
return role
|
||||
}
|
||||
|
||||
func findOrCreateUser(ctx context.Context, users *user.TenantUserStore, email string) (user.User, error) {
|
||||
creds, err := users.GetByEmailForAuth(ctx, email)
|
||||
if err == nil {
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestCompleteOIDCLogin_EndToEnd(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com")
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
states := NewStateStore(pool)
|
||||
users := user.NewTenantUserStore(pool)
|
||||
roles := rbac.NewStore(pool)
|
||||
@@ -90,7 +90,8 @@ func TestCompleteOIDCLogin_EndToEnd(t *testing.T) {
|
||||
Nonce: nonce,
|
||||
Roles: []string{"sso-admins"},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://idp.example.com",
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{testAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
@@ -126,7 +127,7 @@ func TestCompleteOIDCLogin_UnmappedRoleGrantsNothing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com")
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
states := NewStateStore(pool)
|
||||
users := user.NewTenantUserStore(pool)
|
||||
roles := rbac.NewStore(pool)
|
||||
@@ -146,7 +147,8 @@ func TestCompleteOIDCLogin_UnmappedRoleGrantsNothing(t *testing.T) {
|
||||
// hochzustufen — diese Gruppen wurden NIE gemappt.
|
||||
Roles: []string{"tenant_admin", "superadmin"},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://idp.example.com",
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{testAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
@@ -162,6 +164,96 @@ func TestCompleteOIDCLogin_UnmappedRoleGrantsNothing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 3 + Pruefung 3: eine Unit-Ebene, die unabhaengig von
|
||||
// jeder Datenbank belegt, dass der OIDC-Login-Pfad eine Superadmin-Rolle
|
||||
// technisch niemals weiterreicht.
|
||||
func TestCapToTenantScopedRole_NeverGrantsSuperadmin(t *testing.T) {
|
||||
if got := capToTenantScopedRole(rbac.RoleSuperadmin); got != rbac.RoleTenantAdmin {
|
||||
t.Fatalf("erwartet hoechste zulaessige tenant-gescopte rolle (tenant_admin) statt superadmin, habe %q", got)
|
||||
}
|
||||
if got := capToTenantScopedRole(rbac.RoleTenantAdmin); got != rbac.RoleTenantAdmin {
|
||||
t.Fatalf("tenant_admin muss unveraendert bleiben, habe %q", got)
|
||||
}
|
||||
if got := capToTenantScopedRole(rbac.RoleUser); got != rbac.RoleUser {
|
||||
t.Fatalf("user muss unveraendert bleiben, habe %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 3 + Pruefung 3, Integrationsebene: selbst wenn die
|
||||
// Zuordnungstabelle unter Umgehung von RoleMappingStore.SetMapping (das
|
||||
// bereits nur user/tenant_admin zulaesst) DIREKT auf 'superadmin' gesetzt
|
||||
// wuerde — etwa durch einen fehlerhaften Migrations- oder Admin-Eingriff —
|
||||
// vergibt CompleteOIDCLogin trotzdem hoechstens tenant_admin, nie superadmin,
|
||||
// und bricht den Login dafuer nicht mit einem Fehler ab.
|
||||
func TestCompleteOIDCLogin_CapsSuperadminClaimToTenantAdmin(t *testing.T) {
|
||||
pool, cleanup := setupLoginTest(t)
|
||||
defer cleanup()
|
||||
ctx := context.Background()
|
||||
|
||||
// CHECK-Constraint der Zuordnungstabelle bewusst umgehen, um eine
|
||||
// kompromittierte/fehlerhafte Zuordnung zu simulieren, die
|
||||
// RoleMappingStore.SetMapping normalerweise verhindert.
|
||||
if _, err := pool.Exec(ctx, `ALTER TABLE ldap_group_role_mapping DROP CONSTRAINT IF EXISTS ldap_group_role_mapping_role_check`); err != nil {
|
||||
t.Fatalf("check-constraint entfernen: %v", err)
|
||||
}
|
||||
if _, err := pool.Exec(ctx, `
|
||||
INSERT INTO ldap_group_role_mapping (ldap_group, role) VALUES ('kompromittierte-gruppe', 'superadmin')
|
||||
ON CONFLICT (ldap_group) DO UPDATE SET role = 'superadmin'
|
||||
`); err != nil {
|
||||
t.Fatalf("manipulierte zuordnung einfuegen: %v", err)
|
||||
}
|
||||
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
states := NewStateStore(pool)
|
||||
users := user.NewTenantUserStore(pool)
|
||||
roles := rbac.NewStore(pool)
|
||||
roleMapping := ldapsync.NewRoleMappingStore(pool)
|
||||
issuer := auth.NewTokenIssuer("test-secret-nur-fuer-tests")
|
||||
|
||||
// Gegenprobe: die (kompromittierte) Zuordnung liefert tatsaechlich
|
||||
// superadmin, bevor CompleteOIDCLogin die Sperre anwendet.
|
||||
if role, ok, err := roleMapping.HighestRoleFor(ctx, []string{"kompromittierte-gruppe"}); err != nil || !ok || role != rbac.RoleSuperadmin {
|
||||
t.Fatalf("erwartet manipulierte zuordnung liefert superadmin, habe role=%q ok=%v err=%v", role, ok, err)
|
||||
}
|
||||
|
||||
state, nonce, err := states.Generate(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("generate state: %v", err)
|
||||
}
|
||||
|
||||
email := fmt.Sprintf("oidc-test-%d@example.com", time.Now().UnixNano())
|
||||
claims := Claims{
|
||||
Email: email,
|
||||
Nonce: nonce,
|
||||
// Der externe Claim behauptet ueber die kompromittierte Zuordnung
|
||||
// effektiv eine Superadmin-Rolle.
|
||||
Roles: []string{"kompromittierte-gruppe"},
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{testAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
idToken := signTestToken(t, key, "kid-1", claims)
|
||||
|
||||
if _, err := CompleteOIDCLogin(ctx, verifier, states, users, roles, roleMapping, issuer, "acme", state, idToken); err != nil {
|
||||
t.Fatalf("complete oidc login: %v", err)
|
||||
}
|
||||
|
||||
userID := mustGetUserID(t, ctx, users, email)
|
||||
assignment, err := roles.Get(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("rollenzuweisung lesen: %v", err)
|
||||
}
|
||||
if assignment.Role == rbac.RoleSuperadmin {
|
||||
t.Fatal("SICHERHEITSVERLETZUNG: superadmin ueber externen oidc-claim vergeben")
|
||||
}
|
||||
if assignment.Role != rbac.RoleTenantAdmin {
|
||||
t.Fatalf("erwartet hoechste zulaessige tenant-gescopte rolle (tenant_admin), habe %q", assignment.Role)
|
||||
}
|
||||
}
|
||||
|
||||
func mustGetUserID(t *testing.T, ctx context.Context, users *user.TenantUserStore, email string) string {
|
||||
t.Helper()
|
||||
creds, err := users.GetByEmailForAuth(ctx, email)
|
||||
|
||||
+41
-6
@@ -22,23 +22,43 @@ type Claims struct {
|
||||
// Verifier prueft ID-Tokens gegen den JWKS-Schluesselsatz eines konfigurierten
|
||||
// Providers (Akzeptanzkriterium 3: fehlerhafte/abgelaufene Tokens werden
|
||||
// abgewiesen).
|
||||
//
|
||||
// IAM-18: issuer und audience werden AUSSCHLIESSLICH ueber NewVerifier aus der
|
||||
// je-Tenant hinterlegten Konfiguration gesetzt (Server-/Tenant-Konfiguration,
|
||||
// niemals aus dem Token selbst gelesen) und bei jeder Verifikation exakt
|
||||
// gegen die Token-Claims abgeglichen (Akzeptanzkriterium 1). keys wird
|
||||
// ebenfalls ausschliesslich aus der JWKS-URL DIESES konfigurierten Issuers
|
||||
// befuellt (siehe jwks.go/ParseJWKS) — ein Token kann weder einen eigenen
|
||||
// Issuer noch einen eigenen Signaturschluessel vorschreiben
|
||||
// (Akzeptanzkriterium 2).
|
||||
type Verifier struct {
|
||||
keys map[string]*rsa.PublicKey
|
||||
issuer string
|
||||
keys map[string]*rsa.PublicKey
|
||||
issuer string
|
||||
audience string
|
||||
}
|
||||
|
||||
func NewVerifier(keys map[string]*rsa.PublicKey, issuer string) *Verifier {
|
||||
return &Verifier{keys: keys, issuer: issuer}
|
||||
// NewVerifier bindet einen Verifier fest an den fuer einen Tenant
|
||||
// konfigurierten Issuer, die konfigurierte Audience und die aus dessen JWKS
|
||||
// geladenen Schluessel. Alle drei Werte kommen aus der Tenant-Konfiguration
|
||||
// des Aufrufers — nicht aus dem zu pruefenden Token.
|
||||
func NewVerifier(keys map[string]*rsa.PublicKey, issuer, audience string) *Verifier {
|
||||
return &Verifier{keys: keys, issuer: issuer, audience: audience}
|
||||
}
|
||||
|
||||
// Verify prueft Signatur (gegen JWKS), Ablauf (jwt-Bibliothek lehnt
|
||||
// abgelaufene Tokens automatisch ab) und Aussteller.
|
||||
// Verify prueft Signatur (ausschliesslich gegen die uebergebenen JWKS-
|
||||
// Schluessel des konfigurierten Issuers), Ablauf (jwt-Bibliothek lehnt
|
||||
// abgelaufene Tokens automatisch ab), Aussteller und Audience
|
||||
// (Akzeptanzkriterium 1 + 2).
|
||||
func (v *Verifier) Verify(idToken string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(idToken, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
if _, ok := t.Method.(*jwt.SigningMethodRSA); !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
// Der Signaturschluessel kommt AUSSCHLIESSLICH aus der beim
|
||||
// Verifier hinterlegten JWKS-Schluesselmenge des konfigurierten
|
||||
// Issuers — der kid-Header waehlt nur INNERHALB dieser Menge aus,
|
||||
// ein Token kann keinen fremden Schluessel oder Issuer erzwingen.
|
||||
kid, _ := t.Header["kid"].(string)
|
||||
key, ok := v.keys[kid]
|
||||
if !ok {
|
||||
@@ -52,9 +72,24 @@ func (v *Verifier) Verify(idToken string) (*Claims, error) {
|
||||
if claims.Issuer != v.issuer {
|
||||
return nil, fmt.Errorf("%w: unerwarteter aussteller %q", ErrInvalidToken, claims.Issuer)
|
||||
}
|
||||
if !containsAudience(claims.RegisteredClaims.Audience, v.audience) {
|
||||
return nil, fmt.Errorf("%w: unerwartete audience %q", ErrInvalidToken, claims.RegisteredClaims.Audience)
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// containsAudience prueft exakten Abgleich (kein Praefix-/Teilstring-Match)
|
||||
// gegen den aud-Claim, der laut RFC 7519 ein einzelner String oder eine
|
||||
// Liste sein kann.
|
||||
func containsAudience(audience jwt.ClaimStrings, expected string) bool {
|
||||
for _, a := range audience {
|
||||
if a == expected {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// VerifyWithNonce prueft zusaetzlich, dass der nonce-Claim im Token exakt
|
||||
// dem bei der Anfrage ausgestellten Nonce entspricht — verhindert Replay
|
||||
// eines fremden, gueltigen ID-Tokens (Akzeptanzkriterium/Pruefung 3).
|
||||
|
||||
@@ -12,6 +12,11 @@ import (
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
testIssuer = "https://idp.example.com"
|
||||
testAudience = "https://nexarch.example.com/tenants/acme"
|
||||
)
|
||||
|
||||
func generateTestKey(t *testing.T) *rsa.PrivateKey {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
@@ -34,14 +39,15 @@ func signTestToken(t *testing.T, key *rsa.PrivateKey, kid string, claims Claims)
|
||||
|
||||
func TestVerify_AcceptsValidToken(t *testing.T) {
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com")
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
|
||||
claims := Claims{
|
||||
Subject: "user-123",
|
||||
Email: "person@example.com",
|
||||
Nonce: "erwarteter-nonce",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://idp.example.com",
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{testAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
@@ -59,12 +65,13 @@ func TestVerify_AcceptsValidToken(t *testing.T) {
|
||||
// Akzeptanzkriterium 3 + Pruefung 2: Token-Signatur- und Ablaufpruefung.
|
||||
func TestVerify_RejectsExpiredToken(t *testing.T) {
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com")
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
|
||||
claims := Claims{
|
||||
Subject: "user-123",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://idp.example.com",
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{testAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), // abgelaufen
|
||||
},
|
||||
}
|
||||
@@ -79,12 +86,13 @@ func TestVerify_RejectsWrongSigningKey(t *testing.T) {
|
||||
realKey := generateTestKey(t)
|
||||
attackerKey := generateTestKey(t)
|
||||
// Verifier kennt NUR den echten oeffentlichen Schluessel.
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &realKey.PublicKey}, "https://idp.example.com")
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &realKey.PublicKey}, testIssuer, testAudience)
|
||||
|
||||
claims := Claims{
|
||||
Subject: "user-123",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://idp.example.com",
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{testAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
@@ -98,7 +106,7 @@ func TestVerify_RejectsWrongSigningKey(t *testing.T) {
|
||||
|
||||
func TestVerify_RejectsWrongIssuer(t *testing.T) {
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com")
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
|
||||
claims := Claims{
|
||||
Subject: "user-123",
|
||||
@@ -114,16 +122,40 @@ func TestVerify_RejectsWrongIssuer(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium 1 + Pruefung 1: falscher Issuer/Audience wird abgelehnt.
|
||||
// Issuer und Audience kommen ausschliesslich aus der beim Verifier fest
|
||||
// hinterlegten Tenant-Konfiguration (siehe NewVerifier-Aufruf oben) — das
|
||||
// Token selbst kann seine eigene Audience nicht durchsetzen.
|
||||
func TestVerify_RejectsWrongAudience(t *testing.T) {
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
|
||||
claims := Claims{
|
||||
Subject: "user-123",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{"https://ein-anderer-mandant.example.com"},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
tok := signTestToken(t, key, "kid-1", claims)
|
||||
|
||||
if _, err := verifier.Verify(tok); err == nil {
|
||||
t.Fatal("erwartet fehler bei unerwarteter audience")
|
||||
}
|
||||
}
|
||||
|
||||
// Akzeptanzkriterium/Pruefung 3: Nonce-Handling gegen Replay.
|
||||
func TestVerifyWithNonce_RejectsMismatch(t *testing.T) {
|
||||
key := generateTestKey(t)
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com")
|
||||
verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, testIssuer, testAudience)
|
||||
|
||||
claims := Claims{
|
||||
Subject: "user-123",
|
||||
Nonce: "nonce-a",
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://idp.example.com",
|
||||
Issuer: testIssuer,
|
||||
Audience: jwt.ClaimStrings{testAudience},
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
@@ -173,9 +205,9 @@ func TestParseJWKS_RoundTrip(t *testing.T) {
|
||||
}
|
||||
|
||||
// Der wiederhergestellte Schluessel muss ein damit signiertes Token auch tatsaechlich verifizieren.
|
||||
verifier := NewVerifier(parsed, "https://idp.example.com")
|
||||
verifier := NewVerifier(parsed, testIssuer, testAudience)
|
||||
claims := Claims{Subject: "x", RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://idp.example.com", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
Issuer: testIssuer, Audience: jwt.ClaimStrings{testAudience}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
}}
|
||||
tok := signTestToken(t, key, "kid-1", claims)
|
||||
if _, err := verifier.Verify(tok); err != nil {
|
||||
|
||||
@@ -117,7 +117,7 @@ func TestPentest_OIDC_AlgNoneAttack(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("rsa key: %v", err)
|
||||
}
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &rsaKey.PublicKey}, "https://idp.example.test")
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &rsaKey.PublicKey}, "https://idp.example.test", "https://nexarch.example.test/tenants/pentest")
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "angreifer@example.test",
|
||||
@@ -150,7 +150,7 @@ func TestPentest_OIDC_ForeignKeySignature(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("attacker key: %v", err)
|
||||
}
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"trusted-kid": &trustedKey.PublicKey}, "https://idp.example.test")
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"trusted-kid": &trustedKey.PublicKey}, "https://idp.example.test", "https://nexarch.example.test/tenants/pentest")
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "angreifer@example.test",
|
||||
@@ -177,11 +177,12 @@ func TestPentest_OIDC_TamperedClaims(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("rsa key: %v", err)
|
||||
}
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &rsaKey.PublicKey}, "https://idp.example.test")
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &rsaKey.PublicKey}, "https://idp.example.test", "https://nexarch.example.test/tenants/pentest")
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "normaler-benutzer@example.test",
|
||||
"iss": "https://idp.example.test",
|
||||
"aud": "https://nexarch.example.test/tenants/pentest",
|
||||
"roles": []string{"user"},
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
}
|
||||
@@ -206,6 +207,7 @@ func TestPentest_OIDC_TamperedClaims(t *testing.T) {
|
||||
tamperedClaims := jwt.MapClaims{
|
||||
"sub": "normaler-benutzer@example.test",
|
||||
"iss": "https://idp.example.test",
|
||||
"aud": "https://nexarch.example.test/tenants/pentest",
|
||||
"roles": []string{"superadmin"}, // Rechteausweitung
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
}
|
||||
@@ -232,11 +234,12 @@ func TestPentest_OIDC_NonceReplay(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("rsa key: %v", err)
|
||||
}
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &rsaKey.PublicKey}, "https://idp.example.test")
|
||||
verifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &rsaKey.PublicKey}, "https://idp.example.test", "https://nexarch.example.test/tenants/pentest")
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": "benutzer@example.test",
|
||||
"iss": "https://idp.example.test",
|
||||
"aud": "https://nexarch.example.test/tenants/pentest",
|
||||
"nonce": "nonce-aus-login-versuch-1",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestSAMLAndOIDC_WorkInParallelForDifferentTenants(t *testing.T) {
|
||||
|
||||
// Tenant "oidc-tenant": OIDC konfiguriert, voellig unabhaengige Datenbank.
|
||||
oidcKey := generateTestIdPKey(t)
|
||||
oidcVerifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &oidcKey.PublicKey}, "https://oidc-idp.example.com")
|
||||
oidcVerifier := oidc.NewVerifier(map[string]*rsa.PublicKey{"kid-1": &oidcKey.PublicKey}, "https://oidc-idp.example.com", "https://nexarch.example.com/tenants/oidc-tenant")
|
||||
oidcStates := oidc.NewStateStore(poolOIDC)
|
||||
oidcUsers := user.NewTenantUserStore(poolOIDC)
|
||||
oidcRoles := rbac.NewStore(poolOIDC)
|
||||
@@ -197,7 +197,7 @@ func TestSAMLAndOIDC_WorkInParallelForDifferentTenants(t *testing.T) {
|
||||
Email: oidcEmail,
|
||||
Nonce: nonce,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "https://oidc-idp.example.com", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
Issuer: "https://oidc-idp.example.com", Audience: jwt.ClaimStrings{"https://nexarch.example.com/tenants/oidc-tenant"}, ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
|
||||
},
|
||||
}
|
||||
idToken := signOIDCTestToken(t, oidcKey, claims)
|
||||
|
||||
Reference in new issue
Block a user