mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-19 04:47:48 +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[:]
|
||||
}
|
||||
Reference in New Issue
Block a user