Skip to content

Commit

Permalink
client auth tests
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed Sep 28, 2024
1 parent bff1f4a commit 95370b6
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 141 deletions.
2 changes: 2 additions & 0 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ func fmtRequestObjectDecodeError(token *jwt.Token, client JARClient, issuer stri
return outer.WithDebugf("%s client with id '%s' expects request objects to be signed with the 'typ' header value '%s' but the request object was signed with the 'typ' header value '%s'.", hintRequestObjectPrefix(openid), client.GetID(), consts.JSONWebTokenTypeJWT, token.Header[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderEncryptionTypeInvalid):
return outer.WithDebugf("%s client with id '%s' expects request objects to be encrypted with the 'typ' header value '%s' but the request object was encrypted with the 'typ' header value '%s'.", hintRequestObjectPrefix(openid), client.GetID(), consts.JSONWebTokenTypeJWT, token.HeaderJWE[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderContentTypeInvalidMismatch):
return outer.WithDebugf("%s client with id '%s' expects request objects to be encrypted with a 'cty' header value and signed with a 'typ' value that match but the request object was encrypted with the 'cty' header value '%s' and signed with the 'typ' header value '%s'.", hintRequestObjectPrefix(openid), client.GetID(), token.HeaderJWE[consts.JSONWebTokenHeaderContentType], token.HeaderJWE[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderContentTypeInvalid):
return outer.WithDebugf("%s client with id '%s' expects request objects to be encrypted with the 'cty' header value '%s' but the request object was encrypted with the 'cty' header value '%s'.", hintRequestObjectPrefix(openid), client.GetID(), consts.JSONWebTokenTypeJWT, token.HeaderJWE[consts.JSONWebTokenHeaderContentType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderEncryptionKeyIDInvalid):
Expand Down
165 changes: 126 additions & 39 deletions authorize_request_handler_oidc_request_test.go

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client_authentication_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,8 @@ func fmtClientAssertionDecodeError(token *jwt.Token, client AuthenticationMethod
return outer.WithDebugf("OAuth 2.0 client with id '%s' expects client assertions to be signed with the 'typ' header value '%s' but the client assertion was signed with the 'typ' header value '%s'.", client.GetID(), consts.JSONWebTokenTypeJWT, token.Header[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderEncryptionTypeInvalid):
return outer.WithDebugf("OAuth 2.0 client with id '%s' expects client assertions to be encrypted with the 'typ' header value '%s' but the client assertion was encrypted with the 'typ' header value '%s'.", client.GetID(), consts.JSONWebTokenTypeJWT, token.HeaderJWE[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderContentTypeInvalidMismatch):
return outer.WithDebugf("OAuth 2.0 client with id '%s' expects client assertions to be encrypted with a 'cty' header value and signed with a 'typ' value that match but the client assertions was encrypted with the 'cty' header value '%s' and signed with the 'typ' header value '%s'.", client.GetID(), token.HeaderJWE[consts.JSONWebTokenHeaderContentType], token.HeaderJWE[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderContentTypeInvalid):
return outer.WithDebugf("OAuth 2.0 client with id '%s' expects client assertions to be encrypted with the 'cty' header value '%s' but the client assertion was encrypted with the 'cty' header value '%s'.", client.GetID(), consts.JSONWebTokenTypeJWT, token.HeaderJWE[consts.JSONWebTokenHeaderContentType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderEncryptionKeyIDInvalid):
Expand Down
2 changes: 2 additions & 0 deletions handler/oauth2/strategy_jwt_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ func fmtValidateJWTError(token *jwt.Token, client jwt.Client, inner error) (err
return oauth2.ErrInvalidTokenFormat.WithDebugf("Token %sis expected to be signed with the 'typ' header value '%s' but it was signed with the 'typ' header value '%s'.", clientText, consts.JSONWebTokenTypeJWT, token.Header[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderEncryptionTypeInvalid):
return oauth2.ErrInvalidTokenFormat.WithDebugf("Token %sis expected to be encrypted with the 'typ' header value '%s' but it was encrypted with the 'typ' header value '%s'.", clientText, consts.JSONWebTokenTypeJWT, token.HeaderJWE[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderContentTypeInvalidMismatch):
return oauth2.ErrInvalidTokenFormat.WithDebugf("Token %sis expected to be encrypted with a 'cty' header value and signed with a 'typ' value that match but it was encrypted with the 'cty' header value '%s' and signed with the 'typ' header value '%s'.", clientText, token.HeaderJWE[consts.JSONWebTokenHeaderContentType], token.HeaderJWE[consts.JSONWebTokenHeaderType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderContentTypeInvalid):
return oauth2.ErrInvalidTokenFormat.WithDebugf("Token %sis expected to be encrypted with the 'cty' header value '%s' but it was encrypted with the 'cty' header value '%s'.", clientText, consts.JSONWebTokenTypeJWT, token.HeaderJWE[consts.JSONWebTokenHeaderContentType])
case errJWTValidation.Has(jwt.ValidationErrorHeaderEncryptionKeyIDInvalid):
Expand Down
20 changes: 5 additions & 15 deletions token/jwt/jwt_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (j *DefaultStrategy) Encode(ctx context.Context, opts ...StrategyOpt) (toke
var keyEnc *jose.JSONWebKey

if IsEncryptedJWTClientSecretAlg(alg) {
if keyEnc, err = NewJWKFromClientSecret(ctx, o.client, kid, alg, consts.JSONWebTokenUseEncryption); err != nil {
if keyEnc, err = NewClientSecretJWKFromClient(ctx, o.client, kid, alg, enc, consts.JSONWebTokenUseEncryption); err != nil {
return "", "", errorsx.WithStack(fmt.Errorf("Failed to encrypt the JWT using the client secret. %w", err))
}
} else if keyEnc, err = FindClientPublicJWK(ctx, o.client, j.Config.GetJWKSFetcherStrategy(ctx), kid, alg, consts.JSONWebTokenUseEncryption, false); err != nil {
Expand Down Expand Up @@ -128,10 +128,10 @@ func (j *DefaultStrategy) Decrypt(ctx context.Context, tokenStringEnc string, op
}

var (
kid, alg, cty string
kid, alg, enc string
)

if kid, alg, _, cty, err = headerValidateJWE(jwe.Header); err != nil {
if kid, alg, enc, _, err = headerValidateJWE(jwe.Header); err != nil {
return "", "", nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorMalformed, Inner: err})
}

Expand All @@ -144,7 +144,7 @@ func (j *DefaultStrategy) Decrypt(ctx context.Context, tokenStringEnc string, op
return "", "", nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorUnverifiable, Inner: err})
}

if key, err = NewJWKFromClientSecret(ctx, o.client, kid, alg, consts.JSONWebTokenUseEncryption); err != nil {
if key, err = NewClientSecretJWKFromClient(ctx, o.client, kid, alg, enc, consts.JSONWebTokenUseEncryption); err != nil {
return "", "", nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorUnverifiable, Inner: err})
}
} else if key, err = j.Issuer.GetIssuerStrictJWK(ctx, kid, alg, consts.JSONWebTokenUseEncryption); err != nil {
Expand All @@ -159,16 +159,6 @@ func (j *DefaultStrategy) Decrypt(ctx context.Context, tokenStringEnc string, op

tokenString = string(tokenRaw)

var t *jwt.JSONWebToken

if t, err = jwt.ParseSigned(tokenString, o.sigAlgorithm); err != nil {
return "", "", nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorMalformed, Inner: err})
}

if err = headerValidateJWSNested(t.Headers, cty); err != nil {
return "", "", nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorMalformed, Inner: err})
}

if signature, err = getJWTSignature(tokenString); err != nil {
return "", "", nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorMalformed, Inner: err})
}
Expand Down Expand Up @@ -296,7 +286,7 @@ func (j *DefaultStrategy) validate(ctx context.Context, t *jwt.JSONWebToken, des
return errorsx.WithStack(&ValidationError{Errors: ValidationErrorHeaderKeyIDInvalid, Inner: fmt.Errorf("error validating the jws header: alg '%s' does not support tokens with a kid but the token has kid '%s'", alg, kid)})
}

if key, err = NewJWKFromClientSecret(ctx, o.client, "", alg, consts.JSONWebTokenUseSignature); err != nil {
if key, err = NewClientSecretJWKFromClient(ctx, o.client, "", alg, "", consts.JSONWebTokenUseSignature); err != nil {
return errorsx.WithStack(&ValidationError{Errors: ValidationErrorUnverifiable, Inner: err})
}
} else {
Expand Down
35 changes: 24 additions & 11 deletions token/jwt/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,23 +366,28 @@ func (t *Token) Valid(opts ...HeaderValidationOption) (err error) {

if t.HeaderJWE != nil && (t.KeyAlgorithm != "" || t.ContentEncryption != "") {
var (
cty, typ, ttyp any
ok bool
typ any
ok bool
)

if typ, ok = t.HeaderJWE[consts.JSONWebTokenHeaderType]; !ok || typ != consts.JSONWebTokenTypeJWT {
vErr.Inner = errors.New("token was encrypted with invalid typ")
vErr.Errors |= ValidationErrorHeaderEncryptionTypeInvalid
}

if ttyp, ok = t.Header[consts.JSONWebTokenHeaderType]; !ok {
vErr.Inner = errors.New("token was signed with invalid typ")
vErr.Errors |= ValidationErrorHeaderTypeInvalid
ttyp := t.Header[consts.JSONWebTokenHeaderType]
cty := t.HeaderJWE[consts.JSONWebTokenHeaderContentType]

if cty != ttyp {
vErr.Inner = errors.New("token was encrypted with a cty value that doesn't match the typ value")
vErr.Errors |= ValidationErrorHeaderContentTypeInvalidMismatch
}

if cty, ok = t.HeaderJWE[consts.JSONWebTokenHeaderContentType]; !ok || cty != ttyp {
vErr.Inner = errors.New("token was encrypted with invalid cty or signed with an invalid typ")
vErr.Errors |= ValidationErrorHeaderContentTypeInvalid
if len(vopts.types) != 0 {
if !validateTokenTypeValue(vopts.types, cty) {
vErr.Inner = errors.New("token was encrypted with an invalid cty")
vErr.Errors |= ValidationErrorHeaderContentTypeInvalid
}
}
}

Expand Down Expand Up @@ -571,9 +576,8 @@ func pointer(v any) any {
return v
}

func validateTokenType(typValues []string, header map[string]any) bool {
func validateTokenType(values []string, header map[string]any) bool {
var (
typ string
raw any
ok bool
)
Expand All @@ -582,11 +586,20 @@ func validateTokenType(typValues []string, header map[string]any) bool {
return false
}

return validateTokenTypeValue(values, raw)
}

func validateTokenTypeValue(values []string, raw any) bool {
var (
typ string
ok bool
)

if typ, ok = raw.(string); !ok {
return false
}

for _, t := range typValues {
for _, t := range values {
if t == typ {
return true
}
Expand Down
146 changes: 90 additions & 56 deletions token/jwt/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package jwt
import (
"context"
"crypto"
"crypto/aes"
"crypto/sha256"
"crypto/sha512"
"fmt"
"hash"
"regexp"
"strings"

"github.com/go-jose/go-jose/v4"
jjwt "github.com/go-jose/go-jose/v4/jwt"
"github.com/go-jose/go-jose/v4/jwt"
"github.com/pkg/errors"

"authelia.com/provider/oauth2/internal/consts"
Expand Down Expand Up @@ -89,31 +93,6 @@ func headerValidateJWS(headers []jose.Header) (kid, alg string, err error) {
return headers[0].KeyID, headers[0].Algorithm, nil
}

func headerValidateJWSNested(headers []jose.Header, cty string) (err error) {
switch len(headers) {
case 1:
break
case 0:
return fmt.Errorf("jws header is missing")
default:
return fmt.Errorf("jws header is malformed")
}

typ, ok := headers[0].ExtraHeaders[consts.JSONWebTokenHeaderType]
if !ok {
return fmt.Errorf("jws header 'typ' value is missing")
}

switch typ {
case "":
return fmt.Errorf("jws header 'typ' value is empty")
case cty:
return nil
default:
return fmt.Errorf("jws header 'typ' value '%s' is invalid: jwe header 'cty' value '%s' should match the jws header 'typ' value", typ, cty)
}
}

func headerValidateJWE(header jose.Header) (kid, alg, enc, cty string, err error) {
if header.KeyID == "" && !IsEncryptedJWTClientSecretAlg(header.Algorithm) {
return "", "", "", "", fmt.Errorf("jwe header 'kid' value is missing or empty")
Expand All @@ -137,7 +116,6 @@ func headerValidateJWE(header jose.Header) (kid, alg, enc, cty string, err error
} else if p2c < 200000 {
return "", "", "", "", fmt.Errorf("jwe header 'p2c' has an invalid value '%d': less than 200,000", int(p2c))
}

default:
return "", "", "", "", fmt.Errorf("jwe header 'p2c' value has invalid type %T", p2c)
}
Expand All @@ -159,25 +137,8 @@ func headerValidateJWE(header jose.Header) (kid, alg, enc, cty string, err error
}
}

if value, ok = header.ExtraHeaders[consts.JSONWebTokenHeaderContentType]; !ok {
return "", "", "", "", fmt.Errorf("jwe header 'cty' value is missing")
} else {
switch ctyv := value.(type) {
case string:
switch ctyv {
case consts.JSONWebTokenTypeJWT, consts.JSONWebTokenTypeAccessToken, consts.JSONWebTokenTypeAccessTokenAlternative, consts.JSONWebTokenTypeTokenIntrospection:
cty = ctyv
break
default:
return "", "", "", "", fmt.Errorf("jwe header 'cty' value '%s' is invalid", cty)
}
default:
return "", "", "", "", fmt.Errorf("jwe header 'cty' value has invalid type %T", cty)
}
}

if header.JSONWebKey != nil {
return "", "", "", "", fmt.Errorf("jwe header 'jwk' value is present but not supported")
if value, ok = header.ExtraHeaders[consts.JSONWebTokenHeaderContentType]; ok {
cty, _ = value.(string)
}

return header.KeyID, header.Algorithm, enc, cty, nil
Expand Down Expand Up @@ -282,8 +243,8 @@ func SearchJWKS(jwks *jose.JSONWebKeySet, kid, alg, use string, strict bool) (ke
}
}

// NewJWKFromClientSecret returns a JWK from a client secret.
func NewJWKFromClientSecret(ctx context.Context, client BaseClient, kid, alg, use string) (jwk *jose.JSONWebKey, err error) {
// NewClientSecretJWKFromClient returns a client secret based JWK from a client.
func NewClientSecretJWKFromClient(ctx context.Context, client BaseClient, kid, alg, enc, use string) (jwk *jose.JSONWebKey, err error) {
var (
secret []byte
ok bool
Expand All @@ -297,16 +258,89 @@ func NewJWKFromClientSecret(ctx context.Context, client BaseClient, kid, alg, us
return nil, &JWKLookupError{Description: "The client is not configured with a client secret"}
}

return NewClientSecretJWK(ctx, secret, kid, alg, enc, use)
}

// NewClientSecretJWK returns a client secret based JWK from a client secret value.
//
// The symmetric encryption key is derived from the client_secret value by using the left-most bits of a truncated
// SHA-2 hash of the octets of the UTF-8 representation of the client_secret. For keys of 256 or fewer bits, SHA-256
// is used; for keys of 257-384 bits, SHA-384 is used; for keys of 385-512 bits, SHA-512 is used. The hash value MUST
// be truncated retaining the left-most bits to the appropriate bit length for the AES key wrapping or direct
// encryption algorithm used, for instance, truncating the SHA-256 hash to 128 bits for A128KW. If a symmetric key with
// greater than 512 bits is needed, a different method of deriving the key from the client_secret would have to be
// defined by an extension. Symmetric encryption MUST NOT be used by public (non-confidential) Clients because of
// their inability to keep secrets.
func NewClientSecretJWK(ctx context.Context, secret []byte, kid, alg, enc, use string) (jwk *jose.JSONWebKey, err error) {
if len(secret) == 0 {
return nil, &JWKLookupError{Description: "The client is not configured with a client secret that can be used for symmetric algorithms"}
}

return &jose.JSONWebKey{
Key: secret,
KeyID: kid,
Algorithm: alg,
Use: use,
}, nil
switch use {
case consts.JSONWebTokenUseSignature:
return &jose.JSONWebKey{
Key: secret,
KeyID: kid,
Algorithm: alg,
Use: use,
}, nil
case consts.JSONWebTokenUseEncryption:
var (
hasher hash.Hash
bits int
)

keyAlg := jose.KeyAlgorithm(alg)

switch keyAlg {
case jose.A128KW, jose.A128GCMKW, jose.A192KW, jose.A192GCMKW, jose.A256KW, jose.A256GCMKW, jose.PBES2_HS256_A128KW:
hasher = sha256.New()
case jose.PBES2_HS384_A192KW:
hasher = sha512.New384()
case jose.PBES2_HS512_A256KW, jose.DIRECT:
hasher = sha512.New()
default:
return nil, &JWKLookupError{Description: fmt.Sprintf("Unsupported algorithm '%s'", alg)}
}

switch keyAlg {
case jose.A128KW, jose.A128GCMKW, jose.PBES2_HS256_A128KW:
bits = aes.BlockSize
case jose.A192KW, jose.A192GCMKW, jose.PBES2_HS384_A192KW:
bits = aes.BlockSize * 1.5
case jose.A256KW, jose.A256GCMKW, jose.PBES2_HS512_A256KW:
bits = aes.BlockSize * 2
case jose.DIRECT:
switch jose.ContentEncryption(enc) {
case jose.A128CBC_HS256, "":
bits = aes.BlockSize * 2
case jose.A192CBC_HS384:
bits = aes.BlockSize * 3
case jose.A256CBC_HS512:
bits = aes.BlockSize * 4
default:
return nil, &JWKLookupError{Description: fmt.Sprintf("Unsupported content encryption for the direct key algorthm '%s'", enc)}
}
}

if _, err = hasher.Write(secret); err != nil {
return nil, &JWKLookupError{Description: fmt.Sprintf("Unable to derive key from hashing the client secret. %s", err.Error())}
}

return &jose.JSONWebKey{
Key: hasher.Sum(nil)[:bits],
KeyID: kid,
Algorithm: alg,
Use: use,
}, nil
default:
return &jose.JSONWebKey{
Key: secret,
KeyID: kid,
Algorithm: alg,
Use: use,
}, nil
}
}

func EncodeCompactSigned(ctx context.Context, claims MapClaims, headers Mapper, key *jose.JSONWebKey) (tokenString string, signature string, err error) {
Expand Down Expand Up @@ -380,8 +414,8 @@ func getPublicJWK(jwk *jose.JSONWebKey) jose.JSONWebKey {
return jwk.Public()
}

func UnsafeParseSignedAny(tokenString string, dest any) (token *jjwt.JSONWebToken, err error) {
if token, err = jjwt.ParseSigned(tokenString, SignatureAlgorithmsNone); err != nil {
func UnsafeParseSignedAny(tokenString string, dest any) (token *jwt.JSONWebToken, err error) {
if token, err = jwt.ParseSigned(tokenString, SignatureAlgorithmsNone); err != nil {
return nil, err
}

Expand Down
Loading

0 comments on commit 95370b6

Please sign in to comment.