mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-06 22:49:43 +00:00
ae59a5e88a
* 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
364 lines
11 KiB
Go
364 lines
11 KiB
Go
package traefikoidc
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestGenerateNonce tests the nonce generation for OIDC flows
|
|
func TestGenerateNonce(t *testing.T) {
|
|
t.Run("basic generation", func(t *testing.T) {
|
|
nonce, err := generateNonce()
|
|
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, nonce)
|
|
|
|
// 32 bytes base64 URL encoded should produce 44 characters (with potential padding)
|
|
// but typically 43 characters without padding
|
|
assert.GreaterOrEqual(t, len(nonce), 43, "nonce should be at least 43 characters")
|
|
})
|
|
|
|
t.Run("nonce is base64 URL encoded", func(t *testing.T) {
|
|
nonce, err := generateNonce()
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Should be valid base64 URL encoding
|
|
_, err = base64.URLEncoding.DecodeString(nonce)
|
|
assert.NoError(t, err, "nonce should be valid base64 URL encoding")
|
|
})
|
|
|
|
t.Run("multiple generations produce different values", func(t *testing.T) {
|
|
nonce1, err1 := generateNonce()
|
|
nonce2, err2 := generateNonce()
|
|
|
|
require.NoError(t, err1)
|
|
require.NoError(t, err2)
|
|
|
|
assert.NotEqual(t, nonce1, nonce2, "consecutive generations should produce different nonces")
|
|
})
|
|
|
|
t.Run("nonce has sufficient entropy", func(t *testing.T) {
|
|
// Generate multiple nonces and verify they're all unique
|
|
nonces := make(map[string]bool)
|
|
iterations := 100
|
|
|
|
for i := 0; i < iterations; i++ {
|
|
nonce, err := generateNonce()
|
|
require.NoError(t, err)
|
|
|
|
// Check for duplicates
|
|
assert.False(t, nonces[nonce], "nonce should be unique across multiple generations")
|
|
nonces[nonce] = true
|
|
}
|
|
|
|
assert.Len(t, nonces, iterations, "all nonces should be unique")
|
|
})
|
|
|
|
t.Run("nonce length is consistent", func(t *testing.T) {
|
|
nonce1, err1 := generateNonce()
|
|
nonce2, err2 := generateNonce()
|
|
|
|
require.NoError(t, err1)
|
|
require.NoError(t, err2)
|
|
|
|
assert.Equal(t, len(nonce1), len(nonce2), "nonce length should be consistent")
|
|
})
|
|
}
|
|
|
|
// TestGenerateCodeVerifier tests the PKCE code verifier generation
|
|
func TestGenerateCodeVerifier(t *testing.T) {
|
|
t.Run("basic generation", func(t *testing.T) {
|
|
verifier, err := generateCodeVerifier()
|
|
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, verifier)
|
|
|
|
// RFC 7636 requires 43-128 characters for code verifier
|
|
// With 32 bytes base64 raw URL encoded, we get 43 characters
|
|
assert.Len(t, verifier, 43, "code verifier should be 43 characters (32 bytes base64 encoded)")
|
|
})
|
|
|
|
t.Run("verifier is base64 URL encoded", func(t *testing.T) {
|
|
verifier, err := generateCodeVerifier()
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Should be valid base64 URL encoding
|
|
_, err = base64.RawURLEncoding.DecodeString(verifier)
|
|
assert.NoError(t, err, "verifier should be valid base64 URL encoding")
|
|
})
|
|
|
|
t.Run("multiple generations produce different values", func(t *testing.T) {
|
|
verifier1, err1 := generateCodeVerifier()
|
|
verifier2, err2 := generateCodeVerifier()
|
|
|
|
require.NoError(t, err1)
|
|
require.NoError(t, err2)
|
|
|
|
assert.NotEqual(t, verifier1, verifier2, "consecutive generations should produce different verifiers")
|
|
})
|
|
|
|
t.Run("verifier contains only URL-safe characters", func(t *testing.T) {
|
|
verifier, err := generateCodeVerifier()
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Base64 URL encoding should only contain A-Z, a-z, 0-9, -, _
|
|
for _, char := range verifier {
|
|
validChar := (char >= 'A' && char <= 'Z') ||
|
|
(char >= 'a' && char <= 'z') ||
|
|
(char >= '0' && char <= '9') ||
|
|
char == '-' || char == '_'
|
|
assert.True(t, validChar, "verifier should only contain URL-safe characters")
|
|
}
|
|
})
|
|
|
|
t.Run("no padding characters", func(t *testing.T) {
|
|
verifier, err := generateCodeVerifier()
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Raw URL encoding should not have padding
|
|
assert.False(t, strings.Contains(verifier, "="), "verifier should not contain padding")
|
|
})
|
|
}
|
|
|
|
// TestDeriveCodeChallenge tests the PKCE code challenge derivation
|
|
func TestDeriveCodeChallenge(t *testing.T) {
|
|
t.Run("basic derivation", func(t *testing.T) {
|
|
verifier := "test-verifier-value-1234567890abcdefghij"
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
assert.NotEmpty(t, challenge)
|
|
assert.NotEqual(t, verifier, challenge, "challenge should be different from verifier")
|
|
})
|
|
|
|
t.Run("challenge is SHA256 hash", func(t *testing.T) {
|
|
verifier := "test-code-verifier"
|
|
|
|
// Manually compute expected challenge
|
|
hasher := sha256.New()
|
|
hasher.Write([]byte(verifier))
|
|
expectedHash := hasher.Sum(nil)
|
|
expectedChallenge := base64.RawURLEncoding.EncodeToString(expectedHash)
|
|
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
assert.Equal(t, expectedChallenge, challenge, "challenge should match SHA256 hash")
|
|
})
|
|
|
|
t.Run("same verifier produces same challenge", func(t *testing.T) {
|
|
verifier := "consistent-verifier-12345"
|
|
|
|
challenge1 := deriveCodeChallenge(verifier)
|
|
challenge2 := deriveCodeChallenge(verifier)
|
|
|
|
assert.Equal(t, challenge1, challenge2, "same verifier should always produce same challenge")
|
|
})
|
|
|
|
t.Run("different verifiers produce different challenges", func(t *testing.T) {
|
|
verifier1 := "verifier-one"
|
|
verifier2 := "verifier-two"
|
|
|
|
challenge1 := deriveCodeChallenge(verifier1)
|
|
challenge2 := deriveCodeChallenge(verifier2)
|
|
|
|
assert.NotEqual(t, challenge1, challenge2, "different verifiers should produce different challenges")
|
|
})
|
|
|
|
t.Run("challenge is base64 URL encoded", func(t *testing.T) {
|
|
verifier := "test-verifier"
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
// Should be valid base64 URL encoding
|
|
_, err := base64.RawURLEncoding.DecodeString(challenge)
|
|
assert.NoError(t, err, "challenge should be valid base64 URL encoding")
|
|
})
|
|
|
|
t.Run("challenge length is correct", func(t *testing.T) {
|
|
verifier := "some-random-verifier"
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
// SHA256 produces 32 bytes, which when base64 encoded becomes 43 characters
|
|
assert.Len(t, challenge, 43, "SHA256 hash should produce 43-character base64 string")
|
|
})
|
|
|
|
t.Run("no padding in challenge", func(t *testing.T) {
|
|
verifier := "test-verifier-no-padding"
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
assert.False(t, strings.Contains(challenge, "="), "challenge should not contain padding")
|
|
})
|
|
|
|
t.Run("empty verifier produces valid challenge", func(t *testing.T) {
|
|
verifier := ""
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
assert.NotEmpty(t, challenge, "even empty verifier should produce a challenge")
|
|
assert.Len(t, challenge, 43, "challenge should still be 43 characters")
|
|
})
|
|
}
|
|
|
|
// TestPKCEFlowIntegration tests the complete PKCE flow
|
|
func TestPKCEFlowIntegration(t *testing.T) {
|
|
t.Run("complete PKCE flow", func(t *testing.T) {
|
|
// Step 1: Generate code verifier
|
|
verifier, err := generateCodeVerifier()
|
|
require.NoError(t, err)
|
|
|
|
// Step 2: Derive code challenge
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
// Verify challenge was derived from verifier
|
|
expectedChallenge := deriveCodeChallenge(verifier)
|
|
assert.Equal(t, expectedChallenge, challenge)
|
|
|
|
// Verify verifier can be used to recreate challenge
|
|
rechallenge := deriveCodeChallenge(verifier)
|
|
assert.Equal(t, challenge, rechallenge, "verifier should consistently produce same challenge")
|
|
})
|
|
|
|
t.Run("multiple PKCE flows are independent", func(t *testing.T) {
|
|
// Flow 1
|
|
verifier1, err1 := generateCodeVerifier()
|
|
require.NoError(t, err1)
|
|
challenge1 := deriveCodeChallenge(verifier1)
|
|
|
|
// Flow 2
|
|
verifier2, err2 := generateCodeVerifier()
|
|
require.NoError(t, err2)
|
|
challenge2 := deriveCodeChallenge(verifier2)
|
|
|
|
// Flows should be independent
|
|
assert.NotEqual(t, verifier1, verifier2)
|
|
assert.NotEqual(t, challenge1, challenge2)
|
|
|
|
// Each flow should be internally consistent
|
|
assert.Equal(t, challenge1, deriveCodeChallenge(verifier1))
|
|
assert.Equal(t, challenge2, deriveCodeChallenge(verifier2))
|
|
})
|
|
|
|
t.Run("RFC 7636 compliance", func(t *testing.T) {
|
|
verifier, err := generateCodeVerifier()
|
|
require.NoError(t, err)
|
|
|
|
challenge := deriveCodeChallenge(verifier)
|
|
|
|
// RFC 7636 Section 4.2:
|
|
// - code_verifier: high-entropy cryptographic random string
|
|
// - Minimum length: 43 characters
|
|
// - Maximum length: 128 characters
|
|
// - Character set: [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
|
|
assert.GreaterOrEqual(t, len(verifier), 43, "verifier should be at least 43 characters")
|
|
assert.LessOrEqual(t, len(verifier), 128, "verifier should be at most 128 characters")
|
|
|
|
// RFC 7636 Section 4.2:
|
|
// - code_challenge = BASE64URL(SHA256(code_verifier))
|
|
assert.NotEmpty(t, challenge)
|
|
assert.Len(t, challenge, 43, "S256 challenge should be 43 characters")
|
|
})
|
|
}
|
|
|
|
// TestTokenCacheCleanupAndClose tests the no-op Cleanup and Close methods
|
|
func TestTokenCacheCleanupAndClose(t *testing.T) {
|
|
cache := NewTokenCache()
|
|
require.NotNil(t, cache)
|
|
|
|
t.Run("cleanup is safe to call", func(t *testing.T) {
|
|
// Should not panic
|
|
assert.NotPanics(t, func() {
|
|
cache.Cleanup()
|
|
})
|
|
})
|
|
|
|
t.Run("close is safe to call", func(t *testing.T) {
|
|
// Should not panic
|
|
assert.NotPanics(t, func() {
|
|
cache.Close()
|
|
})
|
|
})
|
|
|
|
t.Run("multiple cleanup calls are safe", func(t *testing.T) {
|
|
assert.NotPanics(t, func() {
|
|
cache.Cleanup()
|
|
cache.Cleanup()
|
|
cache.Cleanup()
|
|
})
|
|
})
|
|
|
|
t.Run("multiple close calls are safe", func(t *testing.T) {
|
|
assert.NotPanics(t, func() {
|
|
cache.Close()
|
|
cache.Close()
|
|
cache.Close()
|
|
})
|
|
})
|
|
|
|
t.Run("operations work after cleanup", func(t *testing.T) {
|
|
cache.Cleanup()
|
|
|
|
// Should still work
|
|
testClaims := map[string]interface{}{"sub": "user123"}
|
|
cache.Set("token1", testClaims, 1*time.Minute)
|
|
|
|
claims, found := cache.Get("token1")
|
|
assert.True(t, found)
|
|
assert.Equal(t, testClaims, claims)
|
|
})
|
|
|
|
t.Run("operations work after close", func(t *testing.T) {
|
|
cache.Close()
|
|
|
|
// Should still work (close is a no-op)
|
|
testClaims := map[string]interface{}{"sub": "user456"}
|
|
cache.Set("token2", testClaims, 1*time.Minute)
|
|
|
|
claims, found := cache.Get("token2")
|
|
assert.True(t, found)
|
|
assert.Equal(t, testClaims, claims)
|
|
})
|
|
}
|
|
|
|
// TestCreateStringMap tests the createStringMap utility function
|
|
func TestCreateStringMap(t *testing.T) {
|
|
t.Run("empty slice", func(t *testing.T) {
|
|
result := createStringMap([]string{})
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("single item", func(t *testing.T) {
|
|
result := createStringMap([]string{"key1"})
|
|
assert.Len(t, result, 1)
|
|
_, exists := result["key1"]
|
|
assert.True(t, exists)
|
|
})
|
|
|
|
t.Run("multiple items", func(t *testing.T) {
|
|
result := createStringMap([]string{"key1", "key2", "key3"})
|
|
assert.Len(t, result, 3)
|
|
|
|
for _, key := range []string{"key1", "key2", "key3"} {
|
|
_, exists := result[key]
|
|
assert.True(t, exists, "key %s should exist", key)
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate items", func(t *testing.T) {
|
|
result := createStringMap([]string{"key1", "key2", "key1", "key3", "key2"})
|
|
// Map should only contain unique keys
|
|
assert.Len(t, result, 3)
|
|
|
|
for _, key := range []string{"key1", "key2", "key3"} {
|
|
_, exists := result[key]
|
|
assert.True(t, exists, "key %s should exist", key)
|
|
}
|
|
})
|
|
}
|