Skip to content

Commit

Permalink
feat: request objects
Browse files Browse the repository at this point in the history
  • Loading branch information
james-d-elliott committed Aug 5, 2024
1 parent 95fe4f1 commit 921cead
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 67 deletions.
69 changes: 18 additions & 51 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"net/http"
"strings"

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

"authelia.com/provider/oauth2/i18n"
Expand Down Expand Up @@ -123,65 +122,33 @@ func (f *Fosite) authorizeRequestParametersFromOpenIDConnectRequestObject(ctx co
assertion = request.Form.Get(consts.FormParameterRequest)
}

token, err := jwt.ParseWithClaims(assertion, jwt.MapClaims{}, func(t *jwt.Token) (key any, err error) {
// request_object_signing_alg - OPTIONAL.
// JWS [JWS] alg algorithm [JWA] that MUST be used for signing Request Objects sent to the OP. All Request Objects from this Client MUST be rejected,
// if not signed with this algorithm. Request Objects are described in Section 6.1 of OpenID Connect Core 1.0 [OpenID.Core]. This algorithm MUST
// be used both when the Request Object is passed by value (using the request parameter) and when it is passed by reference (using the request_uri parameter).
// Servers SHOULD support RS256. The value none MAY be used. The default, if omitted, is that any algorithm supported by the OP and the RP MAY be used.
if !algAny && client.GetRequestObjectSigningAlg() != fmt.Sprintf("%s", t.Header[consts.JSONWebTokenHeaderAlgorithm]) {
return nil, errorsx.WithStack(ErrInvalidRequestObject.WithHintf(hintRequestObjectValidate, hintRequestObjectPrefix(openid)).WithDebugf("The OAuth 2.0 client with id '%s' expects request objects to be signed with the '%s' algorithm but the request object was signed with the '%s' algorithm.", request.GetClient().GetID(), client.GetRequestObjectSigningAlg(), t.Header[consts.JSONWebTokenHeaderAlgorithm]))
}

if t.SignatureAlgorithm == jwt.SigningMethodNone {
algNone = true
strategy := f.Config.GetJWTStrategy(ctx)

return jwt.UnsafeAllowNoneSignatureType, nil
} else if algNone {
return nil, errorsx.WithStack(ErrInvalidRequestObject.WithHintf(hintRequestObjectValidate, hintRequestObjectPrefix(openid)).WithDebugf("The OAuth 2.0 client with id '%s' expects request objects to be signed with the '%s' algorithm but the request object was signed with the '%s' algorithm.", request.GetClient().GetID(), client.GetRequestObjectSigningAlg(), t.Header[consts.JSONWebTokenHeaderAlgorithm]))
token, err := strategy.Decode(ctx, assertion, jwt.WithSigAlgorithm(jwt.SignatureAlgorithmsNone...), jwt.WithJARClient(client))
if err != nil {
var e *jwt.ValidationError
if errors.As(err, &e) {
return wrapSigningKeyFailure(ErrInvalidRequestObject.WithHintf("The OAuth 2.0 client with id '%s' could not validate the request object.", client.GetID()), err)
} else {
return errorsx.WithStack(ErrInvalidRequestObject.WithHintf("The OAuth 2.0 client with id '%s' could not validate the request object.", client.GetID()).WithDebugError(err))
}
}

switch t.SignatureAlgorithm {
case jose.RS256, jose.RS384, jose.RS512:
if key, err = f.findClientPublicJWK(ctx, client, t, true); err != nil {
return nil, wrapSigningKeyFailure(
ErrInvalidRequestObject.WithHint("Unable to retrieve RSA signing key from OAuth 2.0 Client."), err)
}

return key, nil
case jose.ES256, jose.ES384, jose.ES512:
if key, err = f.findClientPublicJWK(ctx, client, t, false); err != nil {
return nil, wrapSigningKeyFailure(
ErrInvalidRequestObject.WithHint("Unable to retrieve ECDSA signing key from OAuth 2.0 Client."), err)
}

return key, nil
case jose.PS256, jose.PS384, jose.PS512:
if key, err = f.findClientPublicJWK(ctx, client, t, true); err != nil {
return nil, wrapSigningKeyFailure(
ErrInvalidRequestObject.WithHint("Unable to retrieve RSA signing key from OAuth 2.0 Client."), err)
}
if algAny {
if token.SignatureAlgorithm == "none" {

return key, nil
default:
return nil, errorsx.WithStack(ErrInvalidRequestObject.WithHintf(hintRequestObjectValidate, hintRequestObjectPrefix(openid)).WithDebugf("The OAuth 2.0 client with id '%s' provided a request object that uses the unsupported signing algorithm '%s'.", request.GetClient().GetID(), t.Header[consts.JSONWebTokenHeaderAlgorithm]))
}
})

if err != nil {
// Do not re-process already enhanced errors
var e *jwt.ValidationError
if errors.As(err, &e) {
if e.Inner != nil {
return e.Inner
}
if kid := client.GetRequestObjectSigningKeyID(); kid != "" && kid != token.KeyID {

return errorsx.WithStack(ErrInvalidRequestObject.WithHintf(hintRequestObjectValidate, hintRequestObjectPrefix(openid)).WithDebugf("The OAuth 2.0 client with id '%s' provided a request object which failed to validate with error: %+v.", request.GetClient().GetID(), err).WithWrap(err))
}
} else if string(token.SignatureAlgorithm) != {

Check failure on line 145 in authorize_request_handler.go

View workflow job for this annotation

GitHub Actions / test

syntax error: unexpected {, expected expression
}

Check failure on line 146 in authorize_request_handler.go

View workflow job for this annotation

GitHub Actions / test

syntax error: unexpected }, expected {

return err
} else if err = token.Claims.Valid(); err != nil {
return errorsx.WithStack(ErrInvalidRequestObject.WithHintf(hintRequestObjectValidate, hintRequestObjectPrefix(openid)).WithDebugf("The OAuth 2.0 client with id '%s' provided a request object which could not be validated because its claims could not be validated with error: %+v.", request.GetClient().GetID(), err).WithWrap(err))
if token.SignatureAlgorithm == "none" && !algNone {

Check failure on line 148 in authorize_request_handler.go

View workflow job for this annotation

GitHub Actions / test

syntax error: unexpected if, expected expression
print("")
} else if !algAny && string(token.SignatureAlgorithm) != client.GetRequestObjectSigningAlg() {
print("")
}

claims := token.Claims
Expand Down
16 changes: 12 additions & 4 deletions authorize_request_handler_oidc_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ func TestAuthorizeRequestParametersFromOpenIDConnectRequestObject(t *testing.T)
jwks := &jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{
{
KeyID: "kid-foo",
Use: "sig",
Key: &key.PublicKey,
KeyID: "kid-foo",
Use: "sig",
Algorithm: string(jose.RS256),
Key: &key.PublicKey,
},
},
}
Expand Down Expand Up @@ -371,7 +372,14 @@ func TestAuthorizeRequestParametersFromOpenIDConnectRequestObject(t *testing.T)
},
}

provider := &Fosite{Config: &Config{JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(), IDTokenIssuer: "https://auth.example.com"}}
config := &Config{JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(), IDTokenIssuer: "https://auth.example.com"}

strategy := &jwt.DefaultStrategy{
Config: config,
Issuer: jwt.MustGenDefaultIssuer(),
}

provider := &Fosite{Config: &Config{JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(), IDTokenIssuer: "https://auth.example.com", JWTStrategy: strategy}}

err = provider.authorizeRequestParametersFromOpenIDConnectRequestObject(context.Background(), r, tc.par)
if tc.err != nil {
Expand Down
4 changes: 2 additions & 2 deletions token/jwt/jwt_strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func (j *DefaultStrategy) Encode(ctx context.Context, opts ...StrategyOpt) (toke
if keyEnc, err = NewJWKFromClientSecret(ctx, o.client, kid, alg, consts.JSONWebTokenUseEncryption); err != nil {
return "", "", errorsx.WithStack(fmt.Errorf("error occurred retrieving issuer jwk: error occurred retrieving the client secret: %w", err))
}
} else if keyEnc, err = FindClientPublicJWK(ctx, o.client, j.Config.GetJWKSFetcherStrategy(ctx), kid, alg, consts.JSONWebTokenUseEncryption); err != nil {
} else if keyEnc, err = FindClientPublicJWK(ctx, o.client, j.Config.GetJWKSFetcherStrategy(ctx), kid, alg, consts.JSONWebTokenUseEncryption, false); err != nil {
return "", "", errorsx.WithStack(fmt.Errorf("error occurred retrieving client jwk: %w", err))
}

Expand Down Expand Up @@ -179,7 +179,7 @@ func (j *DefaultStrategy) Decode(ctx context.Context, tokenString string, opts .
return nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorMalformed, Inner: fmt.Errorf("error validating the jws header: alg '%s' does not match the registered alg '%s'", alg, calg)})
}

if key, err = FindClientPublicJWK(ctx, o.client, j.Config.GetJWKSFetcherStrategy(ctx), kid, alg, consts.JSONWebTokenUseSignature); err != nil {
if key, err = FindClientPublicJWK(ctx, o.client, j.Config.GetJWKSFetcherStrategy(ctx), kid, alg, consts.JSONWebTokenUseSignature, true); err != nil {
return nil, errorsx.WithStack(&ValidationError{Errors: ValidationErrorUnverifiable, Inner: err})
}
} else if key, err = j.Issuer.GetIssuerStrictJWK(ctx, kid, alg, consts.JSONWebTokenUseSignature); err != nil {
Expand Down
3 changes: 3 additions & 0 deletions token/jwt/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ func ParseCustomWithClaims(tokenString string, claims MapClaims, keyFunc Keyfunc
// It provides method signatures compatible with jwt-go but implemented
// using go-json
type Token struct {
KeyID string
SignatureAlgorithm jose.SignatureAlgorithm // alg (JWS)
KeyAlgorithm jose.KeyAlgorithm // alg (JWE)
ContentEncryption jose.ContentEncryption // enc (JWE)
Expand Down Expand Up @@ -401,7 +402,9 @@ func newToken(parsedToken *jwt.JSONWebToken, claims MapClaims) (*Token, error) {
}
if h.KeyID != "" {
token.Header[consts.JSONWebTokenHeaderKeyIdentifier] = h.KeyID
token.KeyID = h.KeyID
}

for k, v := range h.ExtraHeaders {
token.Header[string(k)] = v
}
Expand Down
35 changes: 25 additions & 10 deletions token/jwt/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import (
"context"
"crypto"
"fmt"
"github.com/pkg/errors"
"regexp"
"strings"

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

"authelia.com/provider/oauth2/internal/consts"
)
Expand All @@ -18,24 +18,28 @@ var (
reEncryptedJWT = regexp.MustCompile(`^[-_A-Za-z0-9]+\.[-_A-Za-z0-9]+\.[-_A-Za-z0-9]+\.[-_A-Za-z0-9]+\.[-_A-Za-z0-9]+$`)
)

// IsSignedJWT returns true if a given token string meets the basic criteria of a compact serialized signed JWT.
func IsSignedJWT(tokenString string) (signed bool) {
return reSignedJWT.MatchString(tokenString)
}

// IsEncryptedJWT returns true if a given token string meets the basic criteria of a compact serialized encrypted JWT.
func IsEncryptedJWT(tokenString string) (encrypted bool) {
return reEncryptedJWT.MatchString(tokenString)
}

// IsEncryptedJWTClientSecretAlg returns true if a given alg string is a client secret based algorithm i.e. symmetric.
func IsEncryptedJWTClientSecretAlg(alg string) (csa bool) {
switch a := jose.KeyAlgorithm(alg); a {
case jose.A128KW, jose.A192KW, jose.A256KW, jose.DIRECT, jose.A128GCMKW, jose.A192GCMKW, jose.A256GCMKW:
return true
default:
return IsEncryptedJWTPBA(a)
return IsEncryptedJWTPasswordBasedAlg(a)
}
}

func IsEncryptedJWTPBA(alg jose.KeyAlgorithm) (pba bool) {
// IsEncryptedJWTPasswordBasedAlg returns true if a given jose.KeyAlgorithm is a Password Based Algorithm.
func IsEncryptedJWTPasswordBasedAlg(alg jose.KeyAlgorithm) (pba bool) {
switch alg {
case jose.PBES2_HS256_A128KW, jose.PBES2_HS384_A192KW, jose.PBES2_HS512_A256KW:
return true
Expand Down Expand Up @@ -108,7 +112,7 @@ func headerValidateJWE(header jose.Header) (kid, alg, enc, cty string, err error
ok bool
)

if IsEncryptedJWTPBA(jose.KeyAlgorithm(header.Algorithm)) {
if IsEncryptedJWTPasswordBasedAlg(jose.KeyAlgorithm(header.Algorithm)) {
if value, ok = header.ExtraHeaders[consts.JSONWebTokenHeaderPBES2Count]; ok {
switch p2c := value.(type) {
case float64:
Expand Down Expand Up @@ -181,42 +185,53 @@ func (e *JWKLookupError) Error() string {
return fmt.Sprintf("error occurrered looking up JSON web key: %s", e.Description)
}

func FindClientPublicJWK(ctx context.Context, client BaseClient, fetcher JWKSFetcherStrategy, kid, alg, use string) (key *jose.JSONWebKey, err error) {
// FindClientPublicJWK given a BaseClient, JWKSFetcherStrategy, and search parameters will return a *jose.JSONWebKey on
// a valid match. The *jose.JSONWebKey is guaranteed to match the alg and use values, and if strict is true it must
// match the kid value as well.
func FindClientPublicJWK(ctx context.Context, client BaseClient, fetcher JWKSFetcherStrategy, kid, alg, use string, strict bool) (key *jose.JSONWebKey, err error) {
if strict && kid == "" {
return nil, &JWKLookupError{Description: "The JSON Web Key strict search was attempted without a kid but the strict search doesn't permit this."}
}

var (
keys *jose.JSONWebKeySet
)

if keys = client.GetJSONWebKeys(); keys != nil {
return SearchJWKS(keys, kid, alg, use, true)
return SearchJWKS(keys, kid, alg, use, strict)
}

if location := client.GetJSONWebKeysURI(); len(location) > 0 {
if keys, err = fetcher.Resolve(ctx, location, false); err != nil {
return nil, err
}

if key, err = SearchJWKS(keys, kid, alg, use, true); err == nil {
if key, err = SearchJWKS(keys, kid, alg, use, strict); err == nil {
return key, nil
}

if keys, err = fetcher.Resolve(ctx, location, true); err != nil {
return nil, err
}

return SearchJWKS(keys, kid, alg, use, true)
return SearchJWKS(keys, kid, alg, use, strict)
}

return nil, ErrNotRegistered
return nil, &JWKLookupError{Description: "No JWKs have been registered for the client."}
}

func SearchJWKS(jwks *jose.JSONWebKeySet, kid, alg, use string, strict bool) (key *jose.JSONWebKey, err error) {
if len(jwks.Keys) == 0 {
return nil, &JWKLookupError{Description: "The retrieved JSON Web Key Set does not contain any key."}
}

if strict && kid == "" {
return nil, &JWKLookupError{Description: "The JSON Web Key strict search was attempted without a kid but the strict search doesn't permit this."}
}

var keys []jose.JSONWebKey

if kid == "" && !strict {
if kid == "" {
keys = jwks.Keys
} else {
keys = jwks.Key(kid)
Expand Down

0 comments on commit 921cead

Please sign in to comment.