Files
traefikoidc/audience_test.go
T
lukaszraczylo 9cbca4c4fb fix(refresh): honor userIdentifierClaim in token refresh path (#132)
patch-release

The refresh path in token_manager.go hardcoded the "email" claim when
extracting the user identifier from a refreshed ID token, ignoring the
configured userIdentifierClaim. Keycloak users without an email claim
(using sub or another identifier) were kicked out on refresh even
though their initial login worked.

The callback path (auth_flow.go:226-239) already honored
userIdentifierClaim with "sub" fallback; PR #100 (commit a316a98)
added that support but missed the refresh path.

Mirror the callback logic in refreshToken so both paths behave the same.

Cleanup: rename Get/SetEmail to Get/SetUserIdentifier on SessionData
to match the actual semantics. The slot already stored the configured
identifier (email, sub, oid, upn, preferred_username), only the API
name was misleading. Storage key "email" → "user_identifier" and
combinedSessionPayload field E (json:"e") → Ui (json:"ui").

Compat note: existing user sessions invalidate on upgrade — every active
user re-authenticates once after deploying this change.
2026-05-07 09:21:41 +01:00

1519 lines
48 KiB
Go

package traefikoidc
import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"golang.org/x/time/rate"
)
// =============================================================================
// AUDIENCE CONFIGURATION TESTS
// =============================================================================
// TestAudienceConfiguration tests the custom audience configuration feature
func TestAudienceConfiguration(t *testing.T) {
tests := []struct {
name string
configAudience string
clientID string
expectedAudience string
}{
{
name: "no custom audience - uses clientID",
configAudience: "",
clientID: "test-client-id",
expectedAudience: "test-client-id",
},
{
name: "custom audience specified",
configAudience: "api://custom-audience",
clientID: "test-client-id",
expectedAudience: "api://custom-audience",
},
{
name: "auth0 style custom audience",
configAudience: "https://api.example.com",
clientID: "test-client-id",
expectedAudience: "https://api.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create config with custom audience
config := CreateConfig()
config.ProviderURL = "https://provider.example.com"
config.ClientID = tt.clientID
config.ClientSecret = "test-secret"
config.SessionEncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
config.CallbackURL = "/callback"
config.Audience = tt.configAudience
// Create middleware instance
next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
traefikOidc, err := NewWithContext(context.Background(), config, next, "test")
if err != nil {
t.Fatalf("Failed to create middleware: %v", err)
}
// Verify audience is set correctly
if traefikOidc.audience != tt.expectedAudience {
t.Errorf("Expected audience %s, got %s", tt.expectedAudience, traefikOidc.audience)
}
// Cleanup
_ = traefikOidc.Close()
})
}
}
// TestAudienceValidation tests the audience validation in Config.Validate()
func TestAudienceValidation(t *testing.T) {
tests := []struct {
name string
audience string
errorContains string
expectError bool
}{
{
name: "valid custom audience URL",
audience: "https://api.example.com",
expectError: false,
},
{
name: "valid azure style audience",
audience: "api://12345678-1234-1234-1234-123456789012",
expectError: false,
},
{
name: "empty audience is valid (uses clientID)",
audience: "",
expectError: false,
},
{
name: "http URL not allowed",
audience: "http://api.example.com",
expectError: true,
errorContains: "audience URL must use HTTPS",
},
{
name: "wildcard not allowed",
audience: "https://*.example.com",
expectError: true,
errorContains: "audience must not contain wildcards",
},
{
name: "too long audience",
audience: "https://" + string(make([]byte, 250)) + ".com",
expectError: true,
errorContains: "audience must not exceed 256 characters",
},
{
name: "invalid characters",
audience: "api://test\ninjection",
expectError: true,
errorContains: "audience contains invalid characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := CreateConfig()
config.ProviderURL = "https://provider.example.com"
config.ClientID = "test-client"
config.ClientSecret = "test-secret"
config.SessionEncryptionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
config.CallbackURL = "/callback"
config.Audience = tt.audience
err := config.Validate()
if tt.expectError {
if err == nil {
t.Errorf("Expected error but got none")
} else if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf("Expected error containing '%s', got: %v", tt.errorContains, err)
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
})
}
}
// =============================================================================
// CONFIG AUDIENCE VALIDATION TESTS
// =============================================================================
// TestConfigAudienceValidation tests the Config.Validate() method for the audience field
func TestConfigAudienceValidation(t *testing.T) {
tests := []struct {
name string
audience string
errContains string
wantErr bool
}{
{
name: "Empty audience is valid for backward compatibility",
audience: "",
wantErr: false,
},
{
name: "Valid HTTPS URL audience Auth0 format",
audience: "https://api.example.com",
wantErr: false,
},
{
name: "Valid identifier audience",
audience: "my-api",
wantErr: false,
},
{
name: "Valid Azure AD Application ID URI format",
audience: "api://12345-guid-67890",
wantErr: false,
},
{
name: "Valid Auth0 API identifier",
audience: "https://my-company.auth0.com/api/v2/",
wantErr: false,
},
{
name: "HTTP URL audience should fail",
audience: "http://api.example.com",
wantErr: true,
errContains: "must use HTTPS",
},
{
name: "Audience with wildcard should fail",
audience: "https://api.*.example.com",
wantErr: true,
errContains: "must not contain wildcards",
},
{
name: "Audience with single asterisk should fail",
audience: "*",
wantErr: true,
errContains: "must not contain wildcards",
},
{
name: "Audience over 256 characters should fail",
audience: strings.Repeat("a", 257),
wantErr: true,
errContains: "must not exceed 256 characters",
},
{
name: "Audience with newline should fail",
audience: "my-api\ninjection",
wantErr: true,
errContains: "contains invalid characters",
},
{
name: "Audience with carriage return should fail",
audience: "my-api\rinjection",
wantErr: true,
errContains: "contains invalid characters",
},
{
name: "Audience with tab should fail",
audience: "my-api\tinjection",
wantErr: true,
errContains: "contains invalid characters",
},
{
name: "Valid audience exactly 256 characters",
audience: strings.Repeat("a", 256),
wantErr: false,
},
{
name: "Valid simple identifier",
audience: "my-service-api",
wantErr: false,
},
{
name: "Valid URN format",
audience: "urn:myservice:api:v1",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := CreateConfig()
config.ProviderURL = "https://provider.example.com"
config.ClientID = "test-client-id"
config.ClientSecret = "test-client-secret"
config.CallbackURL = "/callback"
config.SessionEncryptionKey = strings.Repeat("a", MinSessionEncryptionKeyLength)
config.Audience = tt.audience
err := config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("Error message should contain %q, got: %v", tt.errContains, err)
}
})
}
}
// =============================================================================
// AUTH0 SCENARIO TESTS
// =============================================================================
// TestAuth0Scenario1WithCustomAudience tests Auth0 scenario 1:
// - Custom audience configured in plugin
// - Authorize endpoint called WITH audience parameter
// - ID token: aud = client_id
// - Access token: aud = [userinfo, custom_audience]
// Expected: Both tokens validate correctly
func TestAuth0Scenario1WithCustomAudience(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
customAudience := "https://my-api.example.com"
ts.tOidc.audience = customAudience
// Create ID token with aud = client_id (OIDC standard)
idToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": "test-client-id", // ID token always has client_id
"nonce": "test-nonce-scenario1", // ID tokens have nonce per OIDC spec
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"email": "test@example.com",
"jti": "id-token-jti",
})
if err != nil {
t.Fatalf("Failed to create ID token: %v", err)
}
// Create access token with aud = [userinfo, custom_audience]
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": []interface{}{
"https://test-issuer.com/userinfo",
customAudience, // Custom API audience
},
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"scope": "openid profile email read:data", // Access tokens have scope
"jti": "access-token-jti",
})
if err != nil {
t.Fatalf("Failed to create access token: %v", err)
}
// Verify ID token validates against client_id
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(idToken)
if err != nil {
t.Errorf("ID token validation failed (should validate against client_id): %v", err)
}
// Verify access token validates against custom audience
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(accessToken)
if err != nil {
t.Errorf("Access token validation failed (should validate against custom audience): %v", err)
}
// Verify buildAuthURL includes audience parameter (URL-encoded)
authURL := ts.tOidc.buildAuthURL("https://example.com/callback", "state", "nonce", "")
if !strings.Contains(authURL, "audience=") {
t.Errorf("Auth URL should contain audience parameter when custom audience is configured, got: %s", authURL)
}
// Verify the audience is properly URL-encoded (contains %3A for :, %2F for /)
if !strings.Contains(authURL, "audience=https%3A%2F%2Fmy-api.example.com") {
t.Errorf("Auth URL should contain URL-encoded custom audience, got: %s", authURL)
}
}
// TestAuth0Scenario2DefaultAudience tests Auth0 scenario 2:
// - No custom audience configured (defaults to client_id)
// - Authorize endpoint called WITHOUT audience parameter
// - ID token: aud = client_id
// - Access token: aud = [userinfo, default_audience] (no client_id)
// Expected: ID token validates, access token falls back to ID token validation
func TestAuth0Scenario2DefaultAudience(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
// No custom audience - defaults to client_id
ts.tOidc.audience = ts.tOidc.clientID
// Create ID token with aud = client_id
idToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": "test-client-id",
"nonce": "test-nonce-scenario2", // ID tokens have nonce per OIDC spec
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"email": "test@example.com",
"jti": "id-token-jti-2",
})
if err != nil {
t.Fatalf("Failed to create ID token: %v", err)
}
// Create access token with aud = [userinfo, some_default_audience]
// This represents Auth0's default audience behavior
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": []interface{}{
"https://test-issuer.com/userinfo",
"https://test-issuer.com/api/v2/", // Default Auth0 Management API
},
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"scope": "openid profile email",
"jti": "access-token-jti-2",
})
if err != nil {
t.Fatalf("Failed to create access token: %v", err)
}
// Verify ID token validates
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(idToken)
if err != nil {
t.Errorf("ID token validation failed: %v", err)
}
// Access token won't have client_id in aud, so it will fail validation
// This is expected for scenario 2 - the session validation relies on ID token
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(accessToken)
if err == nil {
t.Logf("Access token validation passed (unexpected but OK if client_id is in aud array)")
} else {
// Expected failure - access token doesn't have client_id in aud
t.Logf("Access token validation failed as expected (aud doesn't contain client_id): %v", err)
}
// Verify buildAuthURL does NOT include audience parameter (since audience == client_id)
authURL := ts.tOidc.buildAuthURL("https://example.com/callback", "state", "nonce", "")
if strings.Contains(authURL, "audience=") {
t.Errorf("Auth URL should NOT contain audience parameter when audience equals client_id, got: %s", authURL)
}
}
// TestAuth0Scenario3OpaqueAccessToken tests Auth0 scenario 3:
// - No custom audience configured
// - No default audience in Auth0
// - ID token: aud = client_id
// - Access token: opaque (not JWT)
// Expected: ID token validates, opaque access token is accepted
func TestAuth0Scenario3OpaqueAccessToken(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
// Enable opaque tokens for this scenario (Option C requirement)
ts.tOidc.allowOpaqueTokens = true
// No custom audience
ts.tOidc.audience = ts.tOidc.clientID
// Create ID token
idToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": "test-client-id",
"nonce": "test-nonce-scenario3", // ID tokens have nonce per OIDC spec
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"email": "test@example.com",
"jti": "id-token-jti-3",
})
if err != nil {
t.Fatalf("Failed to create ID token: %v", err)
}
// Opaque access token (not a JWT - just a random string)
opaqueAccessToken := "opaque_access_token_random_string_12345"
// Verify ID token validates
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(idToken)
if err != nil {
t.Errorf("ID token validation failed: %v", err)
}
// Opaque access token should fail JWT validation (expected)
err = ts.tOidc.VerifyToken(opaqueAccessToken)
if err == nil {
t.Error("Opaque access token should fail JWT validation")
} else {
t.Logf("Opaque access token correctly rejected by JWT validator: %v", err)
}
// Test that validateStandardTokens handles opaque tokens correctly
// by falling back to ID token validation
req := httptest.NewRequest("GET", "https://example.com/test", nil)
session, err := ts.tOidc.sessionManager.GetSession(req)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
session.SetAuthenticated(true)
session.SetAccessToken(opaqueAccessToken)
session.SetIDToken(idToken)
authenticated, needsRefresh, expired := ts.tOidc.validateStandardTokens(session)
if !authenticated || needsRefresh || expired {
t.Errorf("Session with opaque access token and valid ID token should be authenticated. Got: auth=%v, refresh=%v, expired=%v",
authenticated, needsRefresh, expired)
}
}
// TestAuth0AudienceArrayValidation tests that audience validation
// correctly handles array audiences (common in Auth0)
func TestAuth0AudienceArrayValidation(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
customAudience := "https://my-api.example.com"
ts.tOidc.audience = customAudience
// Access token with audience as array containing our custom audience
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": []interface{}{
"https://test-issuer.com/userinfo",
customAudience,
"https://another-api.example.com",
},
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"scope": "openid profile email read:data write:data",
"jti": "array-aud-token-jti",
})
if err != nil {
t.Fatalf("Failed to create access token: %v", err)
}
// Should validate successfully - custom audience is in the array
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(accessToken)
if err != nil {
t.Errorf("Access token with audience array should validate when custom audience is present: %v", err)
}
}
// TestAuth0MismatchedAudience tests that tokens with wrong audience fail validation
func TestAuth0MismatchedAudience(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
customAudience := "https://my-api.example.com"
ts.tOidc.audience = customAudience
// Access token with WRONG audience
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": []interface{}{
"https://test-issuer.com/userinfo",
"https://different-api.example.com", // Wrong audience
},
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"scope": "openid profile email",
"jti": "wrong-aud-token-jti",
})
if err != nil {
t.Fatalf("Failed to create access token: %v", err)
}
// Should fail validation - audience doesn't match
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(accessToken)
if err == nil {
t.Error("Access token with wrong audience should fail validation")
} else if !strings.Contains(err.Error(), "invalid audience") {
t.Errorf("Expected 'invalid audience' error, got: %v", err)
}
}
// TestAuth0Scenario2StrictMode tests strict audience validation mode:
// - Scenario 2 (access token with wrong audience) should be REJECTED
// - strictAudienceValidation=true prevents fallback to ID token
// - This addresses Allan's security concerns about audience bypass
func TestAuth0Scenario2StrictMode(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
// Enable strict mode to prevent Scenario 2 bypass (Option C)
ts.tOidc.strictAudienceValidation = true
// Configure custom audience
customAudience := "https://my-api.example.com"
ts.tOidc.audience = customAudience
// Create ID token with aud = client_id (valid)
idToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": "test-client-id",
"nonce": "test-nonce-strict",
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"email": "test@example.com",
"jti": "id-token-strict-jti",
})
if err != nil {
t.Fatalf("Failed to create ID token: %v", err)
}
// Create access token with WRONG audience (doesn't include custom audience)
accessToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": []interface{}{
"https://test-issuer.com/userinfo",
"https://wrong-api.example.com", // Wrong audience - not our custom audience
},
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"scope": "openid profile email",
"jti": "access-token-strict-jti",
})
if err != nil {
t.Fatalf("Failed to create access token: %v", err)
}
// Test session validation with wrong access token and valid ID token
req := httptest.NewRequest("GET", "https://example.com/test", nil)
session, err := ts.tOidc.sessionManager.GetSession(req)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
session.SetAuthenticated(true)
session.SetAccessToken(accessToken)
session.SetIDToken(idToken)
session.SetRefreshToken("test-refresh-token") // Add refresh token so it can attempt refresh
// In strict mode, this should FAIL (no fallback to ID token)
authenticated, needsRefresh, expired := ts.tOidc.validateStandardTokens(session)
if authenticated {
t.Errorf("Strict mode: Session with wrong access token audience should be rejected, but got authenticated=true")
}
if !needsRefresh {
t.Errorf("Strict mode: Should signal refresh needed after rejection, got needsRefresh=%v", needsRefresh)
}
if expired {
t.Errorf("Strict mode: Should not mark as expired (should try refresh first), got expired=%v", expired)
}
t.Logf("Strict mode correctly rejected Scenario 2 (access token audience mismatch)")
}
// TestIDTokenAlwaysValidatesAgainstClientID verifies that ID tokens
// are ALWAYS validated against client_id, regardless of configured audience
func TestIDTokenAlwaysValidatesAgainstClientID(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
// Configure a custom audience different from client_id
customAudience := "https://my-api.example.com"
ts.tOidc.audience = customAudience
// Create ID token with aud = client_id (per OIDC spec)
idToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": "test-client-id", // ID token MUST have client_id
"nonce": "test-nonce-123", // ID tokens have nonce for replay protection
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"email": "test@example.com",
"jti": "id-token-client-id-jti",
})
if err != nil {
t.Fatalf("Failed to create ID token: %v", err)
}
// Should validate successfully - ID tokens are checked against client_id
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(idToken)
if err != nil {
t.Errorf("ID token should validate against client_id even when custom audience is configured: %v", err)
}
// Create ID token with WRONG audience (should fail)
wrongIDToken, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": customAudience, // WRONG - should be client_id
"nonce": "test-nonce-wrong-456", // ID token has nonce, so it will be detected as ID token
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Unix()),
"sub": "test-user",
"email": "test@example.com",
"jti": "wrong-id-token-jti",
})
if err != nil {
t.Fatalf("Failed to create wrong ID token: %v", err)
}
// Should fail - ID tokens must have client_id as audience
cleanupReplayCache()
initReplayCache()
err = ts.tOidc.VerifyToken(wrongIDToken)
if err == nil {
t.Error("ID token with custom audience (not client_id) should fail validation")
}
}
// =============================================================================
// JWT AUDIENCE VERIFICATION TESTS
// =============================================================================
// TestJWTAudienceVerification tests JWT verification with custom audience values
func TestJWTAudienceVerification(t *testing.T) {
// Create cleanup helper
tc := newTestCleanup(t)
// Generate RSA key for signing JWTs
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
rsaPublicKey := &rsaPrivateKey.PublicKey
// Create JWK
jwk := JWK{
Kty: "RSA",
Kid: "test-key-id",
Alg: "RS256",
N: base64.RawURLEncoding.EncodeToString(rsaPublicKey.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}),
}
jwks := &JWKSet{
Keys: []JWK{jwk},
}
mockJWKCache := &MockJWKCache{
JWKS: jwks,
Err: nil,
}
logger := NewLogger("debug")
tokenBlacklist := tc.addCache(NewCache())
tokenCache := tc.addTokenCache(NewTokenCache())
tests := []struct {
tokenAudience interface{}
name string
configAudience string
errContains string
wantErr bool
skipReplayCheck bool
}{
{
name: "JWT with string aud matching configured audience",
configAudience: "https://api.example.com",
tokenAudience: "https://api.example.com",
wantErr: false,
skipReplayCheck: true,
},
{
name: "JWT with array aud containing configured audience",
configAudience: "https://api.example.com",
tokenAudience: []interface{}{"https://other.com", "https://api.example.com", "https://another.com"},
wantErr: false,
skipReplayCheck: true,
},
{
name: "JWT with string aud NOT matching configured audience",
configAudience: "https://api.example.com",
tokenAudience: "https://wrong-api.example.com",
wantErr: true,
errContains: "invalid audience",
skipReplayCheck: true,
},
{
name: "JWT with array aud NOT containing configured audience",
configAudience: "https://api.example.com",
tokenAudience: []interface{}{"https://other.com", "https://another.com"},
wantErr: true,
errContains: "invalid audience",
skipReplayCheck: true,
},
{
name: "JWT with clientID as aud when no custom audience configured",
configAudience: "",
tokenAudience: "test-client-id",
wantErr: false,
skipReplayCheck: true,
},
{
name: "JWT with empty string aud",
configAudience: "https://api.example.com",
tokenAudience: "",
wantErr: true,
errContains: "invalid audience",
skipReplayCheck: true,
},
{
name: "Azure AD Application ID URI format",
configAudience: "api://12345-app-id",
tokenAudience: "api://12345-app-id",
wantErr: false,
skipReplayCheck: true,
},
{
name: "Auth0 custom API audience",
configAudience: "https://mycompany.com/api",
tokenAudience: "https://mycompany.com/api",
wantErr: false,
skipReplayCheck: true,
},
{
name: "Token confusion attack - audience for different service",
configAudience: "https://service-a.example.com",
tokenAudience: "https://service-b.example.com",
wantErr: true,
errContains: "invalid audience",
skipReplayCheck: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create TraefikOidc instance
tOidc := &TraefikOidc{
issuerURL: "https://test-issuer.com",
clientID: "test-client-id",
clientSecret: "test-client-secret",
jwkCache: mockJWKCache,
jwksURL: "https://test-jwks-url.com",
tokenBlacklist: tokenBlacklist,
tokenCache: tokenCache,
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
logger: logger,
httpClient: &http.Client{},
}
// Set up the token verifier and JWT verifier
tOidc.jwtVerifier = tOidc
tOidc.tokenVerifier = tOidc
// Determine the expected audience for validation
expectedAudience := tt.configAudience
if expectedAudience == "" {
expectedAudience = tOidc.clientID
}
// Set the audience field on the tOidc instance
tOidc.audience = expectedAudience
// Create JWT with specified audience
jti := generateRandomString(16)
if tt.skipReplayCheck {
// Use a unique JTI for each test to avoid replay detection
jti = fmt.Sprintf("test-%s-%s", tt.name, jti)
}
jwt, err := createTestJWT(rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": tt.tokenAudience,
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "test-subject",
"email": "user@example.com",
"jti": jti,
})
if err != nil {
t.Fatalf("Failed to create test JWT: %v", err)
}
// Verify the token
err = tOidc.VerifyToken(jwt)
if (err != nil) != tt.wantErr {
t.Errorf("VerifyToken() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil && tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("Error message should contain %q, got: %v", tt.errContains, err)
}
})
}
}
// TestJWTAudienceBackwardCompatibility tests that existing behavior is preserved
// when the Audience field is not set
func TestJWTAudienceBackwardCompatibility(t *testing.T) {
ts := NewTestSuite(t)
ts.Setup()
// Test with no custom audience configured - should use clientID
jwt, err := createTestJWT(ts.rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://test-issuer.com",
"aud": "test-client-id", // Should match clientID
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "test-subject",
"email": "user@example.com",
"jti": generateRandomString(16),
})
if err != nil {
t.Fatalf("Failed to create test JWT: %v", err)
}
err = ts.tOidc.VerifyToken(jwt)
if err != nil {
t.Errorf("Backward compatibility broken: VerifyToken() error = %v, expected nil", err)
}
}
// =============================================================================
// INTEGRATION TESTS - AUTH0
// =============================================================================
// TestAudienceIntegrationAuth0Scenario tests Auth0-specific use case
func TestAudienceIntegrationAuth0Scenario(t *testing.T) {
// Create cleanup helper
tc := newTestCleanup(t)
// Simulate Auth0 scenario: custom audience for API access
config := CreateConfig()
config.ProviderURL = "https://mycompany.auth0.com"
config.ClientID = "auth0-client-id"
config.ClientSecret = "auth0-client-secret"
config.CallbackURL = "/callback"
config.SessionEncryptionKey = strings.Repeat("a", MinSessionEncryptionKeyLength)
config.Audience = "https://api.mycompany.com" // Custom API audience
// Validate config
if err := config.Validate(); err != nil {
t.Fatalf("Auth0 config validation failed: %v", err)
}
// Generate test keys
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
rsaPublicKey := &rsaPrivateKey.PublicKey
jwk := JWK{
Kty: "RSA",
Kid: "auth0-key-id",
Alg: "RS256",
N: base64.RawURLEncoding.EncodeToString(rsaPublicKey.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}),
}
jwks := &JWKSet{
Keys: []JWK{jwk},
}
mockJWKCache := &MockJWKCache{
JWKS: jwks,
Err: nil,
}
logger := NewLogger("debug")
tokenBlacklist := tc.addCache(NewCache())
tokenCache := tc.addTokenCache(NewTokenCache())
tOidc := &TraefikOidc{
issuerURL: config.ProviderURL,
clientID: config.ClientID,
clientSecret: config.ClientSecret,
audience: config.Audience, // Set audience from config
jwkCache: mockJWKCache,
jwksURL: "https://mycompany.auth0.com/.well-known/jwks.json",
tokenBlacklist: tokenBlacklist,
tokenCache: tokenCache,
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
logger: logger,
httpClient: &http.Client{},
}
// Default audience to clientID if not specified
if tOidc.audience == "" {
tOidc.audience = tOidc.clientID
}
tOidc.jwtVerifier = tOidc
tOidc.tokenVerifier = tOidc
t.Run("Valid Auth0 API access token with custom audience", func(t *testing.T) {
jwt, err := createTestJWT(rsaPrivateKey, "RS256", "auth0-key-id", map[string]interface{}{
"iss": config.ProviderURL,
"aud": config.Audience, // Matches configured audience
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "auth0|123456",
"email": "user@example.com",
"jti": generateRandomString(16),
})
if err != nil {
t.Fatalf("Failed to create Auth0 JWT: %v", err)
}
err = tOidc.VerifyToken(jwt)
if err != nil {
t.Errorf("Auth0 token verification failed: %v", err)
}
})
t.Run("Auth0 ACCESS token with clientID instead of API audience should fail", func(t *testing.T) {
jwt, err := createTestJWT(rsaPrivateKey, "RS256", "auth0-key-id", map[string]interface{}{
"iss": config.ProviderURL,
"aud": config.ClientID, // Using clientID instead of API audience
"scope": "openid profile email", // Mark as access token
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "auth0|123456",
"email": "user@example.com",
"jti": generateRandomString(16),
})
if err != nil {
t.Fatalf("Failed to create Auth0 JWT: %v", err)
}
err = tOidc.VerifyToken(jwt)
if err == nil {
t.Error("Auth0 access token with wrong audience should have been rejected")
} else if !strings.Contains(err.Error(), "invalid audience") {
t.Errorf("Expected 'invalid audience' error, got: %v", err)
}
})
}
// =============================================================================
// INTEGRATION TESTS - AZURE AD
// =============================================================================
// TestAudienceIntegrationAzureADScenario tests Azure AD-specific use case
func TestAudienceIntegrationAzureADScenario(t *testing.T) {
// Create cleanup helper
tc := newTestCleanup(t)
// Simulate Azure AD scenario: Application ID URI format
config := CreateConfig()
config.ProviderURL = "https://login.microsoftonline.com/tenant-id/v2.0"
config.ClientID = "azure-client-id"
config.ClientSecret = "azure-client-secret"
config.CallbackURL = "/callback"
config.SessionEncryptionKey = strings.Repeat("a", MinSessionEncryptionKeyLength)
config.Audience = "api://12345-abcd-6789-efgh" // Azure AD Application ID URI
// Validate config
if err := config.Validate(); err != nil {
t.Fatalf("Azure AD config validation failed: %v", err)
}
// Generate test keys
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
rsaPublicKey := &rsaPrivateKey.PublicKey
jwk := JWK{
Kty: "RSA",
Kid: "azure-key-id",
Alg: "RS256",
N: base64.RawURLEncoding.EncodeToString(rsaPublicKey.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}),
}
jwks := &JWKSet{
Keys: []JWK{jwk},
}
mockJWKCache := &MockJWKCache{
JWKS: jwks,
Err: nil,
}
logger := NewLogger("debug")
tokenBlacklist := tc.addCache(NewCache())
tokenCache := tc.addTokenCache(NewTokenCache())
tOidc := &TraefikOidc{
issuerURL: config.ProviderURL,
clientID: config.ClientID,
clientSecret: config.ClientSecret,
audience: config.Audience, // Set audience from config
jwkCache: mockJWKCache,
jwksURL: config.ProviderURL + "/.well-known/jwks.json",
tokenBlacklist: tokenBlacklist,
tokenCache: tokenCache,
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
logger: logger,
httpClient: &http.Client{},
}
// Default audience to clientID if not specified
if tOidc.audience == "" {
tOidc.audience = tOidc.clientID
}
tOidc.jwtVerifier = tOidc
tOidc.tokenVerifier = tOidc
t.Run("Valid Azure AD token with Application ID URI audience", func(t *testing.T) {
jwt, err := createTestJWT(rsaPrivateKey, "RS256", "azure-key-id", map[string]interface{}{
"iss": config.ProviderURL,
"aud": config.Audience, // Matches Application ID URI
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "azure-user-id",
"email": "user@example.com",
"oid": "object-id-12345",
"tid": "tenant-id",
"jti": generateRandomString(16),
})
if err != nil {
t.Fatalf("Failed to create Azure AD JWT: %v", err)
}
err = tOidc.VerifyToken(jwt)
if err != nil {
t.Errorf("Azure AD token verification failed: %v", err)
}
})
t.Run("Azure AD token with multiple audiences including correct one", func(t *testing.T) {
jwt, err := createTestJWT(rsaPrivateKey, "RS256", "azure-key-id", map[string]interface{}{
"iss": config.ProviderURL,
"aud": []interface{}{config.ClientID, config.Audience, "https://graph.microsoft.com"},
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "azure-user-id",
"email": "user@example.com",
"oid": "object-id-12345",
"tid": "tenant-id",
"jti": generateRandomString(16),
})
if err != nil {
t.Fatalf("Failed to create Azure AD JWT: %v", err)
}
err = tOidc.VerifyToken(jwt)
if err != nil {
t.Errorf("Azure AD token with multiple audiences verification failed: %v", err)
}
})
}
// =============================================================================
// SECURITY TESTS
// =============================================================================
// TestAudienceSecurityTokenConfusionAttack tests security against token confusion attacks
func TestAudienceSecurityTokenConfusionAttack(t *testing.T) {
// Create cleanup helper
tc := newTestCleanup(t)
// Generate test keys
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
rsaPublicKey := &rsaPrivateKey.PublicKey
jwk := JWK{
Kty: "RSA",
Kid: "test-key-id",
Alg: "RS256",
N: base64.RawURLEncoding.EncodeToString(rsaPublicKey.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}),
}
jwks := &JWKSet{
Keys: []JWK{jwk},
}
mockJWKCache := &MockJWKCache{
JWKS: jwks,
Err: nil,
}
logger := NewLogger("debug")
tokenBlacklist := tc.addCache(NewCache())
tokenCache := tc.addTokenCache(NewTokenCache())
// Service A configuration
serviceA := &TraefikOidc{
issuerURL: "https://auth.example.com",
clientID: "service-a-client-id",
clientSecret: "service-a-secret",
audience: "service-a-client-id", // Service A uses its clientID as audience
jwkCache: mockJWKCache,
jwksURL: "https://auth.example.com/.well-known/jwks.json",
tokenBlacklist: tokenBlacklist,
tokenCache: tokenCache,
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
logger: logger,
httpClient: &http.Client{},
}
serviceA.jwtVerifier = serviceA
serviceA.tokenVerifier = serviceA
t.Run("Token confusion - Try to use service B token on service A", func(t *testing.T) {
// Create a token intended for service B
serviceBToken, err := createTestJWT(rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://auth.example.com",
"aud": "https://service-b.example.com", // For service B
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "attacker@example.com",
"email": "attacker@example.com",
"jti": generateRandomString(16),
})
if err != nil {
t.Fatalf("Failed to create service B token: %v", err)
}
// Try to verify the service B token on service A
err = serviceA.VerifyToken(serviceBToken)
switch {
case err == nil:
t.Error("SECURITY VULNERABILITY: Token confusion attack succeeded - service B token was accepted by service A")
case !strings.Contains(err.Error(), "invalid audience"):
t.Errorf("Expected 'invalid audience' error for token confusion, got: %v", err)
default:
t.Logf("Token confusion attack correctly prevented: %v", err)
}
})
}
// TestAudienceSecurityWildcardInjection tests that wildcards are rejected
func TestAudienceSecurityWildcardInjection(t *testing.T) {
tests := []struct {
name string
audience string
}{
{
name: "Single asterisk",
audience: "*",
},
{
name: "Wildcard in URL",
audience: "https://*.example.com",
},
{
name: "Wildcard in path",
audience: "https://api.example.com/*",
},
{
name: "Multiple wildcards",
audience: "https://*.*.example.com",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := CreateConfig()
config.ProviderURL = "https://provider.example.com"
config.ClientID = "test-client-id"
config.ClientSecret = "test-client-secret"
config.CallbackURL = "/callback"
config.SessionEncryptionKey = strings.Repeat("a", MinSessionEncryptionKeyLength)
config.Audience = tt.audience
err := config.Validate()
if err == nil {
t.Errorf("SECURITY VULNERABILITY: Wildcard audience %q was not rejected", tt.audience)
} else if !strings.Contains(err.Error(), "must not contain wildcards") {
t.Errorf("Expected 'must not contain wildcards' error, got: %v", err)
}
})
}
}
// TestAudienceSecurityInjectionAttempts tests various injection attempts
func TestAudienceSecurityInjectionAttempts(t *testing.T) {
tests := []struct {
name string
audience string
errContains string
}{
{
name: "Newline injection",
audience: "api.example.com\nmalicious.com",
errContains: "contains invalid characters",
},
{
name: "Carriage return injection",
audience: "api.example.com\rmalicious.com",
errContains: "contains invalid characters",
},
{
name: "Tab injection",
audience: "api.example.com\tmalicious.com",
errContains: "contains invalid characters",
},
{
name: "Null byte injection",
audience: "api.example.com\x00malicious.com",
errContains: "contains invalid characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := CreateConfig()
config.ProviderURL = "https://provider.example.com"
config.ClientID = "test-client-id"
config.ClientSecret = "test-client-secret"
config.CallbackURL = "/callback"
config.SessionEncryptionKey = strings.Repeat("a", MinSessionEncryptionKeyLength)
config.Audience = tt.audience
err := config.Validate()
if err == nil {
t.Errorf("SECURITY VULNERABILITY: Injection attempt with %q was not rejected", tt.name)
} else if !strings.Contains(err.Error(), tt.errContains) {
t.Errorf("Expected error containing %q, got: %v", tt.errContains, err)
}
})
}
}
// TestAudienceWithReplayProtection tests that replay protection works correctly with custom audiences
func TestAudienceWithReplayProtection(t *testing.T) {
// Create cleanup helper
tc := newTestCleanup(t)
// Generate test keys
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
rsaPublicKey := &rsaPrivateKey.PublicKey
jwk := JWK{
Kty: "RSA",
Kid: "test-key-id",
Alg: "RS256",
N: base64.RawURLEncoding.EncodeToString(rsaPublicKey.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}),
}
jwks := &JWKSet{
Keys: []JWK{jwk},
}
mockJWKCache := &MockJWKCache{
JWKS: jwks,
Err: nil,
}
logger := NewLogger("debug")
tokenBlacklist := tc.addCache(NewCache())
tokenCache := tc.addTokenCache(NewTokenCache())
tOidc := &TraefikOidc{
issuerURL: "https://auth.example.com",
clientID: "test-client-id",
clientSecret: "test-client-secret",
jwkCache: mockJWKCache,
jwksURL: "https://auth.example.com/.well-known/jwks.json",
tokenBlacklist: tokenBlacklist,
tokenCache: tokenCache,
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
logger: logger,
httpClient: &http.Client{},
}
tOidc.jwtVerifier = tOidc
tOidc.tokenVerifier = tOidc
// Create a token with custom audience and fixed JTI
fixedJTI := "replay-test-jti-" + generateRandomString(8)
customAudience := "https://api.example.com"
// Set the audience field to match what we expect
tOidc.audience = customAudience
jwt, err := createTestJWT(rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://auth.example.com",
"aud": customAudience,
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "test-user",
"email": "user@example.com",
"jti": fixedJTI,
})
if err != nil {
t.Fatalf("Failed to create JWT: %v", err)
}
// First verification should succeed
err = tOidc.VerifyToken(jwt)
if err != nil {
t.Fatalf("First verification failed: %v", err)
}
// Verify that the JTI was blacklisted
if blacklisted, exists := tOidc.tokenBlacklist.Get(fixedJTI); !exists || blacklisted == nil {
t.Logf("Note: JTI was not added to blacklist (may be due to test token prefix)")
} else {
t.Logf("Replay protection verified: JTI %s is correctly blacklisted", fixedJTI)
}
}
// =============================================================================
// END-TO-END TESTS
// =============================================================================
// TestAudienceEndToEndScenario tests a complete end-to-end scenario with middleware
func TestAudienceEndToEndScenario(t *testing.T) {
// Create cleanup helper
tc := newTestCleanup(t)
// Create a test next handler
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Authenticated with custom audience"))
})
// Generate test keys
rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("Failed to generate RSA key: %v", err)
}
rsaPublicKey := &rsaPrivateKey.PublicKey
jwk := JWK{
Kty: "RSA",
Kid: "test-key-id",
Alg: "RS256",
N: base64.RawURLEncoding.EncodeToString(rsaPublicKey.N.Bytes()),
E: base64.RawURLEncoding.EncodeToString([]byte{1, 0, 1}),
}
jwks := &JWKSet{
Keys: []JWK{jwk},
}
mockJWKCache := &MockJWKCache{
JWKS: jwks,
Err: nil,
}
logger := NewLogger("debug")
sm, err := NewSessionManager(strings.Repeat("a", MinSessionEncryptionKeyLength), false, "", "", 0, logger)
if err != nil {
t.Fatalf("Failed to create session manager: %v", err)
}
tokenBlacklist := tc.addCache(NewCache())
tokenCache := tc.addTokenCache(NewTokenCache())
customAudience := "https://api.company.com"
tOidc := &TraefikOidc{
next: nextHandler,
name: "test",
redirURLPath: "/callback",
logoutURLPath: "/callback/logout",
issuerURL: "https://auth.company.com",
clientID: "test-client-id",
clientSecret: "test-client-secret",
audience: customAudience, // Set custom audience
jwkCache: mockJWKCache,
jwksURL: "https://auth.company.com/.well-known/jwks.json",
tokenBlacklist: tokenBlacklist,
tokenCache: tokenCache,
limiter: rate.NewLimiter(rate.Every(time.Second), 10),
logger: logger,
allowedUserDomains: map[string]struct{}{"company.com": {}},
userIdentifierClaim: "email", // Required for user identification
excludedURLs: map[string]struct{}{},
httpClient: &http.Client{},
initComplete: make(chan struct{}),
sessionManager: sm,
extractClaimsFunc: extractClaims,
}
tOidc.jwtVerifier = tOidc
tOidc.tokenVerifier = tOidc
close(tOidc.initComplete)
t.Run("End-to-end with correct custom audience", func(t *testing.T) {
// Create a valid token with the custom audience
validJWT, err := createTestJWT(rsaPrivateKey, "RS256", "test-key-id", map[string]interface{}{
"iss": "https://auth.company.com",
"aud": customAudience,
"exp": float64(time.Now().Add(1 * time.Hour).Unix()),
"iat": float64(time.Now().Add(-2 * time.Minute).Unix()),
"sub": "user-123",
"email": "user@company.com",
"jti": generateRandomString(16),
})
if err != nil {
t.Fatalf("Failed to create valid JWT: %v", err)
}
// Create a request with authenticated session
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "company.com")
// Create session with token
resp := httptest.NewRecorder()
session, err := sm.GetSession(req)
if err != nil {
t.Fatalf("Failed to get session: %v", err)
}
if err := session.SetAuthenticated(true); err != nil {
t.Fatalf("Failed to set authenticated: %v", err)
}
session.SetUserIdentifier("user@company.com")
session.SetIDToken(validJWT)
session.SetAccessToken(validJWT)
if err := session.Save(req, resp); err != nil {
t.Fatalf("Failed to save session: %v", err)
}
// Get cookies and add them to a new request
cookies := resp.Result().Cookies()
req = httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("X-Forwarded-Proto", "https")
req.Header.Set("X-Forwarded-Host", "company.com")
for _, cookie := range cookies {
req.AddCookie(cookie)
}
resp = httptest.NewRecorder()
tOidc.ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d. Body: %s", resp.Code, resp.Body.String())
}
})
}