Skip to content

Commit

Permalink
feat: [ory#631] Support for client_assertion JWE
Browse files Browse the repository at this point in the history
  • Loading branch information
vivshankar committed Aug 5, 2023
1 parent 73ba37d commit adca18b
Show file tree
Hide file tree
Showing 5 changed files with 457 additions and 72 deletions.
11 changes: 11 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ type OpenIDConnectClient interface {
GetTokenEndpointAuthSigningAlgorithm() string
}

// ClientWithAllowedVerificationKeys adds a security control to the client configuration to only allow
// specific verification keys. This ensures that a key that is valid for client X can't be used for client Y
// unless allowed. This becomes especially important for cases where the clients are controlled by third-parties
// and are issued specific keys from a central organization, which may be the OP's org or a central regulatory authority,
// and the security controls of the clients cannot be guaranteed.
type ClientWithAllowedVerificationKeys interface {
// AllowedVerificationKeys provides a list of key IDs that can be used in the JWT
// header for private_key_jwt authentication and for JWT bearer grant flow
AllowedVerificationKeys() []string
}

// ResponseModeClient represents a client capable of handling response_mode
type ResponseModeClient interface {
// GetResponseMode returns the response modes that client is allowed to send
Expand Down
129 changes: 63 additions & 66 deletions client_authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ import (
"crypto/ecdsa"
"crypto/rsa"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"

"github.com/ory/x/errorsx"

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

"github.com/ory/fosite/token/jwt"
)
Expand Down Expand Up @@ -54,7 +52,7 @@ func (f *Fosite) findClientPublicJWK(ctx context.Context, oidcClient OpenIDConne
}

// AuthenticateClient authenticates client requests using the configured strategy
// `Fosite.ClientAuthenticationStrategy`, if nil it uses `Fosite.DefaultClientAuthenticationStrategy`
// `ClientAuthenticationStrategy`, if nil it uses `DefaultClientAuthenticationStrategy`
func (f *Fosite) AuthenticateClient(ctx context.Context, r *http.Request, form url.Values) (Client, error) {
if s := f.Config.GetClientAuthenticationStrategy(ctx); s != nil {
return s(ctx, r, form)
Expand All @@ -71,81 +69,80 @@ func (f *Fosite) DefaultClientAuthenticationStrategy(ctx context.Context, r *htt
return nil, errorsx.WithStack(ErrInvalidRequest.WithHintf("The client_assertion request parameter must be set when using client_assertion_type of '%s'.", clientAssertionJWTBearerType))
}

var clientID string
var client Client

token, err := jwt.ParseWithClaims(assertion, jwt.MapClaims{}, func(t *jwt.Token) (interface{}, error) {
var err error
clientID, _, err = clientCredentialsFromRequestBody(form, false)
if err != nil {
return nil, err
// for backward compatibility
if f.JWTHelper == nil {
f.JWTHelper = &JWTHelper{
JWTStrategy: nil,
Config: f.Config,
}
}

if clientID == "" {
claims := t.Claims
if sub, ok := claims["sub"].(string); !ok {
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The claim 'sub' from the client_assertion JSON Web Token is undefined."))
} else {
clientID = sub
}
}
// Parse the assertion
token, parsedToken, isJWE, err := f.newToken(assertion, "client_assertion", ErrInvalidClient)
if err != nil {
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Unable to parse the client_assertion").WithWrap(err).WithDebug(err.Error()))
}

client, err = f.Store.GetClient(ctx, clientID)
if err != nil {
return nil, errorsx.WithStack(ErrInvalidClient.WithWrap(err).WithDebug(err.Error()))
}
claims := token.Claims

oidcClient, ok := client.(OpenIDConnectClient)
if !ok {
return nil, errorsx.WithStack(ErrInvalidRequest.WithHint("The server configuration does not support OpenID Connect specific authentication methods."))
}
// Validate client
clientID, _, err := clientCredentialsFromRequestBody(form, false)
if err != nil {
return nil, err
}

switch oidcClient.GetTokenEndpointAuthMethod() {
case "private_key_jwt":
break
case "none":
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("This requested OAuth 2.0 client does not support client authentication, however 'client_assertion' was provided in the request."))
case "client_secret_post":
fallthrough
case "client_secret_basic":
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however 'client_assertion' was provided in the request.", oidcClient.GetTokenEndpointAuthMethod()))
case "client_secret_jwt":
fallthrough
default:
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however that method is not supported by this server.", oidcClient.GetTokenEndpointAuthMethod()))
if clientID == "" {
if isJWE {
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The 'client_id' must be part of the request when encrypted client_assertion is used."))
}

if oidcClient.GetTokenEndpointAuthSigningAlgorithm() != fmt.Sprintf("%s", t.Header["alg"]) {
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("The 'client_assertion' uses signing algorithm '%s' but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", t.Header["alg"], oidcClient.GetTokenEndpointAuthSigningAlgorithm()))
if sub, ok := claims["sub"].(string); !ok {
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The claim 'sub' from the client_assertion JSON Web Token is undefined."))
} else {
clientID = sub
}
switch t.Method {
case jose.RS256, jose.RS384, jose.RS512:
return f.findClientPublicJWK(ctx, oidcClient, t, true)
case jose.ES256, jose.ES384, jose.ES512:
return f.findClientPublicJWK(ctx, oidcClient, t, false)
case jose.PS256, jose.PS384, jose.PS512:
return f.findClientPublicJWK(ctx, oidcClient, t, true)
case jose.HS256, jose.HS384, jose.HS512:
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("This authorization server does not support client authentication method 'client_secret_jwt'."))
default:
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("The 'client_assertion' request parameter uses unsupported signing algorithm '%s'.", t.Header["alg"]))
}
})
}

client, err := f.Store.GetClient(ctx, clientID)
if err != nil {
// Do not re-process already enhanced errors
var e *jwt.ValidationError
if errors.As(err, &e) {
if e.Inner != nil {
return nil, e.Inner
}
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Unable to verify the integrity of the 'client_assertion' value.").WithWrap(err).WithDebug(err.Error()))
}
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client could not be authenticated.").WithWrap(err).WithDebug(err.Error()))
}

oidcClient, ok := client.(OpenIDConnectClient)
if !ok {
return nil, errorsx.WithStack(ErrInvalidRequest.WithHint("The server configuration does not support OpenID Connect specific authentication methods."))
}

switch oidcClient.GetTokenEndpointAuthMethod() {
case "private_key_jwt":
break
case "none":
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("This requested OAuth 2.0 client does not support client authentication, however 'client_assertion' was provided in the request."))
case "client_secret_post":
fallthrough
case "client_secret_basic":
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however 'client_assertion' was provided in the request.", oidcClient.GetTokenEndpointAuthMethod()))
case "client_secret_jwt":
fallthrough
default:
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however that method is not supported by this server.", oidcClient.GetTokenEndpointAuthMethod()))
}

// Validate signature
if !isJWE && oidcClient.GetTokenEndpointAuthSigningAlgorithm() != "" && oidcClient.GetTokenEndpointAuthSigningAlgorithm() != parsedToken.Headers[0].Algorithm {
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("The client_assertion uses signing algorithm '%s', but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", parsedToken.Headers[0].Algorithm, oidcClient.GetTokenEndpointAuthSigningAlgorithm()))
}

if token, parsedToken, err = f.ValidateParsedAssertionWithClient(ctx, "client_assertion", assertion, token, parsedToken, oidcClient, false, ErrInvalidClient); err != nil {
return nil, err
} else if err := token.Claims.Valid(); err != nil {
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Unable to verify the request object because its claims could not be validated, check if the expiry time is set correctly.").WithWrap(err).WithDebug(err.Error()))
}

claims := token.Claims
if isJWE && oidcClient.GetTokenEndpointAuthSigningAlgorithm() != "" && oidcClient.GetTokenEndpointAuthSigningAlgorithm() != parsedToken.Headers[0].Algorithm {
return nil, errorsx.WithStack(ErrInvalidClient.WithHintf("The client_assertion uses signing algorithm '%s', but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", parsedToken.Headers[0].Algorithm, oidcClient.GetTokenEndpointAuthSigningAlgorithm()))
}

claims = token.Claims

var jti string
if !claims.VerifyIssuer(clientID, true) {
return nil, errorsx.WithStack(ErrInvalidClient.WithHint("Claim 'iss' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client."))
Expand Down
67 changes: 61 additions & 6 deletions client_authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,31 @@ import (
"github.com/ory/fosite/storage"
)

func encryptAssertionWithRSAKey(t *testing.T, token string, pubKey *rsa.PublicKey) string {
eo := &jose.EncrypterOptions{}
eo = eo.WithContentType("JWT").WithType("JWT")
enc, err := jose.NewEncrypter(
jose.ContentEncryption("A256GCM"),
jose.Recipient{
Algorithm: jose.KeyAlgorithm("RSA-OAEP"),
Key: pubKey,
KeyID: "enc_key",
},
eo)

require.NoError(t, err, "unable to build encrypter; err=%v", err)

// Encrypt the token
o, err := enc.Encrypt([]byte(token))
require.NoError(t, err, "encrypting the token failed. err=%v", err)

// Serialize the encrypted token
token, err = o.CompactSerialize()
require.NoError(t, err, "serializing the encrypted token failed. err=%v", err)

return token
}

func mustGenerateRSAAssertion(t *testing.T, claims jwt.MapClaims, key *rsa.PrivateKey, kid string) string {
token := jwt.NewWithClaims(jose.RS256, claims)
token.Header["kid"] = kid
Expand Down Expand Up @@ -75,13 +100,23 @@ func TestAuthenticateClient(t *testing.T) {
const at = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"

hasher := &BCrypt{Config: &Config{HashCost: 6}}
encKey := gen.MustRSAKey()

config := &Config{
JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(),
ClientSecretsHasher: hasher,
TokenURL: "token-url",
HTTPClient: retryablehttp.NewClient(),
}

f := &Fosite{
Store: storage.NewMemoryStore(),
Config: &Config{
JWKSFetcherStrategy: NewDefaultJWKSFetcherStrategy(),
ClientSecretsHasher: hasher,
TokenURL: "token-url",
HTTPClient: retryablehttp.NewClient(),
Store: storage.NewMemoryStore(),
Config: config,
JWTHelper: &JWTHelper{
Config: config,
JWTStrategy: jwt.NewDefaultStrategy(func(ctx context.Context, context *jwt.KeyContext) (interface{}, error) {
return encKey, nil
}),
},
}

Expand Down Expand Up @@ -300,6 +335,26 @@ func TestAuthenticateClient(t *testing.T) {
}, rsaKey, "kid-foo")}, "client_assertion_type": []string{at}},
r: new(http.Request),
},
{
d: "should pass with proper encrypted RSA assertion when JWKs are set within the client and client_id is set in the request",
client: &DefaultOpenIDConnectClient{DefaultClient: &DefaultClient{ID: "bar", Secret: barSecret}, JSONWebKeys: rsaJwks, TokenEndpointAuthMethod: "private_key_jwt"},
form: url.Values{
"client_id": []string{"bar"},
"client_assertion": {
encryptAssertionWithRSAKey(t,
mustGenerateRSAAssertion(t, jwt.MapClaims{
"sub": "bar",
"exp": time.Now().Add(time.Hour).Unix(),
"iss": "bar",
"jti": "12345",
"aud": "token-url",
}, rsaKey, "kid-foo"),
&encKey.PublicKey),
},
"client_assertion_type": []string{at},
},
r: new(http.Request),
},
{
d: "should pass with proper ECDSA assertion when JWKs are set within the client and client_id is not set in the request",
client: &DefaultOpenIDConnectClient{DefaultClient: &DefaultClient{ID: "bar", Secret: barSecret}, JSONWebKeys: ecdsaJwks, TokenEndpointAuthMethod: "private_key_jwt", TokenEndpointAuthSigningAlgorithm: "ES256"},
Expand Down
2 changes: 2 additions & 0 deletions fosite.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ type Fosite struct {
Store Storage

Config Configurator

*JWTHelper
}

// GetMinParameterEntropy returns MinParameterEntropy if set. Defaults to fosite.MinParameterEntropy.
Expand Down
Loading

0 comments on commit adca18b

Please sign in to comment.