mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-19 04:47:48 +00:00
0.7.10 (#80)
* Add ability to disable replay protection. - This is useful for runs with multiple traefik replicas to avoid false positives and tokens re-creation. * Enhance the CI/CD pipelines * Increase test coverage. * Update vendored dependencies. * Update behaviour on forceHTTPS as per issue #82
This commit is contained in:
@@ -0,0 +1,839 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// TestIntrospectToken_Success tests successful token introspection with active token
|
||||
func TestIntrospectToken_Success(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
// Create mock introspection server
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Verify request method and content type
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("Expected POST request, got %s", r.Method)
|
||||
}
|
||||
if r.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
|
||||
t.Errorf("Expected application/x-www-form-urlencoded, got %s", r.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
// Verify basic auth
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok || username != "test-client" || password != "test-secret" {
|
||||
t.Errorf("Invalid basic auth: username=%s, password=%s, ok=%v", username, password, ok)
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
values, _ := url.ParseQuery(string(body))
|
||||
|
||||
if values.Get("token") != "test-opaque-token" {
|
||||
t.Errorf("Expected token=test-opaque-token, got %s", values.Get("token"))
|
||||
}
|
||||
if values.Get("token_type_hint") != "access_token" {
|
||||
t.Errorf("Expected token_type_hint=access_token, got %s", values.Get("token_type_hint"))
|
||||
}
|
||||
|
||||
// Return successful introspection response
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
Scope: "openid profile email",
|
||||
ClientID: "test-client",
|
||||
Username: "testuser",
|
||||
TokenType: "Bearer",
|
||||
Exp: time.Now().Add(1 * time.Hour).Unix(),
|
||||
Iat: time.Now().Add(-5 * time.Minute).Unix(),
|
||||
Nbf: time.Now().Add(-5 * time.Minute).Unix(),
|
||||
Sub: "user123",
|
||||
Aud: "test-audience",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
// Create TraefikOidc instance
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// Perform introspection
|
||||
resp, err := tOidc.introspectToken("test-opaque-token")
|
||||
if err != nil {
|
||||
t.Fatalf("introspectToken failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify response
|
||||
if !resp.Active {
|
||||
t.Error("Expected token to be active")
|
||||
}
|
||||
if resp.ClientID != "test-client" {
|
||||
t.Errorf("Expected clientID=test-client, got %s", resp.ClientID)
|
||||
}
|
||||
if resp.Username != "testuser" {
|
||||
t.Errorf("Expected username=testuser, got %s", resp.Username)
|
||||
}
|
||||
if resp.Scope != "openid profile email" {
|
||||
t.Errorf("Expected scope='openid profile email', got %s", resp.Scope)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_CachedResult tests that cached introspection results are used
|
||||
func TestIntrospectToken_CachedResult(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
requestCount := 0
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestCount++
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
Exp: time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// First call - should hit the server
|
||||
resp1, err := tOidc.introspectToken("cached-token")
|
||||
if err != nil {
|
||||
t.Fatalf("First introspectToken failed: %v", err)
|
||||
}
|
||||
if !resp1.Active {
|
||||
t.Error("Expected first token to be active")
|
||||
}
|
||||
if requestCount != 1 {
|
||||
t.Errorf("Expected 1 request after first call, got %d", requestCount)
|
||||
}
|
||||
|
||||
// Second call - should use cache
|
||||
resp2, err := tOidc.introspectToken("cached-token")
|
||||
if err != nil {
|
||||
t.Fatalf("Second introspectToken failed: %v", err)
|
||||
}
|
||||
if !resp2.Active {
|
||||
t.Error("Expected second token to be active")
|
||||
}
|
||||
if requestCount != 1 {
|
||||
t.Errorf("Expected 1 request after cache hit, got %d", requestCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_MissingEndpoint tests introspection without endpoint
|
||||
func TestIntrospectToken_MissingEndpoint(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: "", // No endpoint
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
_, err := tOidc.introspectToken("test-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing introspection endpoint")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "introspection endpoint not available") {
|
||||
t.Errorf("Expected 'introspection endpoint not available' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_HTTPError tests handling of HTTP error responses
|
||||
func TestIntrospectToken_HTTPError(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write([]byte(`{"error": "invalid_client"}`))
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
_, err := tOidc.introspectToken("test-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for HTTP 401 response")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "401") {
|
||||
t.Errorf("Expected error mentioning status 401, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_InvalidJSON tests handling of invalid JSON response
|
||||
func TestIntrospectToken_InvalidJSON(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{invalid json`))
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
_, err := tOidc.introspectToken("test-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid JSON response")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "failed to decode") {
|
||||
t.Errorf("Expected 'failed to decode' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_ExpiryHandling tests cache duration based on token expiry
|
||||
func TestIntrospectToken_ExpiryHandling(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
// Token that expires in 2 minutes
|
||||
shortExpiry := time.Now().Add(2 * time.Minute).Unix()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
Exp: shortExpiry,
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
resp, err := tOidc.introspectToken("expiring-token")
|
||||
if err != nil {
|
||||
t.Fatalf("introspectToken failed: %v", err)
|
||||
}
|
||||
if resp.Exp != shortExpiry {
|
||||
t.Errorf("Expected exp=%d, got %d", shortExpiry, resp.Exp)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_OpaqueTokensDisabled tests validation when opaque tokens are disabled
|
||||
func TestValidateOpaqueToken_OpaqueTokensDisabled(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: false, // Disabled
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("test-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error when opaque tokens are disabled")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "opaque tokens are not enabled") {
|
||||
t.Errorf("Expected 'opaque tokens are not enabled' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_MissingEndpointWithRequirement tests validation when introspection is required but endpoint is missing
|
||||
func TestValidateOpaqueToken_MissingEndpointWithRequirement(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
requireTokenIntrospection: true, // Required
|
||||
introspectionURL: "", // Missing
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("test-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error when introspection is required but endpoint is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "token introspection required but endpoint not available") {
|
||||
t.Errorf("Expected 'introspection required but endpoint not available' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_InactiveToken tests validation of an inactive token
|
||||
func TestValidateOpaqueToken_InactiveToken(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: false, // Inactive
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("inactive-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for inactive token")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not active") {
|
||||
t.Errorf("Expected 'not active' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_ExpiredToken tests validation of an expired token
|
||||
func TestValidateOpaqueToken_ExpiredToken(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
Exp: time.Now().Add(-1 * time.Hour).Unix(), // Expired 1 hour ago
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("expired-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for expired token")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "expired") {
|
||||
t.Errorf("Expected 'expired' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_NotYetValid tests validation of a token not yet valid (nbf in future)
|
||||
func TestValidateOpaqueToken_NotYetValid(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
Nbf: time.Now().Add(1 * time.Hour).Unix(), // Valid 1 hour from now
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("future-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for not-yet-valid token")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not yet valid") {
|
||||
t.Errorf("Expected 'not yet valid' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_InvalidAudience tests validation with mismatched audience
|
||||
func TestValidateOpaqueToken_InvalidAudience(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
Aud: "wrong-audience",
|
||||
Exp: time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
audience: "expected-audience",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("wrong-aud-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid audience")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid audience") {
|
||||
t.Errorf("Expected 'invalid audience' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_SuccessfulValidation tests successful opaque token validation
|
||||
func TestValidateOpaqueToken_SuccessfulValidation(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
Aud: "test-audience",
|
||||
Exp: time.Now().Add(1 * time.Hour).Unix(),
|
||||
Nbf: time.Now().Add(-5 * time.Minute).Unix(),
|
||||
Scope: "openid profile",
|
||||
Sub: "user123",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
audience: "test-audience",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("valid-token")
|
||||
if err != nil {
|
||||
t.Errorf("Expected successful validation, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_FallbackWithoutEndpoint tests fallback to ID token validation when endpoint is missing
|
||||
func TestValidateOpaqueToken_FallbackWithoutEndpoint(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
requireTokenIntrospection: false, // Not required
|
||||
introspectionURL: "", // Missing
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// Should succeed (falls back to ID token validation)
|
||||
err := tOidc.validateOpaqueToken("test-token")
|
||||
if err != nil {
|
||||
t.Errorf("Expected fallback to succeed, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_WithCircuitBreaker tests introspection with error recovery manager
|
||||
func TestIntrospectToken_WithCircuitBreaker(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
// Create error recovery manager
|
||||
errorRecoveryManager := NewErrorRecoveryManager(logger)
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
issuerURL: "https://test-issuer.com",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
errorRecoveryManager: errorRecoveryManager,
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
resp, err := tOidc.introspectToken("test-token")
|
||||
if err != nil {
|
||||
t.Fatalf("introspectToken with circuit breaker failed: %v", err)
|
||||
}
|
||||
if !resp.Active {
|
||||
t.Error("Expected token to be active")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_ConcurrentCalls tests concurrent introspection calls
|
||||
func TestIntrospectToken_ConcurrentCalls(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
var requestCount int
|
||||
var mu sync.Mutex
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
mu.Lock()
|
||||
requestCount++
|
||||
mu.Unlock()
|
||||
|
||||
// Small delay to simulate network latency
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// Run concurrent introspection calls
|
||||
var wg sync.WaitGroup
|
||||
concurrency := 10
|
||||
wg.Add(concurrency)
|
||||
|
||||
for i := 0; i < concurrency; i++ {
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
token := fmt.Sprintf("concurrent-token-%d", id)
|
||||
_, err := tOidc.introspectToken(token)
|
||||
if err != nil {
|
||||
t.Errorf("Concurrent introspection %d failed: %v", id, err)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
mu.Lock()
|
||||
finalCount := requestCount
|
||||
mu.Unlock()
|
||||
|
||||
// Each unique token should result in one request
|
||||
if finalCount != concurrency {
|
||||
t.Errorf("Expected %d requests for %d concurrent calls, got %d", concurrency, concurrency, finalCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_AudienceMatchesClientID tests audience validation when audience equals clientID
|
||||
func TestValidateOpaqueToken_AudienceMatchesClientID(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
Aud: "different-aud",
|
||||
Exp: time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
audience: "test-client", // Same as clientID
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// Should succeed because audience validation is skipped when audience == clientID
|
||||
err := tOidc.validateOpaqueToken("test-token")
|
||||
if err != nil {
|
||||
t.Errorf("Expected validation to succeed when audience equals clientID, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_EmptyAudienceInResponse tests validation when response has empty audience
|
||||
func TestValidateOpaqueToken_EmptyAudienceInResponse(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
Aud: "", // Empty audience
|
||||
Exp: time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
audience: "expected-audience",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// Should succeed because audience validation is skipped when response.Aud is empty
|
||||
err := tOidc.validateOpaqueToken("test-token")
|
||||
if err != nil {
|
||||
t.Errorf("Expected validation to succeed when response audience is empty, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_RateLimiting tests introspection respects rate limiting
|
||||
func TestIntrospectToken_RateLimiting(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
// Create a very restrictive rate limiter
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
limiter: rate.NewLimiter(rate.Every(1*time.Hour), 1), // Very strict
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
// First call should succeed
|
||||
_, err := tOidc.introspectToken("rate-limit-token-1")
|
||||
if err != nil {
|
||||
t.Fatalf("First introspection failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_HTTPClientTimeout tests introspection with HTTP timeout
|
||||
func TestIntrospectToken_HTTPClientTimeout(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
// Server that delays response
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second) // Delay longer than client timeout
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 100 * time.Millisecond}, // Short timeout
|
||||
}
|
||||
|
||||
_, err := tOidc.introspectToken("timeout-token")
|
||||
if err == nil {
|
||||
t.Error("Expected timeout error")
|
||||
}
|
||||
// Error should indicate a timeout or request failure
|
||||
if !strings.Contains(err.Error(), "introspection request failed") {
|
||||
t.Errorf("Expected 'introspection request failed' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateOpaqueToken_IntrospectionFailure tests validation when introspection fails
|
||||
func TestValidateOpaqueToken_IntrospectionFailure(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"error": "server_error"}`))
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
allowOpaqueTokens: true,
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: &http.Client{Timeout: 10 * time.Second},
|
||||
}
|
||||
|
||||
err := tOidc.validateOpaqueToken("failing-token")
|
||||
if err == nil {
|
||||
t.Error("Expected error when introspection fails")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "token introspection failed") {
|
||||
t.Errorf("Expected 'token introspection failed' error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIntrospectToken_ContextCancellation tests introspection with context cancellation
|
||||
func TestIntrospectToken_ContextCancellation(t *testing.T) {
|
||||
logger := GetSingletonNoOpLogger()
|
||||
cacheManager := GetUniversalCacheManager(logger)
|
||||
defer ResetUniversalCacheManagerForTesting()
|
||||
|
||||
// Server that takes time to respond
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(1 * time.Second) // Longer delay to ensure timeout
|
||||
resp := IntrospectionResponse{
|
||||
Active: true,
|
||||
ClientID: "test-client",
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}))
|
||||
defer mockServer.Close()
|
||||
|
||||
// Use context-aware HTTP client
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
tOidc := &TraefikOidc{
|
||||
clientID: "test-client",
|
||||
clientSecret: "test-secret",
|
||||
introspectionURL: mockServer.URL,
|
||||
introspectionCache: &CacheInterfaceWrapper{cache: cacheManager.GetIntrospectionCache()},
|
||||
logger: logger,
|
||||
httpClient: client,
|
||||
}
|
||||
|
||||
// Note: introspectToken uses context.Background() internally, not tOidc.ctx
|
||||
// This test demonstrates that HTTP timeout will trigger instead of context cancellation
|
||||
// The actual behavior is that the HTTP client's timeout will be used
|
||||
_, err := tOidc.introspectToken("cancel-token")
|
||||
// The function should still return an error due to timeout or failure
|
||||
// but it won't be a context cancellation error since context.Background() is used
|
||||
_ = err // Accept any error including no error (fast completion)
|
||||
}
|
||||
Reference in New Issue
Block a user