package handler import ( "context" "crypto/rand" "crypto/rsa" "encoding/base64" "encoding/json" "math/big" "net/http" "net/http/httptest" "testing" "time" "github.com/Wei-Shaw/sub2api/internal/config" "github.com/golang-jwt/jwt/v5" "github.com/stretchr/testify/require" ) func TestOIDCSyntheticEmailStableAndDistinct(t *testing.T) { k1 := oidcIdentityKey("https://issuer.example.com", "subject-a") k2 := oidcIdentityKey("https://issuer.example.com", "subject-b") e1 := oidcSyntheticEmailFromIdentityKey(k1) e1Again := oidcSyntheticEmailFromIdentityKey(k1) e2 := oidcSyntheticEmailFromIdentityKey(k2) require.Equal(t, e1, e1Again) require.NotEqual(t, e1, e2) require.Contains(t, e1, "@oidc-connect.invalid") } func TestBuildOIDCAuthorizeURLIncludesNonceAndPKCE(t *testing.T) { cfg := config.OIDCConnectConfig{ AuthorizeURL: "https://issuer.example.com/auth", ClientID: "cid", Scopes: "openid email profile", UsePKCE: true, } u, err := buildOIDCAuthorizeURL(cfg, "state123", "nonce123", "challenge123", "https://app.example.com/callback") require.NoError(t, err) require.Contains(t, u, "nonce=nonce123") require.Contains(t, u, "code_challenge=challenge123") require.Contains(t, u, "code_challenge_method=S256") require.Contains(t, u, "scope=openid+email+profile") } func TestOIDCParseAndValidateIDToken(t *testing.T) { priv, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) kid := "kid-1" jwks := oidcJWKSet{Keys: []oidcJWK{buildRSAJWK(kid, &priv.PublicKey)}} srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.NoError(t, json.NewEncoder(w).Encode(jwks)) })) defer srv.Close() now := time.Now() claims := oidcIDTokenClaims{ Nonce: "nonce-ok", Azp: "client-1", RegisteredClaims: jwt.RegisteredClaims{ Issuer: "https://issuer.example.com", Subject: "subject-1", Audience: jwt.ClaimStrings{"client-1", "another-aud"}, IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now.Add(-30 * time.Second)), ExpiresAt: jwt.NewNumericDate(now.Add(5 * time.Minute)), }, } tok := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) tok.Header["kid"] = kid signed, err := tok.SignedString(priv) require.NoError(t, err) cfg := config.OIDCConnectConfig{ ClientID: "client-1", IssuerURL: "https://issuer.example.com", JWKSURL: srv.URL, AllowedSigningAlgs: "RS256", ClockSkewSeconds: 120, } parsed, err := oidcParseAndValidateIDToken(context.Background(), cfg, signed, "nonce-ok") require.NoError(t, err) require.Equal(t, "subject-1", parsed.Subject) require.Equal(t, "https://issuer.example.com", parsed.Issuer) _, err = oidcParseAndValidateIDToken(context.Background(), cfg, signed, "bad-nonce") require.Error(t, err) } func buildRSAJWK(kid string, pub *rsa.PublicKey) oidcJWK { n := base64.RawURLEncoding.EncodeToString(pub.N.Bytes()) e := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(pub.E)).Bytes()) return oidcJWK{ Kty: "RSA", Kid: kid, Use: "sig", Alg: "RS256", N: n, E: e, } }