mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
Cleanup [dec2025] (#101)
* Cleanup excessive comments. * Remove leftovers hanging around from previous refactor * Improve test coverage
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
package fixtures
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TokenFixture provides JWT token generation for tests
|
||||
type TokenFixture struct {
|
||||
RSAPrivateKey *rsa.PrivateKey
|
||||
RSAPublicKey *rsa.PublicKey
|
||||
ECPrivateKey *ecdsa.PrivateKey
|
||||
ECPublicKey *ecdsa.PublicKey
|
||||
KeyID string
|
||||
Issuer string
|
||||
Audience string
|
||||
ClockSkew time.Duration
|
||||
}
|
||||
|
||||
// NewTokenFixture creates a new token fixture with generated keys
|
||||
func NewTokenFixture() (*TokenFixture, error) {
|
||||
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TokenFixture{
|
||||
RSAPrivateKey: rsaKey,
|
||||
RSAPublicKey: &rsaKey.PublicKey,
|
||||
ECPrivateKey: ecKey,
|
||||
ECPublicKey: &ecKey.PublicKey,
|
||||
KeyID: "test-key-id",
|
||||
Issuer: "https://test-issuer.com",
|
||||
Audience: "test-client-id",
|
||||
ClockSkew: 2 * time.Minute,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DefaultClaims returns standard JWT claims
|
||||
func (f *TokenFixture) DefaultClaims() map[string]interface{} {
|
||||
now := time.Now()
|
||||
return map[string]interface{}{
|
||||
"iss": f.Issuer,
|
||||
"aud": f.Audience,
|
||||
"sub": "test-subject",
|
||||
"email": "[email protected]",
|
||||
"exp": now.Add(1 * time.Hour).Unix(),
|
||||
"iat": now.Add(-f.ClockSkew).Unix(),
|
||||
"nbf": now.Add(-f.ClockSkew).Unix(),
|
||||
"nonce": "test-nonce",
|
||||
"jti": generateJTI(),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidToken creates a valid JWT token with optional claim overrides
|
||||
func (f *TokenFixture) ValidToken(claimOverrides map[string]interface{}) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
for k, v := range claimOverrides {
|
||||
claims[k] = v
|
||||
}
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// ExpiredToken creates an expired JWT token
|
||||
func (f *TokenFixture) ExpiredToken() (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["exp"] = time.Now().Add(-1 * time.Hour).Unix()
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// NotYetValidToken creates a token that's not valid yet
|
||||
func (f *TokenFixture) NotYetValidToken() (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["nbf"] = time.Now().Add(1 * time.Hour).Unix()
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenWithSkew creates a token with a specific time offset
|
||||
func (f *TokenFixture) TokenWithSkew(skew time.Duration) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["exp"] = time.Now().Add(skew).Unix()
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenWithRoles creates a token with specific roles
|
||||
func (f *TokenFixture) TokenWithRoles(roles []string) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["roles"] = roles
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenWithGroups creates a token with specific groups
|
||||
func (f *TokenFixture) TokenWithGroups(groups []string) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["groups"] = groups
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenWithEmail creates a token with a specific email
|
||||
func (f *TokenFixture) TokenWithEmail(email string) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["email"] = email
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenWithAudience creates a token with a specific audience
|
||||
func (f *TokenFixture) TokenWithAudience(audience string) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["aud"] = audience
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenWithIssuer creates a token with a specific issuer
|
||||
func (f *TokenFixture) TokenWithIssuer(issuer string) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
claims["iss"] = issuer
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenMissingClaim creates a token missing specified claims
|
||||
func (f *TokenFixture) TokenMissingClaim(missingClaims ...string) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
for _, claim := range missingClaims {
|
||||
delete(claims, claim)
|
||||
}
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// TokenWithCustomClaims creates a token with custom claims
|
||||
func (f *TokenFixture) TokenWithCustomClaims(customClaims map[string]interface{}) (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
for k, v := range customClaims {
|
||||
claims[k] = v
|
||||
}
|
||||
return f.createJWT(claims, "RS256", f.KeyID)
|
||||
}
|
||||
|
||||
// MalformedToken returns an invalid JWT string
|
||||
func (f *TokenFixture) MalformedToken() string {
|
||||
return "not.a.valid.jwt"
|
||||
}
|
||||
|
||||
// EmptyToken returns an empty string
|
||||
func (f *TokenFixture) EmptyToken() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// TokenWithWrongSignature creates a token signed with a different key
|
||||
func (f *TokenFixture) TokenWithWrongSignature() (string, error) {
|
||||
wrongKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claims := f.DefaultClaims()
|
||||
return createJWTWithKey(claims, "RS256", f.KeyID, wrongKey)
|
||||
}
|
||||
|
||||
// TokenWithWrongAlgorithm creates a token with mismatched algorithm
|
||||
func (f *TokenFixture) TokenWithWrongAlgorithm() (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
// Create token claiming RS256 but we'll return it as-is
|
||||
// This simulates algorithm confusion attacks
|
||||
return f.createJWT(claims, "none", f.KeyID)
|
||||
}
|
||||
|
||||
// ECToken creates a token signed with EC key
|
||||
func (f *TokenFixture) ECToken() (string, error) {
|
||||
claims := f.DefaultClaims()
|
||||
return f.createECJWT(claims, "ES256", f.KeyID)
|
||||
}
|
||||
|
||||
// GetJWKS returns a JWKS containing the test public key
|
||||
func (f *TokenFixture) GetJWKS() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"keys": []map[string]interface{}{
|
||||
{
|
||||
"kty": "RSA",
|
||||
"kid": f.KeyID,
|
||||
"use": "sig",
|
||||
"alg": "RS256",
|
||||
"n": base64.RawURLEncoding.EncodeToString(f.RSAPublicKey.N.Bytes()),
|
||||
"e": base64.RawURLEncoding.EncodeToString(bigIntToBytes(big.NewInt(int64(f.RSAPublicKey.E)))),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetJWKSBytes returns JWKS as JSON bytes
|
||||
func (f *TokenFixture) GetJWKSBytes() ([]byte, error) {
|
||||
return json.Marshal(f.GetJWKS())
|
||||
}
|
||||
|
||||
// createJWT creates a JWT with the fixture's RSA key
|
||||
func (f *TokenFixture) createJWT(claims map[string]interface{}, alg, kid string) (string, error) {
|
||||
return createJWTWithKey(claims, alg, kid, f.RSAPrivateKey)
|
||||
}
|
||||
|
||||
// createECJWT creates a JWT with the fixture's EC key
|
||||
func (f *TokenFixture) createECJWT(claims map[string]interface{}, alg, kid string) (string, error) {
|
||||
return createECJWTWithKey(claims, alg, kid, f.ECPrivateKey)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func generateJTI() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = rand.Read(b) // #nosec G104 - test fixture, crypto strength not critical
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
func bigIntToBytes(i *big.Int) []byte {
|
||||
return i.Bytes()
|
||||
}
|
||||
|
||||
func createJWTWithKey(claims map[string]interface{}, alg, kid string, key *rsa.PrivateKey) (string, error) {
|
||||
header := map[string]interface{}{
|
||||
"alg": alg,
|
||||
"typ": "JWT",
|
||||
"kid": kid,
|
||||
}
|
||||
|
||||
headerBytes, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claimsBytes, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
headerB64 := base64.RawURLEncoding.EncodeToString(headerBytes)
|
||||
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsBytes)
|
||||
|
||||
signingInput := headerB64 + "." + claimsB64
|
||||
|
||||
// For "none" algorithm, return without signature
|
||||
if alg == "none" {
|
||||
return signingInput + ".", nil
|
||||
}
|
||||
|
||||
// Sign with RSA-SHA256
|
||||
signature, err := signRS256([]byte(signingInput), key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signatureB64 := base64.RawURLEncoding.EncodeToString(signature)
|
||||
return signingInput + "." + signatureB64, nil
|
||||
}
|
||||
|
||||
func createECJWTWithKey(claims map[string]interface{}, alg, kid string, key *ecdsa.PrivateKey) (string, error) {
|
||||
header := map[string]interface{}{
|
||||
"alg": alg,
|
||||
"typ": "JWT",
|
||||
"kid": kid,
|
||||
}
|
||||
|
||||
headerBytes, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
claimsBytes, err := json.Marshal(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
headerB64 := base64.RawURLEncoding.EncodeToString(headerBytes)
|
||||
claimsB64 := base64.RawURLEncoding.EncodeToString(claimsBytes)
|
||||
|
||||
signingInput := headerB64 + "." + claimsB64
|
||||
|
||||
// Sign with ECDSA-SHA256
|
||||
signature, err := signES256([]byte(signingInput), key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signatureB64 := base64.RawURLEncoding.EncodeToString(signature)
|
||||
return signingInput + "." + signatureB64, nil
|
||||
}
|
||||
|
||||
func signRS256(data []byte, key *rsa.PrivateKey) ([]byte, error) {
|
||||
h := hashSHA256(data)
|
||||
return rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, h)
|
||||
}
|
||||
|
||||
func signES256(data []byte, key *ecdsa.PrivateKey) ([]byte, error) {
|
||||
h := hashSHA256(data)
|
||||
r, s, err := ecdsa.Sign(rand.Reader, key, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encode r and s as fixed-size byte arrays
|
||||
curveBits := key.Curve.Params().BitSize
|
||||
keyBytes := curveBits / 8
|
||||
if curveBits%8 > 0 {
|
||||
keyBytes++
|
||||
}
|
||||
|
||||
rBytes := r.Bytes()
|
||||
sBytes := s.Bytes()
|
||||
|
||||
signature := make([]byte, 2*keyBytes)
|
||||
copy(signature[keyBytes-len(rBytes):keyBytes], rBytes)
|
||||
copy(signature[2*keyBytes-len(sBytes):], sBytes)
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
func hashSHA256(data []byte) []byte {
|
||||
h := sha256.Sum256(data)
|
||||
return h[:]
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package fixtures
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewTokenFixture(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, fixture.RSAPrivateKey)
|
||||
assert.NotNil(t, fixture.RSAPublicKey)
|
||||
assert.NotNil(t, fixture.ECPrivateKey)
|
||||
assert.NotNil(t, fixture.ECPublicKey)
|
||||
assert.NotEmpty(t, fixture.KeyID)
|
||||
assert.NotEmpty(t, fixture.Issuer)
|
||||
assert.NotEmpty(t, fixture.Audience)
|
||||
}
|
||||
|
||||
func TestDefaultClaims(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
claims := fixture.DefaultClaims()
|
||||
|
||||
assert.Equal(t, fixture.Issuer, claims["iss"])
|
||||
assert.Equal(t, fixture.Audience, claims["aud"])
|
||||
assert.NotEmpty(t, claims["sub"])
|
||||
assert.NotEmpty(t, claims["email"])
|
||||
assert.NotNil(t, claims["exp"])
|
||||
assert.NotNil(t, claims["iat"])
|
||||
assert.NotNil(t, claims["nbf"])
|
||||
assert.NotEmpty(t, claims["jti"])
|
||||
}
|
||||
|
||||
func TestValidToken(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("creates valid JWT structure", func(t *testing.T) {
|
||||
token, err := fixture.ValidToken(nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
// JWT has 3 parts
|
||||
parts := strings.Split(token, ".")
|
||||
assert.Len(t, parts, 3)
|
||||
})
|
||||
|
||||
t.Run("applies claim overrides", func(t *testing.T) {
|
||||
token, err := fixture.ValidToken(map[string]interface{}{
|
||||
"email": "[email protected]",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpiredToken(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.ExpiredToken()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
parts := strings.Split(token, ".")
|
||||
assert.Len(t, parts, 3)
|
||||
}
|
||||
|
||||
func TestNotYetValidToken(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.NotYetValidToken()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestTokenWithSkew(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("positive skew", func(t *testing.T) {
|
||||
token, err := fixture.TokenWithSkew(5 * time.Minute)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
|
||||
t.Run("negative skew", func(t *testing.T) {
|
||||
token, err := fixture.TokenWithSkew(-5 * time.Minute)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTokenWithRoles(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.TokenWithRoles([]string{"admin", "user"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestTokenWithGroups(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.TokenWithGroups([]string{"developers", "admins"})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestTokenWithEmail(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.TokenWithEmail("[email protected]")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestTokenWithAudience(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.TokenWithAudience("custom-audience")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestTokenWithIssuer(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.TokenWithIssuer("https://custom-issuer.com")
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestTokenMissingClaim(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("missing single claim", func(t *testing.T) {
|
||||
token, err := fixture.TokenMissingClaim("email")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
|
||||
t.Run("missing multiple claims", func(t *testing.T) {
|
||||
token, err := fixture.TokenMissingClaim("email", "sub", "nonce")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
})
|
||||
}
|
||||
|
||||
func TestTokenWithCustomClaims(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.TokenWithCustomClaims(map[string]interface{}{
|
||||
"custom_claim": "custom_value",
|
||||
"another_claim": 123,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
}
|
||||
|
||||
func TestMalformedToken(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token := fixture.MalformedToken()
|
||||
|
||||
assert.Equal(t, "not.a.valid.jwt", token)
|
||||
parts := strings.Split(token, ".")
|
||||
assert.Len(t, parts, 4) // 4 parts instead of 3
|
||||
}
|
||||
|
||||
func TestEmptyToken(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token := fixture.EmptyToken()
|
||||
|
||||
assert.Empty(t, token)
|
||||
}
|
||||
|
||||
func TestTokenWithWrongSignature(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
token, err := fixture.TokenWithWrongSignature()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
parts := strings.Split(token, ".")
|
||||
assert.Len(t, parts, 3)
|
||||
}
|
||||
|
||||
func TestGetJWKS(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
jwks := fixture.GetJWKS()
|
||||
|
||||
assert.Contains(t, jwks, "keys")
|
||||
keys, ok := jwks["keys"].([]map[string]interface{})
|
||||
require.True(t, ok)
|
||||
assert.Len(t, keys, 1)
|
||||
|
||||
key := keys[0]
|
||||
assert.Equal(t, "RSA", key["kty"])
|
||||
assert.Equal(t, fixture.KeyID, key["kid"])
|
||||
assert.NotEmpty(t, key["n"])
|
||||
assert.NotEmpty(t, key["e"])
|
||||
}
|
||||
|
||||
func TestGetJWKSBytes(t *testing.T) {
|
||||
fixture, err := NewTokenFixture()
|
||||
require.NoError(t, err)
|
||||
|
||||
jwksBytes, err := fixture.GetJWKSBytes()
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, jwksBytes)
|
||||
assert.Contains(t, string(jwksBytes), "keys")
|
||||
}
|
||||
Reference in New Issue
Block a user