mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-17 04:37:48 +00:00
Further pursue of perfection.
This commit is contained in:
@@ -0,0 +1,412 @@
|
||||
package traefikoidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestTokens provides a comprehensive set of standardized test tokens
|
||||
// for consistent testing across the entire codebase.
|
||||
type TestTokens struct{}
|
||||
|
||||
// NewTestTokens creates a new TestTokens instance
|
||||
func NewTestTokens() *TestTokens {
|
||||
return &TestTokens{}
|
||||
}
|
||||
|
||||
// Valid JWT tokens for testing
|
||||
const (
|
||||
// ValidAccessToken - A properly formatted JWT access token for testing
|
||||
ValidAccessToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MzAwMDAwMDAwMCwiaWF0IjoxMDAwMDAwMDAwLCJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImp0aSI6ImU0NzE3ZGFkMGZmMDI5M2QiLCJuYmYiOjEwMDAwMDAwMDAsIm5vbmNlIjoibm9uY2UxMjMiLCJzdWIiOiJ0ZXN0LXN1YmplY3QifQ.bmwp-vk0B7Ir9UiUkzib8L7yJbebJ00o3U9QrB6gP2H9-RfqyCbN8M9Rkx7Rb8Vdh3YzqkBBoLS_G0i414rs2I9uABnTC4E6-63qkGdUrLB7p-XbjcRW2RoIBwXHk7lfumi8eX0uWzBsJ9CY0__UECVsex5XORfBb4Bcqj0LK4y-glxkpI51I7BPySfciWC_PkdaQ1Qe5pCAlxeNs2E9NMGXp-Ox6vAufUzoC2cws1LswGPPP6icQ-Zlzd5WMCIWhdIkN4yTxk8FMqsTC52k2zskRHNSSd4DDVETonfzawZNqDcMpnTyN53sCJ9UHiQTl9mCm61ttYW-W9Gc-ze4Xw"
|
||||
|
||||
// ValidIDToken - A properly formatted JWT ID token for testing
|
||||
ValidIDToken = "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LWlkIiwidHlwIjoiSldUIn0.eyJhdWQiOiJ0ZXN0LWNsaWVudC1pZCIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImV4cCI6MzAwMDAwMDAwMCwiaWF0IjoxMDAwMDAwMDAwLCJpc3MiOiJodHRwczovL3Rlc3QtaXNzdWVyLmNvbSIsImp0aSI6IjZjMGNlNmYxMzhjYTMzNzYiLCJuYmYiOjEwMDAwMDAwMDAsIm5vbmNlIjoibm9uY2UxMjMiLCJzdWIiOiJ0ZXN0LXN1YmplY3QifQ.RBQYejA9vP4lnh2EhFqWerePWaCyDTF0ZE1jlU2xm4g2wWVeaEHpv5SNg92_gwk633N9xx7ugS0UrlEu4qbT7wSb1HBDR00q_andyYnyFk4OoxPpD0AqHkVr-pjS-Z7UCGF3sLgQ4ECmU9695PIys3XvgUGMzEn_mK-PHcpY5AnbBGFsbj7epUld_sb6WfjjjwAa8kKfKObPvaIpuJ4TlxI1Uf0wYOoIA0zh5ipeAn-i8Ud-GErxis1Hp8UQK7IRolXpToiXnFcnf3vI3eCS7Yu3oPl7LRxTxKMCI9h0MCwu25ZNsOg2C9ohyebpU0jbURX9Q74GNOaphv-Lz9rCRA"
|
||||
|
||||
// ValidRefreshToken - A properly formatted refresh token for testing
|
||||
ValidRefreshToken = "valid-refresh-token-12345"
|
||||
|
||||
// MinimalValidJWT - The shortest valid JWT for testing (actual base64url)
|
||||
MinimalValidJWT = "eyJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjMifQ.abc123def456ghi789jkl012mno345pqr678stu901vwx234yz"
|
||||
|
||||
// ValidRefreshTokenGoogle - A Google-style refresh token for testing
|
||||
ValidRefreshTokenGoogle = "google_refresh_token_12345"
|
||||
)
|
||||
|
||||
// Invalid tokens for testing validation
|
||||
const (
|
||||
// InvalidTokenNoDots - Token with no dots (invalid JWT format)
|
||||
InvalidTokenNoDots = "notajwttoken"
|
||||
|
||||
// InvalidTokenOneDot - Token with one dot (invalid JWT format)
|
||||
InvalidTokenOneDot = "header.payload"
|
||||
|
||||
// InvalidTokenThreeDots - Token with three dots (invalid JWT format)
|
||||
InvalidTokenThreeDots = "header.payload.signature.extra"
|
||||
|
||||
// EmptyToken - Empty token
|
||||
EmptyToken = ""
|
||||
|
||||
// CorruptedBase64Token - Token with invalid base64 data for chunking tests
|
||||
CorruptedBase64Token = "corrupted_base64_!@#$"
|
||||
)
|
||||
|
||||
// CreateLargeValidJWT creates a JWT of approximately the specified size
|
||||
// This replaces the ad-hoc createLargeValidJWT function in tests
|
||||
func (tt *TestTokens) CreateLargeValidJWT(targetSize int) string {
|
||||
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
// Create a valid base64url signature
|
||||
signatureBytes := make([]byte, 32)
|
||||
rand.Read(signatureBytes)
|
||||
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||
|
||||
// Calculate required payload size
|
||||
usedSize := len(header) + len(signature) + 2 // account for dots
|
||||
payloadSize := targetSize - usedSize
|
||||
if payloadSize < 50 {
|
||||
payloadSize = 50
|
||||
}
|
||||
|
||||
// Create a payload with realistic JWT claims
|
||||
claims := map[string]interface{}{
|
||||
"sub": "user123",
|
||||
"iss": "https://example.com",
|
||||
"aud": "client123",
|
||||
"exp": 9999999999,
|
||||
"iat": 1000000000,
|
||||
}
|
||||
|
||||
dataSize := payloadSize - 100 // Account for other claims and base64 encoding
|
||||
if dataSize < 10 {
|
||||
dataSize = 10 // Minimum data size
|
||||
}
|
||||
|
||||
claims["data"] = tt.generateRandomString(dataSize)
|
||||
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||
|
||||
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
||||
}
|
||||
|
||||
// CreateLargeRefreshToken creates a refresh token of approximately the specified size
|
||||
func (tt *TestTokens) CreateLargeRefreshToken(targetSize int) string {
|
||||
baseToken := "refresh_token_"
|
||||
padding := tt.generateRandomString(targetSize - len(baseToken))
|
||||
return baseToken + padding
|
||||
}
|
||||
|
||||
// CreateExpiredJWT creates an expired JWT token for testing
|
||||
func (tt *TestTokens) CreateExpiredJWT() string {
|
||||
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
|
||||
// Create claims with expired timestamp
|
||||
claims := map[string]interface{}{
|
||||
"sub": "user123",
|
||||
"iss": "https://example.com",
|
||||
"aud": "client123",
|
||||
"exp": time.Now().Unix() - 3600, // Expired 1 hour ago
|
||||
"iat": time.Now().Unix() - 7200, // Issued 2 hours ago
|
||||
}
|
||||
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||
// Create a valid base64url signature
|
||||
signatureBytes := make([]byte, 16)
|
||||
rand.Read(signatureBytes)
|
||||
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||
|
||||
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
||||
}
|
||||
|
||||
// CreateUniqueValidJWT creates a unique valid JWT for concurrent testing
|
||||
func (tt *TestTokens) CreateUniqueValidJWT(id string) string {
|
||||
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
|
||||
claims := map[string]interface{}{
|
||||
"sub": "user_" + id,
|
||||
"iss": "https://example.com",
|
||||
"aud": "client123",
|
||||
"exp": 9999999999,
|
||||
"iat": 1000000000,
|
||||
"jti": id,
|
||||
}
|
||||
|
||||
claimsJSON, _ := json.Marshal(claims)
|
||||
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||
// Create a valid base64url signature
|
||||
signatureBytes := make([]byte, 16)
|
||||
rand.Read(signatureBytes)
|
||||
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||
|
||||
return fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
||||
}
|
||||
|
||||
// CreateIncompressibleToken creates a token that cannot be compressed effectively
|
||||
// This is useful for testing chunking scenarios where compression doesn't help
|
||||
func (tt *TestTokens) CreateIncompressibleToken(targetSize int) string {
|
||||
header := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
|
||||
// Create a valid base64url signature
|
||||
signatureBytes := make([]byte, 32)
|
||||
rand.Read(signatureBytes)
|
||||
signature := base64.RawURLEncoding.EncodeToString(signatureBytes)
|
||||
|
||||
// Calculate required payload size
|
||||
usedSize := len(header) + len(signature) + 2 // account for dots
|
||||
payloadSize := max(targetSize-usedSize, 100)
|
||||
|
||||
// Generate multiple random fields to prevent compression
|
||||
randomFields := make(map[string]interface{})
|
||||
randomFields["sub"] = "user123"
|
||||
randomFields["iss"] = "https://example.com"
|
||||
randomFields["aud"] = "client123"
|
||||
randomFields["exp"] = 9999999999
|
||||
randomFields["iat"] = 1000000000
|
||||
|
||||
// Add many random fields with random data to prevent compression
|
||||
remainingSize := payloadSize - 200 // Account for base64 encoding and other fields
|
||||
fieldCount := remainingSize / 100 // ~100 bytes per field
|
||||
if fieldCount < 1 {
|
||||
fieldCount = 1
|
||||
}
|
||||
|
||||
for i := 0; i < fieldCount; i++ {
|
||||
// Generate truly random data for each field
|
||||
randomBytes := make([]byte, 50)
|
||||
rand.Read(randomBytes)
|
||||
fieldName := fmt.Sprintf("random_field_%d_%s", i, tt.generateRandomString(8))
|
||||
randomFields[fieldName] = base64.StdEncoding.EncodeToString(randomBytes)
|
||||
}
|
||||
|
||||
claimsJSON, _ := json.Marshal(randomFields)
|
||||
payload := base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||
|
||||
token := fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
||||
|
||||
// If still too small, pad with more random data
|
||||
if len(token) < targetSize {
|
||||
padding := targetSize - len(token)
|
||||
extraRandomBytes := make([]byte, padding/2)
|
||||
rand.Read(extraRandomBytes)
|
||||
randomFields["padding"] = base64.StdEncoding.EncodeToString(extraRandomBytes)
|
||||
claimsJSON, _ = json.Marshal(randomFields)
|
||||
payload = base64.RawURLEncoding.EncodeToString(claimsJSON)
|
||||
token = fmt.Sprintf("%s.%s.%s", header, payload, signature)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// GetValidTokenSet returns a complete set of valid tokens for testing
|
||||
func (tt *TestTokens) GetValidTokenSet() TokenSet {
|
||||
return TokenSet{
|
||||
AccessToken: ValidAccessToken,
|
||||
IDToken: ValidIDToken,
|
||||
RefreshToken: ValidRefreshToken,
|
||||
}
|
||||
}
|
||||
|
||||
// GetGoogleTokenSet returns tokens that simulate Google OIDC provider responses
|
||||
func (tt *TestTokens) GetGoogleTokenSet() TokenSet {
|
||||
return TokenSet{
|
||||
AccessToken: ValidAccessToken,
|
||||
IDToken: ValidIDToken,
|
||||
RefreshToken: ValidRefreshTokenGoogle,
|
||||
}
|
||||
}
|
||||
|
||||
// GetLargeTokenSet returns a set of large tokens for chunking tests
|
||||
func (tt *TestTokens) GetLargeTokenSet() TokenSet {
|
||||
return TokenSet{
|
||||
AccessToken: tt.CreateLargeValidJWT(5000),
|
||||
IDToken: tt.CreateLargeValidJWT(2000),
|
||||
RefreshToken: tt.CreateLargeRefreshToken(3000),
|
||||
}
|
||||
}
|
||||
|
||||
// GetInvalidTokens returns various invalid tokens for validation testing
|
||||
func (tt *TestTokens) GetInvalidTokens() InvalidTokenSet {
|
||||
return InvalidTokenSet{
|
||||
NoDots: InvalidTokenNoDots,
|
||||
OneDot: InvalidTokenOneDot,
|
||||
ThreeDots: InvalidTokenThreeDots,
|
||||
Empty: EmptyToken,
|
||||
Corrupted: CorruptedBase64Token,
|
||||
}
|
||||
}
|
||||
|
||||
// generateRandomString creates a random string of the specified length
|
||||
func (tt *TestTokens) generateRandomString(length int) string {
|
||||
// FIXED: Handle negative or zero lengths safely
|
||||
if length <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := 0; i < length; i++ {
|
||||
randomByte := make([]byte, 1)
|
||||
rand.Read(randomByte)
|
||||
b[i] = charset[int(randomByte[0])%len(charset)]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// TokenSet represents a complete set of tokens for testing
|
||||
type TokenSet struct {
|
||||
AccessToken string
|
||||
IDToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
// InvalidTokenSet represents various invalid tokens for validation testing
|
||||
type InvalidTokenSet struct {
|
||||
NoDots string // Token with 0 dots
|
||||
OneDot string // Token with 1 dot
|
||||
ThreeDots string // Token with 3 dots
|
||||
Empty string // Empty token
|
||||
Corrupted string // Corrupted/invalid characters
|
||||
}
|
||||
|
||||
// TestScenarios provides predefined test scenarios
|
||||
type TestScenarios struct {
|
||||
tokens *TestTokens
|
||||
}
|
||||
|
||||
// NewTestScenarios creates a new TestScenarios instance
|
||||
func NewTestScenarios() *TestScenarios {
|
||||
return &TestScenarios{
|
||||
tokens: NewTestTokens(),
|
||||
}
|
||||
}
|
||||
|
||||
// NormalFlow returns tokens for normal authentication flow testing
|
||||
func (ts *TestScenarios) NormalFlow() TokenSet {
|
||||
return ts.tokens.GetValidTokenSet()
|
||||
}
|
||||
|
||||
// GoogleFlow returns tokens simulating Google OIDC provider
|
||||
func (ts *TestScenarios) GoogleFlow() TokenSet {
|
||||
return ts.tokens.GetGoogleTokenSet()
|
||||
}
|
||||
|
||||
// ChunkingRequired returns large tokens that require chunking
|
||||
func (ts *TestScenarios) ChunkingRequired() TokenSet {
|
||||
return ts.tokens.GetLargeTokenSet()
|
||||
}
|
||||
|
||||
// CorruptionTest returns tokens and corruption scenarios for testing
|
||||
func (ts *TestScenarios) CorruptionTest() CorruptionTestSet {
|
||||
return CorruptionTestSet{
|
||||
ValidTokens: ts.tokens.GetValidTokenSet(),
|
||||
InvalidTokens: ts.tokens.GetInvalidTokens(),
|
||||
LargeTokens: ts.tokens.GetLargeTokenSet(),
|
||||
CorruptedToken: CorruptedBase64Token,
|
||||
}
|
||||
}
|
||||
|
||||
// ConcurrentTest returns unique tokens for concurrent testing
|
||||
func (ts *TestScenarios) ConcurrentTest(count int) []TokenSet {
|
||||
sets := make([]TokenSet, count)
|
||||
for i := 0; i < count; i++ {
|
||||
sets[i] = TokenSet{
|
||||
AccessToken: ts.tokens.CreateUniqueValidJWT(fmt.Sprintf("concurrent_%d", i)),
|
||||
IDToken: ts.tokens.CreateUniqueValidJWT(fmt.Sprintf("id_%d", i)),
|
||||
RefreshToken: fmt.Sprintf("refresh_concurrent_%d", i),
|
||||
}
|
||||
}
|
||||
return sets
|
||||
}
|
||||
|
||||
// CorruptionTestSet represents tokens and scenarios for corruption testing
|
||||
type CorruptionTestSet struct {
|
||||
ValidTokens TokenSet
|
||||
InvalidTokens InvalidTokenSet
|
||||
LargeTokens TokenSet
|
||||
CorruptedToken string
|
||||
}
|
||||
|
||||
// TokenValidationTestCases returns test cases for token validation
|
||||
func (tt *TestTokens) TokenValidationTestCases() []ValidationTestCase {
|
||||
return []ValidationTestCase{
|
||||
{
|
||||
Name: "Empty token",
|
||||
Token: EmptyToken,
|
||||
ExpectStored: true, // Empty tokens are allowed for clearing
|
||||
ExpectRetrieved: false, // But return as empty
|
||||
},
|
||||
{
|
||||
Name: "Single dot",
|
||||
Token: InvalidTokenOneDot,
|
||||
ExpectStored: false, // Invalid JWT format
|
||||
ExpectRetrieved: false,
|
||||
},
|
||||
{
|
||||
Name: "No dots",
|
||||
Token: InvalidTokenNoDots,
|
||||
ExpectStored: false, // Invalid JWT format
|
||||
ExpectRetrieved: false,
|
||||
},
|
||||
{
|
||||
Name: "Too many dots",
|
||||
Token: InvalidTokenThreeDots,
|
||||
ExpectStored: false, // Invalid JWT format
|
||||
ExpectRetrieved: false,
|
||||
},
|
||||
{
|
||||
Name: "Valid minimal JWT",
|
||||
Token: MinimalValidJWT,
|
||||
ExpectStored: true,
|
||||
ExpectRetrieved: true,
|
||||
},
|
||||
{
|
||||
Name: "Valid standard JWT",
|
||||
Token: ValidAccessToken,
|
||||
ExpectStored: true,
|
||||
ExpectRetrieved: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidationTestCase represents a single token validation test case
|
||||
type ValidationTestCase struct {
|
||||
Name string
|
||||
Token string
|
||||
ExpectStored bool
|
||||
ExpectRetrieved bool
|
||||
}
|
||||
|
||||
// Helper functions for common test patterns
|
||||
|
||||
// AssertValidTokenStorage verifies that a valid token can be stored and retrieved
|
||||
func AssertValidTokenStorage(t TestingInterface, session *SessionData, token string) {
|
||||
session.SetAccessToken(token)
|
||||
retrieved := session.GetAccessToken()
|
||||
if retrieved != token {
|
||||
t.Errorf("Token storage failed: expected %q, got %q", token, retrieved)
|
||||
}
|
||||
}
|
||||
|
||||
// AssertInvalidTokenRejection verifies that an invalid token is rejected
|
||||
func AssertInvalidTokenRejection(t TestingInterface, session *SessionData, token string) {
|
||||
original := session.GetAccessToken()
|
||||
session.SetAccessToken(token)
|
||||
after := session.GetAccessToken()
|
||||
if after != original {
|
||||
t.Errorf("Invalid token was not rejected: expected %q, got %q", original, after)
|
||||
}
|
||||
}
|
||||
|
||||
// TestingInterface provides the minimal interface needed for testing
|
||||
type TestingInterface interface {
|
||||
Errorf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in New Issue
Block a user