Skip to content

Commit

Permalink
client auth
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed Sep 22, 2024
1 parent 3a07d29 commit b1f7e9b
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 99 deletions.
18 changes: 2 additions & 16 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,25 +134,11 @@ func (f *Fosite) authorizeRequestParametersFromOpenIDConnectRequestObject(ctx co
return errorsx.WithStack(fmtRequestObjectDecodeError(token, client, issuer, openid, err))
}

if algAny {
if token.SignatureAlgorithm == consts.JSONWebTokenAlgNone {
return errorsx.WithStack(
ErrInvalidRequestObject.
WithHintf("%s client provided a request object that has an invalid 'kid' or 'alg' header value.", hintRequestObjectPrefix(openid)).
WithDebugf("%s client with id '%s' was not explicitly registered with a 'request_object_signing_alg' value of 'none' but the request object had the 'alg' value 'none' in the header.", hintRequestObjectPrefix(openid), client.GetID()))
}
} else if string(token.SignatureAlgorithm) != alg {
return errorsx.WithStack(
ErrInvalidRequestObject.
WithHintf("%s client provided a request object that has an invalid 'kid' or 'alg' header value.", hintRequestObjectPrefix(openid)).
WithDebugf("%s client with id '%s' was registered with a 'request_object_signing_alg' value of '%s' but the request object had the 'alg' value '%s' in the header.", hintRequestObjectPrefix(openid), client.GetID(), alg, token.SignatureAlgorithm))
}

if kid := client.GetRequestObjectSigningKeyID(); kid != "" && kid != token.KeyID {
if algAny && token.SignatureAlgorithm == consts.JSONWebTokenAlgNone {
return errorsx.WithStack(
ErrInvalidRequestObject.
WithHintf("%s client provided a request object that has an invalid 'kid' or 'alg' header value.", hintRequestObjectPrefix(openid)).
WithDebugf("%s client with id '%s' was registered with a 'request_object_signing_key_id' value of '%s' but the request object had the 'kid' value '%s' in the header.", hintRequestObjectPrefix(openid), client.GetID(), kid, token.KeyID))
WithDebugf("%s client with id '%s' was not explicitly registered with a 'request_object_signing_alg' value of 'none' but the request object had the 'alg' value 'none' in the header.", hintRequestObjectPrefix(openid), client.GetID()))
}

claims := token.Claims
Expand Down
45 changes: 45 additions & 0 deletions client_authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,51 @@ type EndpointClientAuthHandler interface {
AllowAuthMethodAny() bool
}

type EndpointClientAuthJWTClient struct {
client AuthenticationMethodClient
handler EndpointClientAuthHandler
}

func (c *EndpointClientAuthJWTClient) GetID() string {
return c.client.GetID()
}

func (c *EndpointClientAuthJWTClient) GetClientSecretPlainText() (secret []byte, ok bool, err error) {
return c.client.GetClientSecretPlainText()
}

func (c *EndpointClientAuthJWTClient) GetJSONWebKeys() (jwks *jose.JSONWebKeySet) {
return c.client.GetJSONWebKeys()
}

func (c *EndpointClientAuthJWTClient) GetJSONWebKeysURI() (uri string) {
return c.client.GetJSONWebKeysURI()
}

func (c *EndpointClientAuthJWTClient) GetSigningKeyID() (kid string) {
return ""
}

func (c *EndpointClientAuthJWTClient) GetSigningAlg() (alg string) {
return c.handler.GetAuthSigningAlg(c.client)
}

func (c *EndpointClientAuthJWTClient) GetEncryptionKeyID() (kid string) {
return ""
}

func (c *EndpointClientAuthJWTClient) GetEncryptionAlg() (alg string) {
return ""
}

func (c *EndpointClientAuthJWTClient) GetEncryptionEnc() (enc string) {
return ""
}

func (c *EndpointClientAuthJWTClient) IsClientSigned() (is bool) {
return true
}

