mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-15 04:18:03 +00:00
Add redis support for distributed caching (#83)
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
5fcbd54955
commit
e64fc7f730
@@ -0,0 +1,684 @@
|
||||
//go:build !yaegi
|
||||
|
||||
package token
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Mock implementations for validator tests
|
||||
type mockTokenCache struct {
|
||||
data map[string]map[string]interface{}
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newMockTokenCache() *mockTokenCache {
|
||||
return &mockTokenCache{
|
||||
data: make(map[string]map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockTokenCache) CacheToken(token string, claims map[string]interface{}) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.data[token] = claims
|
||||
}
|
||||
|
||||
func (m *mockTokenCache) GetCachedToken(token string) (map[string]interface{}, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
claims, exists := m.data[token]
|
||||
return claims, exists
|
||||
}
|
||||
|
||||
func (m *mockTokenCache) InvalidateToken(token string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.data, token)
|
||||
}
|
||||
|
||||
func (m *mockTokenCache) StartCleanup(interval time.Duration) {
|
||||
// No-op for tests
|
||||
}
|
||||
|
||||
func (m *mockTokenCache) StopCleanup() {
|
||||
// No-op for tests
|
||||
}
|
||||
|
||||
// Validator tests
|
||||
func TestNewValidator(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
Audience: "test-audience",
|
||||
IssuerURL: "https://issuer.example.com",
|
||||
JwksURL: "https://issuer.example.com/jwks",
|
||||
TokenCache: newMockTokenCache(),
|
||||
TokenBlacklist: newMockCache(),
|
||||
TokenTypeCache: newMockCache(),
|
||||
HTTPClient: &http.Client{},
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
if validator == nil {
|
||||
t.Fatal("Expected NewValidator to return non-nil")
|
||||
}
|
||||
|
||||
if validator.clientID != "test-client" {
|
||||
t.Error("Expected clientID to be set")
|
||||
}
|
||||
|
||||
if validator.audience != "test-audience" {
|
||||
t.Error("Expected audience to be set")
|
||||
}
|
||||
|
||||
if validator.issuerURL != "https://issuer.example.com" {
|
||||
t.Error("Expected issuerURL to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewValidator_NilMetadataMu(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
// MetadataMu is nil
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
if validator.metadataMu != nil {
|
||||
t.Error("Expected metadataMu to be nil when not provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_VerifyToken_EmptyToken(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenCache: newMockTokenCache(),
|
||||
TokenBlacklist: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
err := validator.VerifyToken("")
|
||||
if err == nil {
|
||||
t.Error("Expected error for empty token")
|
||||
}
|
||||
|
||||
if err.Error() != "invalid JWT format: token is empty" {
|
||||
t.Errorf("Expected empty token error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_VerifyToken_InvalidFormat(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenCache: newMockTokenCache(),
|
||||
TokenBlacklist: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
// Token with only 2 parts (missing 3rd part)
|
||||
err := validator.VerifyToken("header.payload")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid token format")
|
||||
}
|
||||
|
||||
// Token with too many parts
|
||||
err = validator.VerifyToken("part1.part2.part3.part4")
|
||||
if err == nil {
|
||||
t.Error("Expected error for token with too many parts")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_VerifyToken_TooShort(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenCache: newMockTokenCache(),
|
||||
TokenBlacklist: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
err := validator.VerifyToken("ab.cd.ef")
|
||||
if err == nil {
|
||||
t.Error("Expected error for too short token")
|
||||
}
|
||||
|
||||
if err.Error() != "token too short to be valid JWT" {
|
||||
t.Errorf("Expected too short error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_DetermineTokenType(t *testing.T) {
|
||||
// Test ID token
|
||||
configID := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
validatorID := NewValidator(configID)
|
||||
|
||||
jwtID := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"nonce": "test-nonce",
|
||||
},
|
||||
}
|
||||
|
||||
tokenType := validatorID.determineTokenType(jwtID)
|
||||
if tokenType != TokenTypeID {
|
||||
t.Errorf("Expected ID token type, got: %s", tokenType)
|
||||
}
|
||||
|
||||
// Test access token with separate validator to avoid cache interference
|
||||
configAccess := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
validatorAccess := NewValidator(configAccess)
|
||||
|
||||
jwtAccess := &JWT{
|
||||
Header: map[string]interface{}{
|
||||
"typ": "at+jwt",
|
||||
},
|
||||
Claims: map[string]interface{}{},
|
||||
}
|
||||
|
||||
tokenType = validatorAccess.determineTokenType(jwtAccess)
|
||||
if tokenType != TokenTypeAccess {
|
||||
t.Errorf("Expected access token type, got: %s", tokenType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_DetectTokenType_Nonce(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"nonce": "test-nonce-123",
|
||||
},
|
||||
}
|
||||
|
||||
isIDToken := validator.detectTokenType(jwt, "test-token")
|
||||
if !isIDToken {
|
||||
t.Error("Expected nonce to indicate ID token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_DetectTokenType_AtJwt(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Header: map[string]interface{}{
|
||||
"typ": "at+jwt",
|
||||
},
|
||||
Claims: map[string]interface{}{},
|
||||
}
|
||||
|
||||
isIDToken := validator.detectTokenType(jwt, "test-token")
|
||||
if isIDToken {
|
||||
t.Error("Expected at+jwt type to indicate access token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_DetectTokenType_TokenUse(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
// ID token
|
||||
jwtID := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"token_use": "id",
|
||||
},
|
||||
}
|
||||
|
||||
if !validator.detectTokenType(jwtID, "test-token-id") {
|
||||
t.Error("Expected token_use=id to indicate ID token")
|
||||
}
|
||||
|
||||
// Access token
|
||||
jwtAccess := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"token_use": "access",
|
||||
},
|
||||
}
|
||||
|
||||
if validator.detectTokenType(jwtAccess, "test-token-access") {
|
||||
t.Error("Expected token_use=access to indicate access token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_DetectTokenType_Scope(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"scope": "openid profile email",
|
||||
},
|
||||
}
|
||||
|
||||
isIDToken := validator.detectTokenType(jwt, "test-token")
|
||||
if isIDToken {
|
||||
t.Error("Expected scope claim to indicate access token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_DetectTokenType_AudienceMatching(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client-id",
|
||||
TokenTypeCache: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
// Single audience matching client ID
|
||||
jwtSingleAud := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"aud": "test-client-id",
|
||||
},
|
||||
}
|
||||
|
||||
if !validator.detectTokenType(jwtSingleAud, "test-token-1") {
|
||||
t.Error("Expected matching audience to indicate ID token")
|
||||
}
|
||||
|
||||
// Array audience with matching client ID
|
||||
jwtArrayAud := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"aud": []interface{}{"test-client-id"},
|
||||
},
|
||||
}
|
||||
|
||||
if !validator.detectTokenType(jwtArrayAud, "test-token-2") {
|
||||
t.Error("Expected matching audience array to indicate ID token")
|
||||
}
|
||||
|
||||
// Non-matching audience
|
||||
jwtNoMatch := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"aud": "different-audience",
|
||||
},
|
||||
}
|
||||
|
||||
if validator.detectTokenType(jwtNoMatch, "test-token-3") {
|
||||
t.Error("Expected non-matching audience to indicate access token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_DetectTokenType_Caching(t *testing.T) {
|
||||
cache := newMockCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: cache,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test"
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"nonce": "test",
|
||||
},
|
||||
}
|
||||
|
||||
// First call - should cache
|
||||
isIDToken := validator.detectTokenType(jwt, token)
|
||||
if !isIDToken {
|
||||
t.Error("Expected ID token")
|
||||
}
|
||||
|
||||
// Verify cache was populated
|
||||
cacheKey := token[:32]
|
||||
cached, exists := cache.Get(cacheKey)
|
||||
if !exists {
|
||||
t.Error("Expected token type to be cached")
|
||||
}
|
||||
|
||||
if isID, ok := cached["is_id_token"].(bool); !ok || !isID {
|
||||
t.Error("Expected cached value to be true for ID token")
|
||||
}
|
||||
|
||||
// Modify JWT but use cached value
|
||||
jwt.Claims = map[string]interface{}{
|
||||
"scope": "openid", // Would indicate access token
|
||||
}
|
||||
|
||||
// Should still return cached ID token result
|
||||
isIDToken = validator.detectTokenType(jwt, token)
|
||||
if !isIDToken {
|
||||
t.Error("Expected cached ID token result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_CheckJTIBlacklist_Disabled(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
DisableReplayDetection: true,
|
||||
TokenBlacklist: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"jti": "blacklisted-jti",
|
||||
},
|
||||
}
|
||||
|
||||
// Should not check blacklist when disabled
|
||||
err := validator.checkJTIBlacklist(jwt, "test-token")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when replay detection disabled, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_CheckJTIBlacklist_NoJTI(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenBlacklist: newMockCache(),
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
// No JTI claim
|
||||
},
|
||||
}
|
||||
|
||||
err := validator.checkJTIBlacklist(jwt, "test-token")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error when JTI missing, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_AddJTIToBlacklist(t *testing.T) {
|
||||
blacklist := newMockCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenBlacklist: blacklist,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"jti": "test-jti-123",
|
||||
},
|
||||
}
|
||||
|
||||
validator.addJTIToBlacklist(jwt)
|
||||
|
||||
// Verify JTI was blacklisted
|
||||
data, exists := blacklist.Get("test-jti-123")
|
||||
if !exists {
|
||||
t.Error("Expected JTI to be blacklisted")
|
||||
}
|
||||
|
||||
if reason, ok := data["reason"].(string); !ok || reason != "jti_replay_prevention" {
|
||||
t.Error("Expected JTI blacklist reason to be jti_replay_prevention")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_AddJTIToBlacklist_Disabled(t *testing.T) {
|
||||
blacklist := newMockCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
DisableReplayDetection: true,
|
||||
TokenBlacklist: blacklist,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"jti": "test-jti",
|
||||
},
|
||||
}
|
||||
|
||||
validator.addJTIToBlacklist(jwt)
|
||||
|
||||
// Should not blacklist when disabled
|
||||
_, exists := blacklist.Get("test-jti")
|
||||
if exists {
|
||||
t.Error("Expected JTI not to be blacklisted when replay detection disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_AddJTIToBlacklist_NoJTI(t *testing.T) {
|
||||
blacklist := newMockCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenBlacklist: blacklist,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
// No JTI
|
||||
},
|
||||
}
|
||||
|
||||
validator.addJTIToBlacklist(jwt)
|
||||
|
||||
// Should handle gracefully
|
||||
if len(blacklist.data) != 0 {
|
||||
t.Error("Expected no entries in blacklist when JTI missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_CacheTokenType(t *testing.T) {
|
||||
cache := newMockCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: cache,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
validator.cacheTokenType("cache-key-123", true)
|
||||
|
||||
data, exists := cache.Get("cache-key-123")
|
||||
if !exists {
|
||||
t.Error("Expected token type to be cached")
|
||||
}
|
||||
|
||||
if isID, ok := data["is_id_token"].(bool); !ok || !isID {
|
||||
t.Error("Expected is_id_token to be true")
|
||||
}
|
||||
|
||||
if _, ok := data["cached_at"].(int64); !ok {
|
||||
t.Error("Expected cached_at timestamp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_CacheVerifiedToken(t *testing.T) {
|
||||
tokenCache := newMockTokenCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenCache: tokenCache,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
claims := map[string]interface{}{
|
||||
"sub": "user123",
|
||||
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||
}
|
||||
|
||||
validator.cacheVerifiedToken("test-token", claims)
|
||||
|
||||
cached, exists := tokenCache.GetCachedToken("test-token")
|
||||
if !exists {
|
||||
t.Error("Expected token to be cached")
|
||||
}
|
||||
|
||||
if cached["sub"] != "user123" {
|
||||
t.Error("Expected cached claims to match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_CheckRateLimit(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
// Default implementation returns true
|
||||
if !validator.checkRateLimit() {
|
||||
t.Error("Expected checkRateLimit to return true by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_FindMatchingKey(t *testing.T) {
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
jwks := &JWKS{
|
||||
Keys: []JWK{
|
||||
{Kid: "key-1", Kty: "RSA"},
|
||||
{Kid: "key-2", Kty: "RSA"},
|
||||
{Kid: "key-3", Kty: "RSA"},
|
||||
},
|
||||
}
|
||||
|
||||
key := validator.findMatchingKey(jwks, "key-2")
|
||||
if key == nil {
|
||||
t.Fatal("Expected to find matching key")
|
||||
}
|
||||
|
||||
if key.Kid != "key-2" {
|
||||
t.Errorf("Expected kid 'key-2', got '%s'", key.Kid)
|
||||
}
|
||||
|
||||
// Test non-existent key
|
||||
key = validator.findMatchingKey(jwks, "key-999")
|
||||
if key != nil {
|
||||
t.Error("Expected nil for non-existent key")
|
||||
}
|
||||
|
||||
// Test nil JWKS
|
||||
key = validator.findMatchingKey(nil, "key-1")
|
||||
if key != nil {
|
||||
t.Error("Expected nil for nil JWKS")
|
||||
}
|
||||
}
|
||||
|
||||
// Race condition tests
|
||||
func TestValidator_ConcurrentTokenTypeDetection(t *testing.T) {
|
||||
cache := newMockCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenTypeCache: cache,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
token := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.test-concurrent"
|
||||
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"nonce": "test",
|
||||
},
|
||||
}
|
||||
|
||||
// Concurrent token type detection
|
||||
for i := 0; i < 50; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = validator.detectTokenType(jwt, token)
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Should have cached the result
|
||||
cacheKey := token[:32]
|
||||
if _, exists := cache.Get(cacheKey); !exists {
|
||||
t.Error("Expected token type to be cached after concurrent access")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidator_ConcurrentJTIBlacklisting(t *testing.T) {
|
||||
blacklist := newMockCache()
|
||||
config := ValidatorConfig{
|
||||
ClientID: "test-client",
|
||||
TokenBlacklist: blacklist,
|
||||
MetadataMu: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
validator := NewValidator(config)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Concurrent JTI blacklisting
|
||||
for i := 0; i < 100; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
jwt := &JWT{
|
||||
Claims: map[string]interface{}{
|
||||
"jti": string(rune('A' + idx%26)),
|
||||
},
|
||||
}
|
||||
validator.addJTIToBlacklist(jwt)
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Should have multiple JTIs blacklisted
|
||||
if len(blacklist.data) == 0 {
|
||||
t.Error("Expected JTIs to be blacklisted")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user