package oidc import ( "crypto/rand" "crypto/rsa" "encoding/base64" "encoding/binary" "encoding/json" "testing" "time" "github.com/golang-jwt/jwt/v5" ) func generateTestKey(t *testing.T) *rsa.PrivateKey { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("generate rsa key: %v", err) } return key } func signTestToken(t *testing.T, key *rsa.PrivateKey, kid string, claims Claims) string { t.Helper() token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) token.Header["kid"] = kid s, err := token.SignedString(key) if err != nil { t.Fatalf("sign token: %v", err) } return s } func TestVerify_AcceptsValidToken(t *testing.T) { key := generateTestKey(t) verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com") claims := Claims{ Subject: "user-123", Email: "person@example.com", Nonce: "erwarteter-nonce", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "https://idp.example.com", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), }, } tok := signTestToken(t, key, "kid-1", claims) got, err := verifier.Verify(tok) if err != nil { t.Fatalf("verify: %v", err) } if got.Email != "person@example.com" { t.Fatalf("email = %q", got.Email) } } // 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") claims := Claims{ Subject: "user-123", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "https://idp.example.com", ExpiresAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)), // abgelaufen }, } tok := signTestToken(t, key, "kid-1", claims) if _, err := verifier.Verify(tok); err == nil { t.Fatal("erwartet fehler bei abgelaufenem token") } } 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") claims := Claims{ Subject: "user-123", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "https://idp.example.com", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), }, } // Mit dem Angreifer-Schluessel signiert, aber unter demselben kid wie der echte. tok := signTestToken(t, attackerKey, "kid-1", claims) if _, err := verifier.Verify(tok); err == nil { t.Fatal("erwartet fehler bei fremd signiertem token") } } func TestVerify_RejectsWrongIssuer(t *testing.T) { key := generateTestKey(t) verifier := NewVerifier(map[string]*rsa.PublicKey{"kid-1": &key.PublicKey}, "https://idp.example.com") claims := Claims{ Subject: "user-123", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "https://ganz-anderer-idp.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 unerwartetem aussteller") } } // 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") claims := Claims{ Subject: "user-123", Nonce: "nonce-a", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "https://idp.example.com", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)), }, } tok := signTestToken(t, key, "kid-1", claims) if _, err := verifier.VerifyWithNonce(tok, "nonce-b"); err == nil { t.Fatal("erwartet fehler bei nonce-mismatch (moeglicher replay eines fremden tokens)") } if _, err := verifier.VerifyWithNonce(tok, "nonce-a"); err != nil { t.Fatalf("erwartet erfolg bei passendem nonce: %v", err) } } func TestParseJWKS_RoundTrip(t *testing.T) { key := generateTestKey(t) nBytes := key.PublicKey.N.Bytes() eBuf := make([]byte, 8) binary.BigEndian.PutUint64(eBuf, uint64(key.PublicKey.E)) // Fuehrende Nullbytes des Exponenten abschneiden (ueblich bei JWK-Kodierung). eStart := 0 for eStart < len(eBuf)-1 && eBuf[eStart] == 0 { eStart++ } set := jwkSet{Keys: []jwk{{ Kid: "kid-1", Kty: "RSA", N: base64.RawURLEncoding.EncodeToString(nBytes), E: base64.RawURLEncoding.EncodeToString(eBuf[eStart:]), }}} data, err := json.Marshal(set) if err != nil { t.Fatalf("marshal: %v", err) } parsed, err := ParseJWKS(data) if err != nil { t.Fatalf("parse jwks: %v", err) } got, ok := parsed["kid-1"] if !ok { t.Fatal("erwartet kid-1 im ergebnis") } if got.E != key.PublicKey.E || got.N.Cmp(key.PublicKey.N) != 0 { t.Fatalf("erwartet identischen oeffentlichen schluessel nach roundtrip") } // Der wiederhergestellte Schluessel muss ein damit signiertes Token auch tatsaechlich verifizieren. verifier := NewVerifier(parsed, "https://idp.example.com") claims := Claims{Subject: "x", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "https://idp.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.Fatalf("verify mit aus jwks wiederhergestelltem schluessel: %v", err) } }