type TokenEndpointClientAuthHandler struct{}

func (h *TokenEndpointClientAuthHandler) GetAuthMethod(client AuthenticationMethodClient) string {
Expand Down
58 changes: 39 additions & 19 deletions client_authentication_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
xjwt "github.com/golang-jwt/jwt/v5"

"authelia.com/provider/oauth2/internal/consts"
"authelia.com/provider/oauth2/token/jwt"
"authelia.com/provider/oauth2/x/errorsx"
)

Expand All @@ -21,6 +22,7 @@ type DefaultClientAuthenticationStrategy struct {
ClientManager
}
Config interface {
JWTStrategyProvider
JWKSFetcherStrategyProvider
AllowedJWTAssertionAudiencesProvider
}
Expand Down Expand Up @@ -48,7 +50,7 @@ func (s *DefaultClientAuthenticationStrategy) AuthenticateClient(ctx context.Con
var assertion *ClientAssertion

if hasAssertion {
if assertion, err = NewClientAssertion(ctx, s.Store, assertionValue, assertionType, resolver); err != nil {
if assertion, err = NewClientAssertion(ctx, s.Config.GetJWTStrategy(ctx), s.Store, assertionValue, assertionType, resolver); err != nil {
return nil, "", err
}
}
Expand Down Expand Up @@ -135,9 +137,9 @@ func (s *DefaultClientAuthenticationStrategy) authenticate(ctx context.Context,
}

// NewClientAssertion converts a raw assertion string into a *ClientAssertion.
func NewClientAssertion(ctx context.Context, store ClientManager, assertion, assertionType string, resolver EndpointClientAuthHandler) (a *ClientAssertion, err error) {
func NewClientAssertion(ctx context.Context, strategy jwt.Strategy, store ClientManager, assertion, assertionType string, resolver EndpointClientAuthHandler) (a *ClientAssertion, err error) {
var (
token *xjwt.Token
token *jwt.Token

id, alg, method string
client Client
Expand All @@ -152,12 +154,14 @@ func NewClientAssertion(ctx context.Context, store ClientManager, assertion, ass
return &ClientAssertion{Assertion: assertion, Type: assertionType}, errorsx.WithStack(ErrInvalidRequest.WithHintf("Unknown client_assertion_type '%s'.", assertionType))
}

if token, _, err = xjwt.NewParser(xjwt.WithoutClaimsValidation()).ParseUnverified(assertion, &xjwt.MapClaims{}); err != nil {
if token, err = strategy.Decode(ctx, assertion, jwt.WithAllowUnverified()); err != nil {
return &ClientAssertion{Assertion: assertion, Type: assertionType}, resolveJWTErrorToRFCError(err)
}

if id, err = token.Claims.GetSubject(); err != nil {
if id, err = token.Claims.GetIssuer(); err != nil {
var ok bool

if id, ok = token.Claims.GetSubject(); !ok {
if id, ok = token.Claims.GetIssuer(); !ok {
return &ClientAssertion{Assertion: assertion, Type: assertionType}, nil
}
}
Expand All @@ -166,7 +170,9 @@ func NewClientAssertion(ctx context.Context, store ClientManager, assertion, ass
return &ClientAssertion{Assertion: assertion, Type: assertionType, ID: id}, nil
}

if c, ok := client.(AuthenticationMethodClient); ok {
var c AuthenticationMethodClient

if c, ok = client.(AuthenticationMethodClient); ok {
alg, method = resolver.GetAuthSigningAlg(c), resolver.GetAuthMethod(c)
}

Expand Down Expand Up @@ -278,13 +284,36 @@ func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionJWTBearer(c
}
}

func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionParseAssertionJWTBearer(ctx context.Context, client Client, assertion *ClientAssertion, resolver EndpointClientAuthHandler) (method, kid, alg string, token *xjwt.Token, claims *xjwt.RegisteredClaims, err error) {
func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionParseAssertionJWTBearer(ctx context.Context, client Client, assertion *ClientAssertion, resolver EndpointClientAuthHandler) (method, kid, alg string, token *jwt.Token, claims *jwt.MapClaims, err error) {
audience := s.Config.GetAllowedJWTAssertionAudiences(ctx)

if len(audience) == 0 {
return "", "", "", nil, nil, errorsx.WithStack(ErrMisconfiguration.WithHint("The authorization server does not support OAuth 2.0 JWT Profile Client Authentication RFC7523 or OpenID Connect 1.0 specific authentication methods.").WithDebug("The authorization server could not determine any safe value for it's audience but it's required to validate the RFC7523 client assertions."))
}

var (
c AuthenticationMethodClient
ok bool
)

if c, ok = client.(AuthenticationMethodClient); !ok {
return "", "", "", nil, nil, errorsx.WithStack(ErrInvalidRequest.WithHint("The registered client does not support OAuth 2.0 JWT Profile Client Authentication RFC7523 or OpenID Connect 1.0 specific authentication methods."))
}

if token, err = s.Config.GetJWTStrategy(ctx).Decode(ctx, assertion.Assertion, jwt.WithClient(&EndpointClientAuthJWTClient{client: c, handler: resolver})); err != nil {
panic(err)
}

optsClaims := []jwt.ClaimValidationOption{
jwt.ValidateAudienceAny(audience...), // Satisfies RFC7523 Section 3 Point 3.
jwt.ValidateRequireExpiresAt(), // Satisfies RFC7523 Section 3 Point 4.
jwt.ValidateTimeFunc(time.Now),
}

if err = token.Claims.Valid(optsClaims...); err != nil {
panic(err)
}

opts := []xjwt.ParserOption{
xjwt.WithStrictDecoding(),
//xjwt.WithAudience(tokenURI), // Satisfies RFC7523 Section 3 Point 3.
Expand All @@ -295,7 +324,7 @@ func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionParseAssert
// Automatically satisfies RFC7523 Section 3 Point 5, 8, 9, and 10.
parser := xjwt.NewParser(opts...)

claims = &xjwt.RegisteredClaims{}
claims = &jwt.MapClaims{}

if token, err = parser.ParseWithClaims(assertion.Assertion, claims, func(token *xjwt.Token) (key any, err error) {

Check failure on line 329 in client_authentication_strategy.go

View workflow job for this annotation

GitHub Actions / test

cannot use parser.ParseWithClaims(assertion.Assertion, claims, func(token *xjwt.Token) (key any, err error) {…}) (value of type *"github.com/golang-jwt/jwt/v5".Token) as *"authelia.com/provider/oauth2/token/jwt".Token value in assignment

Check failure on line 329 in client_authentication_strategy.go

View workflow job for this annotation

GitHub Actions / test

cannot use claims (variable of type *"authelia.com/provider/oauth2/token/jwt".MapClaims) as "github.com/golang-jwt/jwt/v5".Claims value in argument to parser.ParseWithClaims: *"authelia.com/provider/oauth2/token/jwt".MapClaims does not implement "github.com/golang-jwt/jwt/v5".Claims (wrong type for method GetAudience)
if subtle.ConstantTimeCompare([]byte(client.GetID()), []byte(claims.Subject)) == 0 {

Check failure on line 330 in client_authentication_strategy.go

View workflow job for this annotation

GitHub Actions / test

claims.Subject undefined (type *"authelia.com/provider/oauth2/token/jwt".MapClaims has no field or method Subject)
Expand All @@ -308,15 +337,6 @@ func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionParseAssert
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The claim 'sub' from the 'client_assertion' isn't defined."))
}

var (
c AuthenticationMethodClient
ok bool
)

if c, ok = client.(AuthenticationMethodClient); !ok {
return nil, errorsx.WithStack(ErrInvalidRequest.WithHint("The registered client does not support OAuth 2.0 JWT Profile Client Authentication RFC7523 or OpenID Connect 1.0 specific authentication methods."))
}

return s.doAuthenticateAssertionParseAssertionJWTBearerFindKey(ctx, token.Header, c, resolver)
}); err != nil {
return "", "", "", nil, nil, resolveJWTErrorToRFCError(err)
Expand All @@ -330,7 +350,7 @@ func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionParseAssert
return method, kid, alg, token, claims, nil
}

func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionJWTBearerClaimAudience(ctx context.Context, audience []string, claims *xjwt.RegisteredClaims) (err error) {
func (s *DefaultClientAuthenticationStrategy) doAuthenticateAssertionJWTBearerClaimAudience(ctx context.Context, audience []string, claims *jwt.MapClaims) (err error) {
if len(claims.Audience) == 0 {

Check failure on line 354 in client_authentication_strategy.go

View workflow job for this annotation

GitHub Actions / test

claims.Audience undefined (type *"authelia.com/provider/oauth2/token/jwt".MapClaims has no field or method Audience)
return errorsx.WithStack(
ErrInvalidClient.
Expand Down
36 changes: 25 additions & 11 deletions client_authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,13 +694,20 @@ func TestAuthenticateClient(t *testing.T) {

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &Config{
JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(),
AllowedJWTAssertionAudiences: []string{"token-url"},
HTTPClient: retryablehttp.NewClient(),
}

config.JWTStrategy = &jwt.DefaultStrategy{
Config: config,
Issuer: jwt.NewDefaultIssuerUnverifiedFromJWKS(jwksRSA),
}

provider := &Fosite{
Store: storage.NewMemoryStore(),
Config: &Config{
JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(),
AllowedJWTAssertionAudiences: []string{"token-url"},
HTTPClient: retryablehttp.NewClient(),
},
Store: storage.NewMemoryStore(),
Config: config,
}

var h http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -761,12 +768,19 @@ func TestAuthenticateClientTwice(t *testing.T) {
store := storage.NewMemoryStore()
store.Clients[client.ID] = client

config := &Config{
JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(),
AllowedJWTAssertionAudiences: []string{"token-url"},
}

config.JWTStrategy = &jwt.DefaultStrategy{
Config: config,
Issuer: jwt.NewDefaultIssuerRS256Unverified(key),
}

provider := &Fosite{
Store: store,
Config: &Config{
JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(),
AllowedJWTAssertionAudiences: []string{"token-url"},
},
Store: store,
Config: config,
}

formValues := url.Values{"client_id": []string{"bar"}, "client_assertion": {mustGenerateRSAAssertion(t, jwt.MapClaims{
Expand Down
1 change: 0 additions & 1 deletion compose/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ func ComposeAllEnabled(config *oauth2.Config, storage any, key any) oauth2.Provi
CoreStrategy: NewOAuth2HMACStrategy(config),
OpenIDConnectTokenStrategy: NewOpenIDConnectStrategy(keyGetter, strategy, config),
Strategy: strategy,
//Signer: &jwt.DefaultSigner{GetPrivateKey: keyGetter},
},
OAuth2AuthorizeExplicitFactory,
OAuth2AuthorizeImplicitFactory,
Expand Down
6 changes: 6 additions & 0 deletions config_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ func (c *Config) GetJWTSecuredAuthorizeResponseModeStrategy(ctx context.Context)
}

func (c *Config) GetJWTStrategy(ctx context.Context) jwt.Strategy {
if c.JWTStrategy == nil {
c.JWTStrategy = &jwt.DefaultStrategy{
Config: c,
}
}

return c.JWTStrategy
}

Expand Down
4 changes: 2 additions & 2 deletions token/jwt/claims_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ func (m MapClaims) Valid(opts ...ClaimValidationOption) (err error) {
var now int64

if vopts.timef != nil {
now = vopts.timef().Unix()
now = vopts.timef().UTC().Unix()
} else {
now = TimeFunc().Unix()
now = TimeFunc().UTC().Unix()
}

vErr := new(ValidationError)
Expand Down
33 changes: 32 additions & 1 deletion token/jwt/issuer.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"authelia.com/provider/oauth2/internal/consts"
)

// NewDefaultIssuer returns a new issuer and verifies that one RS256 key exists.
func NewDefaultIssuer(keys ...jose.JSONWebKey) (issuer *DefaultIssuer, err error) {
jwks := &jose.JSONWebKeySet{
Keys: make([]jose.JSONWebKey, len(keys)),
Expand All @@ -22,6 +23,10 @@ func NewDefaultIssuer(keys ...jose.JSONWebKey) (issuer *DefaultIssuer, err error
for i, key := range keys {
jwks.Keys[i] = key

if hasRS256 {
continue
}

if key.Use != consts.JSONWebTokenUseSignature {
continue
}
Expand All @@ -37,9 +42,31 @@ func NewDefaultIssuer(keys ...jose.JSONWebKey) (issuer *DefaultIssuer, err error
return nil, errors.New("no RS256 signature algorithm found")
}

return issuer, nil
return NewDefaultIssuerUnverifiedFromJWKS(jwks), nil
}

func NewDefaultIssuerFromJWKS(jwks *jose.JSONWebKeySet) (issuer *DefaultIssuer, err error) {
for _, key := range jwks.Keys {
if key.Use != consts.JSONWebTokenUseSignature {
continue
}

if key.Algorithm != string(jose.RS256) {
continue
}

return &DefaultIssuer{jwks: jwks}, nil
}

return nil, errors.New("no RS256 signature algorithm found")
}

// NewDefaultIssuerUnverifiedFromJWKS returns a new issuer from a jose.JSONWebKeySet without verification.
func NewDefaultIssuerUnverifiedFromJWKS(jwks *jose.JSONWebKeySet) (issuer *DefaultIssuer) {
return &DefaultIssuer{jwks: jwks}
}

// MustNewDefaultIssuerRS256 is the same as NewDefaultIssuerRS256 but it panics if an error occurs.
func MustNewDefaultIssuerRS256(key any) (issuer *DefaultIssuer) {
var err error

Expand All @@ -50,6 +77,7 @@ func MustNewDefaultIssuerRS256(key any) (issuer *DefaultIssuer) {
return issuer
}

// NewDefaultIssuerRS256 returns an issuer with a single key and returns an error if it's not an RSA2048 or higher key.
func NewDefaultIssuerRS256(key any) (issuer *DefaultIssuer, err error) {
switch k := key.(type) {
case *rsa.PrivateKey:
Expand All @@ -63,6 +91,7 @@ func NewDefaultIssuerRS256(key any) (issuer *DefaultIssuer, err error) {
}
}

// NewDefaultIssuerRS256Unverified returns an issuer with a single key asserting the type is an RSA key.
func NewDefaultIssuerRS256Unverified(key any) (issuer *DefaultIssuer) {
return &DefaultIssuer{
jwks: &jose.JSONWebKeySet{
Expand All @@ -78,6 +107,7 @@ func NewDefaultIssuerRS256Unverified(key any) (issuer *DefaultIssuer) {
}
}

// GenDefaultIssuer generates a *DefaultIssuer with a random RSA key.
func GenDefaultIssuer() (issuer *DefaultIssuer, err error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
Expand All @@ -87,6 +117,7 @@ func GenDefaultIssuer() (issuer *DefaultIssuer, err error) {
return NewDefaultIssuerRS256(key)
}

// MustGenDefaultIssuer is the same as GenDefaultIssuer but it panics on an error.
func MustGenDefaultIssuer() (issuer *DefaultIssuer) {
var err error

Expand Down
Loading

0 comments on commit b1f7e9b

Please sign in to comment.