From 7cf907b23df52130c1d54d3ad1b183d8b505d672 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sat, 15 Apr 2023 20:55:38 +1000 Subject: [PATCH] feat(oidc): client_secret_jwt authentication This adds the authentication machinery for the client_secret_jwt Default Client Authentication Strategy. Signed-off-by: James Elliott --- config.template.yml | 4 + .../identity-providers/open-id-connect.md | 9 +- .../openid-connect/introduction.md | 2 +- internal/configuration/config.template.yml | 4 + .../schema/identity_providers.go | 3 +- internal/configuration/schema/keys.go | 1 + internal/configuration/validator/const.go | 14 +- .../validator/identity_providers.go | 29 +- .../validator/identity_providers_test.go | 185 +- internal/handlers/handler_oidc_token_test.go | 1512 +++++++++++++++++ internal/handlers/handler_oidc_userinfo.go | 4 +- internal/oidc/client.go | 9 +- internal/oidc/client_auth.go | 331 ++++ internal/oidc/client_test.go | 14 +- internal/oidc/const.go | 40 +- internal/oidc/const_test.go | 62 +- internal/oidc/discovery.go | 12 +- internal/oidc/errors.go | 2 +- internal/oidc/keys.go | 8 +- internal/oidc/keys_test.go | 2 +- internal/oidc/provider.go | 1 + internal/oidc/provider_test.go | 45 +- internal/oidc/store_test.go | 10 +- internal/oidc/types.go | 12 + internal/oidc/types_test.go | 8 - 25 files changed, 2170 insertions(+), 153 deletions(-) create mode 100644 internal/handlers/handler_oidc_token_test.go create mode 100644 internal/oidc/client_auth.go diff --git a/config.template.yml b/config.template.yml index 2319109de..69270a5d0 100644 --- a/config.template.yml +++ b/config.template.yml @@ -1517,6 +1517,10 @@ notifier: ## The permitted client authentication method for the Token Endpoint for this client. # token_endpoint_auth_method: 'client_secret_basic' + ## The permitted client authentication signing algorithm for the Token Endpoint for this client when using + ## the 'client_secret_jwt' token_endpoint_auth_method. + # token_endpoint_auth_signing_alg: HS256 + ## The policy to require for this client; one_factor or two_factor. # authorization_policy: 'two_factor' diff --git a/docs/content/en/configuration/identity-providers/open-id-connect.md b/docs/content/en/configuration/identity-providers/open-id-connect.md index 0b1afe399..6ac8d1249 100644 --- a/docs/content/en/configuration/identity-providers/open-id-connect.md +++ b/docs/content/en/configuration/identity-providers/open-id-connect.md @@ -555,11 +555,18 @@ more information. The registered client authentication mechanism used by this client for the [Token Endpoint]. If no method is defined the confidential client type will accept any supported method. The public client type defaults to `none` as this is required by the specification. This may be required as a breaking change in future versions. -Supported values are `client_secret_basic`, `client_secret_post`, and `none`. +Supported values are `client_secret_basic`, `client_secret_post`, `client_secret_jwt`, and `none`. See the [integration guide](../../integration/openid-connect/introduction.md#client-authentication-method) for more information. +#### token_endpoint_auth_signing_alg + +{{< confkey type="string" default="HS256" required="no" >}} + +The JWT signing algorithm accepted when the [token_endpoint_auth_method](#tokenendpointauthmethod) is configured as +`client_secret_jwt`. Supported values are `HS256`, `HS385`, and `HS512`. + #### consent_mode {{< confkey type="string" default="auto" required="no" >}} diff --git a/docs/content/en/integration/openid-connect/introduction.md b/docs/content/en/integration/openid-connect/introduction.md index 37cca969d..043124f99 100644 --- a/docs/content/en/integration/openid-connect/introduction.md +++ b/docs/content/en/integration/openid-connect/introduction.md @@ -174,7 +174,7 @@ specification and the [OAuth 2.0 - Client Types] specification for more informat |:------------------------------------:|:-----------------------------:|:----------------------:|:-----------------------:|:--------------------------------------------------------:| | Secret via HTTP Basic Auth Scheme | `client_secret_basic` | `confidential` | N/A | N/A | | Secret via HTTP POST Body | `client_secret_post` | `confidential` | N/A | N/A | -| JWT (signed by secret) | `client_secret_jwt` | Not Supported | N/A | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` | +| JWT (signed by secret) | `client_secret_jwt` | `confidential` | N/A | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` | | JWT (signed by private key) | `private_key_jwt` | Not Supported | N/A | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` | | [OAuth 2.0 Mutual-TLS] | `tls_client_auth` | Not Supported | N/A | N/A | | [OAuth 2.0 Mutual-TLS] (Self Signed) | `self_signed_tls_client_auth` | Not Supported | N/A | N/A | diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 2319109de..69270a5d0 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -1517,6 +1517,10 @@ notifier: ## The permitted client authentication method for the Token Endpoint for this client. # token_endpoint_auth_method: 'client_secret_basic' + ## The permitted client authentication signing algorithm for the Token Endpoint for this client when using + ## the 'client_secret_jwt' token_endpoint_auth_method. + # token_endpoint_auth_signing_alg: HS256 + ## The policy to require for this client; one_factor or two_factor. # authorization_policy: 'two_factor' diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index d253a4d07..a89f923ad 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -64,7 +64,8 @@ type OpenIDConnectClientConfiguration struct { ResponseTypes []string `koanf:"response_types"` ResponseModes []string `koanf:"response_modes"` - TokenEndpointAuthMethod string `koanf:"token_endpoint_auth_method"` + TokenEndpointAuthMethod string `koanf:"token_endpoint_auth_method"` + TokenEndpointAuthSigningAlg string `koanf:"token_endpoint_auth_signing_alg"` Policy string `koanf:"authorization_policy"` diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index 6bbddf150..ec7947e1a 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -46,6 +46,7 @@ var Keys = []string{ "identity_providers.oidc.clients[].response_types", "identity_providers.oidc.clients[].response_modes", "identity_providers.oidc.clients[].token_endpoint_auth_method", + "identity_providers.oidc.clients[].token_endpoint_auth_signing_alg", "identity_providers.oidc.clients[].authorization_policy", "identity_providers.oidc.clients[].enforce_par", "identity_providers.oidc.clients[].enforce_pkce", diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 63be6a4fc..f9933e372 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -161,9 +161,10 @@ const ( errFmtOIDCClientsWithEmptyID = "identity_providers: oidc: clients: option 'id' is required but was absent on the clients in positions %s" errFmtOIDCClientsDeprecated = "identity_providers: oidc: clients: warnings for clients above indicate deprecated functionality and it's strongly suggested these issues are checked and fixed if they're legitimate issues or reported if they are not as in a future version these warnings will become errors" - errFmtOIDCClientInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is required" - errFmtOIDCClientInvalidSecretPlainText = "identity_providers: oidc: client '%s': option 'secret' is plaintext but it should be a hashed value as plaintext values are deprecated and will be removed when oidc becomes stable" - errFmtOIDCClientPublicInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is " + + errFmtOIDCClientInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is required" + errFmtOIDCClientInvalidSecretPlainText = "identity_providers: oidc: client '%s': option 'secret' is plaintext but for clients not using the 'token_endpoint_auth_method' of 'client_secret_jwt' it should be a hashed value as plaintext values are deprecated with the exception of 'client_secret_jwt' and will be removed when oidc becomes stable" + errFmtOIDCClientInvalidSecretNotPlainText = "identity_providers: oidc: client '%s': option 'secret' must be plaintext with option 'token_endpoint_auth_method' with a value of 'client_secret_jwt'" + errFmtOIDCClientPublicInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is " + "required to be empty when option 'public' is true" errFmtOIDCClientRedirectURICantBeParsed = "identity_providers: oidc: client '%s': option 'redirect_uris' has an " + "invalid value: redirect uri '%s' could not be parsed: %v" @@ -183,6 +184,8 @@ const ( "'token_endpoint_auth_method' must be one of %s when configured as the confidential client type unless it only includes implicit flow response types such as %s but it's configured as '%s'" errFmtOIDCClientInvalidTokenEndpointAuthMethodPublic = "identity_providers: oidc: client '%s': option " + "'token_endpoint_auth_method' must be 'none' when configured as the public client type but it's configured as '%s'" + errFmtOIDCClientInvalidTokenEndpointAuthSigAlg = "identity_providers: oidc: client '%s': option " + + "'token_endpoint_auth_signing_alg' must be %s when option 'token_endpoint_auth_method' is %s" errFmtOIDCClientInvalidSectorIdentifier = "identity_providers: oidc: client '%s': option " + "'sector_identifier' with value '%s': must be a URL with only the host component for example '%s' but it has a %s with the value '%s'" errFmtOIDCClientInvalidSectorIdentifierWithoutValue = "identity_providers: oidc: client '%s': option " + @@ -413,7 +416,7 @@ var ( validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointPushedAuthorizationRequest, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo} validOIDCClientScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, oidc.ScopeOfflineAccess} - validOIDCClientUserinfoAlgorithms = []string{oidc.SigningAlgorithmNone, oidc.SigningAlgorithmRSAWithSHA256} + validOIDCClientUserinfoAlgorithms = []string{oidc.SigningAlgNone, oidc.SigningAlgRSAUsingSHA256} validOIDCClientConsentModes = []string{auto, oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()} validOIDCClientResponseModes = []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment} validOIDCClientResponseTypes = []string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth} @@ -422,8 +425,9 @@ var ( validOIDCClientResponseTypesRefreshToken = []string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth} validOIDCClientGrantTypes = []string{oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeAuthorizationCode} - validOIDCClientTokenEndpointAuthMethods = []string{oidc.ClientAuthMethodNone, oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic} + validOIDCClientTokenEndpointAuthMethods = []string{oidc.ClientAuthMethodNone, oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodClientSecretJWT} validOIDCClientTokenEndpointAuthMethodsConfidential = []string{oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic} + validOIDCClientTokenEndpointAuthSigAlgs = []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512} ) var ( diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index cb61e31db..21ba0aab1 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -193,8 +193,13 @@ func validateOIDCClient(c int, config *schema.OpenIDConnectConfiguration, val *s } else { if config.Clients[c].Secret == nil { val.Push(fmt.Errorf(errFmtOIDCClientInvalidSecret, config.Clients[c].ID)) - } else if config.Clients[c].Secret.IsPlainText() { - val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidSecretPlainText, config.Clients[c].ID)) + } else { + switch { + case config.Clients[c].Secret.IsPlainText() && config.Clients[c].TokenEndpointAuthMethod != oidc.ClientAuthMethodClientSecretJWT: + val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidSecretPlainText, config.Clients[c].ID)) + case !config.Clients[c].Secret.IsPlainText() && config.Clients[c].TokenEndpointAuthMethod == oidc.ClientAuthMethodClientSecretJWT: + val.Push(fmt.Errorf(errFmtOIDCClientInvalidSecretNotPlainText, config.Clients[c].ID)) + } } } @@ -222,7 +227,7 @@ func validateOIDCClient(c int, config *schema.OpenIDConnectConfiguration, val *s validateOIDCClientGrantTypes(c, config, val, errDeprecatedFunc) validateOIDCClientRedirectURIs(c, config, val, errDeprecatedFunc) - validateOIDCClientTokenEndpointAuthMethod(c, config, val) + validateOIDCClientTokenEndpointAuth(c, config, val) validateOIDDClientUserinfoAlgorithm(c, config, val) validateOIDCClientSectorIdentifier(c, config, val) @@ -481,13 +486,9 @@ func validateOIDCClientRedirectURIs(c int, config *schema.OpenIDConnectConfigura } } -func validateOIDCClientTokenEndpointAuthMethod(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { +func validateOIDCClientTokenEndpointAuth(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { implcit := len(config.Clients[c].ResponseTypes) != 0 && utils.IsStringSliceContainsAll(config.Clients[c].ResponseTypes, validOIDCClientResponseTypesImplicitFlow) - if config.Clients[c].TokenEndpointAuthMethod == "" && (config.Clients[c].Public || implcit) { - config.Clients[c].TokenEndpointAuthMethod = oidc.ClientAuthMethodNone - } - switch { case config.Clients[c].TokenEndpointAuthMethod == "": break @@ -501,6 +502,18 @@ func validateOIDCClientTokenEndpointAuthMethod(c int, config *schema.OpenIDConne val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthMethodPublic, config.Clients[c].ID, config.Clients[c].TokenEndpointAuthMethod)) } + + switch config.Clients[c].TokenEndpointAuthMethod { + case "": + break + case oidc.ClientAuthMethodClientSecretJWT: + switch { + case config.Clients[c].TokenEndpointAuthSigningAlg == "": + config.Clients[c].TokenEndpointAuthSigningAlg = oidc.SigningAlgHMACUsingSHA256 + case !utils.IsStringInSlice(config.Clients[c].TokenEndpointAuthSigningAlg, validOIDCClientTokenEndpointAuthSigAlgs): + val.Push(fmt.Errorf(errFmtOIDCClientInvalidTokenEndpointAuthSigAlg, config.Clients[c].ID, strJoinOr(validOIDCClientTokenEndpointAuthSigAlgs), config.Clients[c].TokenEndpointAuthMethod)) + } + } } func validateOIDDClientUserinfoAlgorithm(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index bdb11f3d0..e00b76141 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -758,7 +758,7 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnPlainTextClients(t *testin assert.Len(t, validator.Errors(), 0) require.Len(t, validator.Warnings(), 1) - assert.EqualError(t, validator.Warnings()[0], "identity_providers: oidc: client 'client-with-invalid-secret_standard': option 'secret' is plaintext but it should be a hashed value as plaintext values are deprecated and will be removed when oidc becomes stable") + assert.EqualError(t, validator.Warnings()[0], "identity_providers: oidc: client 'client-with-invalid-secret_standard': option 'secret' is plaintext but for clients not using the 'token_endpoint_auth_method' of 'client_secret_jwt' it should be a hashed value as plaintext values are deprecated with the exception of 'client_secret_jwt' and will be removed when oidc becomes stable") } // All valid schemes are supported as defined in https://datatracker.ietf.org/doc/html/rfc8252#section-7.1 @@ -1445,51 +1445,6 @@ func TestValidateOIDCClients(t *testing.T) { nil, nil, }, - { - "ShouldSetDefaultTokenEndpointClientAuthMethodPublicClientType", - func(have *schema.OpenIDConnectConfiguration) { - have.Clients[0].Public = true - have.Clients[0].Secret = nil - }, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, oidc.ClientAuthMethodNone, have.Clients[0].TokenEndpointAuthMethod) - }, - tcv{ - nil, - nil, - nil, - nil, - }, - tcv{ - []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, - []string{oidc.ResponseTypeAuthorizationCodeFlow}, - []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, - []string{oidc.GrantTypeAuthorizationCode}, - }, - nil, - nil, - }, - { - "ShouldSetDefaultTokenEndpointClientAuthMethodConfidentialClientTypeImplicitFlow", - nil, - func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, oidc.ClientAuthMethodNone, have.Clients[0].TokenEndpointAuthMethod) - }, - tcv{ - nil, - []string{oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth}, - nil, - nil, - }, - tcv{ - []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, - []string{oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth}, - []string{oidc.ResponseModeFormPost, oidc.ResponseModeFragment}, - []string{oidc.GrantTypeImplicit}, - }, - nil, - nil, - }, { "ShouldNotOverrideValidClientAuthMethod", func(have *schema.OpenIDConnectConfiguration) { @@ -1535,7 +1490,7 @@ func TestValidateOIDCClients(t *testing.T) { }, nil, []string{ - "identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'none', 'client_secret_post', or 'client_secret_basic' but it's configured as 'client_credentials'", + "identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'none', 'client_secret_post', 'client_secret_basic', or 'client_secret_jwt' but it's configured as 'client_credentials'", }, }, { @@ -1619,7 +1574,7 @@ func TestValidateOIDCClients(t *testing.T) { "ShouldSetDefaultUserInfoAlg", nil, func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, oidc.SigningAlgorithmNone, have.Clients[0].UserinfoSigningAlgorithm) + assert.Equal(t, oidc.SigningAlgNone, have.Clients[0].UserinfoSigningAlgorithm) }, tcv{ nil, @@ -1639,10 +1594,10 @@ func TestValidateOIDCClients(t *testing.T) { { "ShouldNotOverrideUserInfoAlg", func(have *schema.OpenIDConnectConfiguration) { - have.Clients[0].UserinfoSigningAlgorithm = oidc.SigningAlgorithmRSAWithSHA256 + have.Clients[0].UserinfoSigningAlgorithm = oidc.SigningAlgRSAUsingSHA256 }, func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, oidc.SigningAlgorithmRSAWithSHA256, have.Clients[0].UserinfoSigningAlgorithm) + assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, have.Clients[0].UserinfoSigningAlgorithm) }, tcv{ nil, @@ -1827,6 +1782,131 @@ func TestValidateOIDCClients(t *testing.T) { nil, nil, }, + { + "ShouldRaiseErrorOnIncorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", + func(have *schema.OpenIDConnectConfiguration) { + have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT + have.Clients[0].Secret = MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng") + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + []string{ + "identity_providers: oidc: client 'test': option 'secret' must be plaintext with option 'token_endpoint_auth_method' with a value of 'client_secret_jwt'", + }, + }, + { + "ShouldNotRaiseWarningOrErrorOnCorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", + func(have *schema.OpenIDConnectConfiguration) { + have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT + have.Clients[0].Secret = MustDecodeSecret("$plaintext$abc123") + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + nil, + }, + { + "ShouldSetDefaultTokenEndpointAuthSigAlg", + func(have *schema.OpenIDConnectConfiguration) { + have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT + have.Clients[0].Secret = MustDecodeSecret("$plaintext$abc123") + }, + func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + assert.Equal(t, oidc.SigningAlgHMACUsingSHA256, have.Clients[0].TokenEndpointAuthSigningAlg) + }, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + nil, + }, + { + "ShouldRaiseErrorOnInvalidPublicTokenAuthAlg", + func(have *schema.OpenIDConnectConfiguration) { + have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT + have.Clients[0].TokenEndpointAuthSigningAlg = oidc.SigningAlgHMACUsingSHA256 + have.Clients[0].Secret = nil + have.Clients[0].Public = true + }, + func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + assert.Equal(t, oidc.SigningAlgHMACUsingSHA256, have.Clients[0].TokenEndpointAuthSigningAlg) + }, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + []string{ + "identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be 'none' when configured as the public client type but it's configured as 'client_secret_jwt'", + }, + }, + { + "ShouldRaiseErrorOnInvalidTokenAuthAlgClientTypeConfidential", + func(have *schema.OpenIDConnectConfiguration) { + have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT + have.Clients[0].TokenEndpointAuthSigningAlg = "abcinvalid" + have.Clients[0].Secret = MustDecodeSecret("$plaintext$abc123") + }, + func(t *testing.T, have *schema.OpenIDConnectConfiguration) { + assert.Equal(t, "abcinvalid", have.Clients[0].TokenEndpointAuthSigningAlg) + }, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + []string{ + "identity_providers: oidc: client 'test': option 'token_endpoint_auth_signing_alg' must be 'HS256', 'HS384', or 'HS512' when option 'token_endpoint_auth_method' is client_secret_jwt", + }, + }, } errDeprecatedFunc := func() {} @@ -1891,10 +1971,9 @@ func TestValidateOIDCClientTokenEndpointAuthMethod(t *testing.T) { errs []string }{ {"ShouldSetDefaultValueConfidential", "", false, "", nil}, - {"ShouldSetDefaultValuePublic", "", true, oidc.ClientAuthMethodNone, nil}, {"ShouldErrorOnInvalidValue", "abc", false, "abc", []string{ - "identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'none', 'client_secret_post', or 'client_secret_basic' but it's configured as 'abc'", + "identity_providers: oidc: client 'test': option 'token_endpoint_auth_method' must be one of 'none', 'client_secret_post', 'client_secret_basic', or 'client_secret_jwt' but it's configured as 'abc'", }, }, {"ShouldErrorOnInvalidValueForPublicClient", "client_secret_post", true, "client_secret_post", @@ -1923,7 +2002,7 @@ func TestValidateOIDCClientTokenEndpointAuthMethod(t *testing.T) { val := schema.NewStructValidator() - validateOIDCClientTokenEndpointAuthMethod(0, have, val) + validateOIDCClientTokenEndpointAuth(0, have, val) assert.Equal(t, tc.expected, have.Clients[0].TokenEndpointAuthMethod) assert.Len(t, val.Warnings(), 0) diff --git a/internal/handlers/handler_oidc_token_test.go b/internal/handlers/handler_oidc_token_test.go new file mode 100644 index 000000000..52aacbbd9 --- /dev/null +++ b/internal/handlers/handler_oidc_token_test.go @@ -0,0 +1,1512 @@ +package handlers + +import ( + "context" + "crypto/ecdsa" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "database/sql" + "encoding/base64" + "encoding/pem" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "testing" + "time" + + "github.com/golang-jwt/jwt/v4" + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/ory/fosite" + "github.com/stretchr/testify/suite" + "github.com/valyala/fasthttp" + + "github.com/authelia/authelia/v4/internal/authorization" + "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/mocks" + "github.com/authelia/authelia/v4/internal/model" + "github.com/authelia/authelia/v4/internal/oidc" +) + +func TestClientAuthenticationStrategySuite(t *testing.T) { + suite.Run(t, &ClientAuthenticationStrategySuite{}) +} + +type ClientAuthenticationStrategySuite struct { + suite.Suite + + issuerURL *url.URL + + ctrl *gomock.Controller + store *mocks.MockStorage + provider *oidc.OpenIDConnectProvider +} + +func (s *ClientAuthenticationStrategySuite) GetIssuerURL() *url.URL { + if s.issuerURL == nil { + s.issuerURL = MustParseRequestURI("https://auth.example.com") + } + + return s.issuerURL +} + +func (s *ClientAuthenticationStrategySuite) GetTokenURL() *url.URL { + return s.GetIssuerURL().JoinPath(oidc.EndpointPathToken) +} + +func (s *ClientAuthenticationStrategySuite) GetBaseRequest(body io.Reader) (r *http.Request) { + var err error + + r, err = http.NewRequest(http.MethodPost, s.GetTokenURL().String(), body) + + s.Require().NoError(err) + s.Require().NotNil(r) + + r.Header.Set(fasthttp.HeaderContentType, "application/x-www-form-urlencoded") + + return r +} + +func (s *ClientAuthenticationStrategySuite) GetRequest(values *url.Values) (r *http.Request) { + var body io.Reader + + if values != nil { + body = strings.NewReader(values.Encode()) + } + + r = s.GetBaseRequest(body) + + s.Require().NoError(r.ParseForm()) + + return r +} + +func (s *ClientAuthenticationStrategySuite) GetAssertionValues(token string) *url.Values { + values := &url.Values{} + + values.Set(oidc.FormParameterClientAssertionType, oidc.ClientAssertionJWTBearerType) + + if token != "" { + values.Set(oidc.FormParameterClientAssertion, token) + } + + return values +} + +func (s *ClientAuthenticationStrategySuite) GetClientValues(id string) *url.Values { + values := &url.Values{} + + values.Set(oidc.FormParameterClientID, id) + + return values +} + +func (s *ClientAuthenticationStrategySuite) GetClientValuesPost(id, secret string) *url.Values { + values := s.GetClientValues(id) + + values.Set(oidc.FormParameterClientSecret, secret) + + return values +} + +func (s *ClientAuthenticationStrategySuite) GetClientSecretBasicRequest(id, secret string) (r *http.Request) { + values := s.GetClientValues(id) + + r = s.GetRequest(values) + + r.SetBasicAuth(id, secret) + + return r +} + +func (s *ClientAuthenticationStrategySuite) GetClientSecretPostRequest(id, secret string) (r *http.Request) { + values := s.GetClientValuesPost(id, secret) + + return s.GetRequest(values) +} + +func (s *ClientAuthenticationStrategySuite) GetAssertionRequest(token string) (r *http.Request) { + values := s.GetAssertionValues(token) + + return s.GetRequest(values) +} + +func (s *ClientAuthenticationStrategySuite) GetCtx() oidc.OpenIDConnectContext { + return &oidc.MockOpenIDConnectContext{ + Context: context.Background(), + MockIssuerURL: s.GetIssuerURL(), + } +} + +func (s *ClientAuthenticationStrategySuite) SetupTest() { + s.ctrl = gomock.NewController(s.T()) + s.store = mocks.NewMockStorage(s.ctrl) + + var err error + + secret := MustDecodeSecret("$plaintext$client-secret") + + s.provider, err = oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: MustParseRSAPrivateKey(exampleRSAPrivateKey), + HMACSecret: "abc123", + Clients: []schema.OpenIDConnectClientConfiguration{ + { + ID: "hs256", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgHMACUsingSHA256, + }, + { + ID: "hs384", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgHMACUsingSHA384, + }, + { + ID: "hs512", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgHMACUsingSHA512, + }, + { + ID: "rs256", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgRSAUsingSHA256, + }, + { + ID: "rs384", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgRSAUsingSHA384, + }, + { + ID: "rs512", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgRSAUsingSHA512, + }, + { + ID: "ps256", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgRSAPSSUsingSHA256, + }, + { + ID: "ps384", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgRSAPSSUsingSHA384, + }, + { + ID: "ps512", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgRSAPSSUsingSHA512, + }, + { + ID: "es256", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgECDSAUsingP256AndSHA256, + }, + { + ID: "es384", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgECDSAUsingP384AndSHA384, + }, + { + ID: "es512", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgECDSAUsingP521AndSHA512, + }, + { + ID: "hs5122", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgHMACUsingSHA512, + }, + { + ID: "hashed", + Secret: MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng"), + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretJWT, + TokenEndpointAuthSigningAlg: oidc.SigningAlgHMACUsingSHA512, + }, + { + ID: oidc.ClientAuthMethodClientSecretBasic, + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretBasic, + TokenEndpointAuthSigningAlg: oidc.SigningAlgNone, + }, + { + ID: oidc.ClientAuthMethodNone, + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodNone, + TokenEndpointAuthSigningAlg: oidc.SigningAlgNone, + }, + { + ID: oidc.ClientAuthMethodClientSecretPost, + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, + TokenEndpointAuthSigningAlg: oidc.SigningAlgNone, + }, + { + ID: "bad_method", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + TokenEndpointAuthMethod: "bad_method", + TokenEndpointAuthSigningAlg: oidc.SigningAlgNone, + }, + { + ID: "base", + Secret: secret, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + }, + { + ID: "public", + Public: true, + Policy: authorization.OneFactor.String(), + TokenEndpointAuthMethod: oidc.ClientAuthMethodNone, + RedirectURIs: []string{ + "https://client.example.com", + }, + }, + { + ID: "public-nomethod", + Public: true, + Policy: authorization.OneFactor.String(), + RedirectURIs: []string{ + "https://client.example.com", + }, + }, + { + ID: "public-basic", + Public: true, + Policy: authorization.OneFactor.String(), + TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretBasic, + RedirectURIs: []string{ + "https://client.example.com", + }, + }, + }, + }, s.store, nil) + + s.Require().NoError(err) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldValidateAssertionHS256() { + assertion := NewAssertion("hs256", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS256, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + sig := fmt.Sprintf("%x", sha256.Sum256([]byte(assertion.ID))) + + ctx := s.GetCtx() + + gomock.InOrder( + s.store. + EXPECT().LoadOAuth2BlacklistedJTI(ctx, sig). + Return(nil, sql.ErrNoRows), + + s.store. + EXPECT().SaveOAuth2BlacklistedJTI(ctx, model.OAuth2BlacklistedJTI{Signature: sig, ExpiresAt: assertion.ExpiresAt.Time}). + Return(nil), + ) + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.NoError(ErrorToRFC6749ErrorTest(err)) + s.Require().NotNil(client) + s.Equal("hs256", client.GetID()) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldValidateAssertionHS384() { + assertion := NewAssertion("hs384", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS384, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + sig := fmt.Sprintf("%x", sha256.Sum256([]byte(assertion.ID))) + + ctx := s.GetCtx() + + gomock.InOrder( + s.store. + EXPECT().LoadOAuth2BlacklistedJTI(ctx, sig). + Return(nil, sql.ErrNoRows), + + s.store. + EXPECT().SaveOAuth2BlacklistedJTI(ctx, model.OAuth2BlacklistedJTI{Signature: sig, ExpiresAt: assertion.ExpiresAt.Time}). + Return(nil), + ) + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.NoError(ErrorToRFC6749ErrorTest(err)) + s.Require().NotNil(client) + s.Equal("hs384", client.GetID()) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldValidateAssertionHS512() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + sig := fmt.Sprintf("%x", sha256.Sum256([]byte(assertion.ID))) + + ctx := s.GetCtx() + + gomock.InOrder( + s.store. + EXPECT().LoadOAuth2BlacklistedJTI(ctx, sig). + Return(nil, sql.ErrNoRows), + + s.store. + EXPECT().SaveOAuth2BlacklistedJTI(ctx, model.OAuth2BlacklistedJTI{Signature: sig, ExpiresAt: assertion.ExpiresAt.Time}). + Return(nil), + ) + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.NoError(ErrorToRFC6749ErrorTest(err)) + s.Require().NotNil(client) + s.Equal("hs512", client.GetID()) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnMismatchedAlg() { + assertion := NewAssertion("rs256", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The 'client_assertion' uses signing algorithm 'HS512' but the requested OAuth 2.0 Client enforces signing algorithm 'RS256'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnMismatchedAlgSameMethod() { + assertion := NewAssertion("hs256", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The 'client_assertion' uses signing algorithm 'HS512' but the requested OAuth 2.0 Client enforces signing algorithm 'HS256'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysRS256() { + assertion := NewAssertion("rs256", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodRS256, assertion) + + token, err := assertionJWT.SignedString(MustParseRSAPrivateKey(exampleRSAPrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysRS384() { + assertion := NewAssertion("rs384", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodRS384, assertion) + + token, err := assertionJWT.SignedString(MustParseRSAPrivateKey(exampleRSAPrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysRS512() { + assertion := NewAssertion("rs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodRS512, assertion) + + token, err := assertionJWT.SignedString(MustParseRSAPrivateKey(exampleRSAPrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysPS256() { + assertion := NewAssertion("ps256", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodPS256, assertion) + + token, err := assertionJWT.SignedString(MustParseRSAPrivateKey(exampleRSAPrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysPS384() { + assertion := NewAssertion("ps384", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodPS384, assertion) + + token, err := assertionJWT.SignedString(MustParseRSAPrivateKey(exampleRSAPrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysPS512() { + assertion := NewAssertion("ps512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodPS512, assertion) + + token, err := assertionJWT.SignedString(MustParseRSAPrivateKey(exampleRSAPrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysES256() { + assertion := NewAssertion("es256", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodES256, assertion) + + token, err := assertionJWT.SignedString(MustParseECPrivateKey(exampleECP256PrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysES384() { + assertion := NewAssertion("es384", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodES384, assertion) + + token, err := assertionJWT.SignedString(MustParseECPrivateKey(exampleECP384PrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnUnregisteredKeysES512() { + assertion := NewAssertion("es512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodES512, assertion) + + token, err := assertionJWT.SignedString(MustParseECPrivateKey(exampleECP521PrivateKey)) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnJTIKnown() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + sig := fmt.Sprintf("%x", sha256.Sum256([]byte(assertion.ID))) + + ctx := s.GetCtx() + + gomock.InOrder( + s.store. + EXPECT().LoadOAuth2BlacklistedJTI(ctx, sig). + Return(nil, sql.ErrNoRows), + + s.store. + EXPECT().SaveOAuth2BlacklistedJTI(ctx, model.OAuth2BlacklistedJTI{Signature: sig, ExpiresAt: assertion.ExpiresAt.Time}). + Return(fosite.ErrJTIKnown), + ) + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "The jti was already used.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldValidateJWTWithArbitraryClaims() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + a := assertion.ToMapClaims() + a["aaa"] = "abc" + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, a) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + sig := fmt.Sprintf("%x", sha256.Sum256([]byte(assertion.ID))) + + ctx := s.GetCtx() + + gomock.InOrder( + s.store. + EXPECT().LoadOAuth2BlacklistedJTI(ctx, sig). + Return(nil, sql.ErrNoRows), + + s.store. + EXPECT().SaveOAuth2BlacklistedJTI(ctx, model.OAuth2BlacklistedJTI{Signature: sig, ExpiresAt: assertion.ExpiresAt.Time}). + Return(nil), + ) + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.NoError(ErrorToRFC6749ErrorTest(err)) + s.Require().NotNil(client) + s.Equal("hs512", client.GetID()) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithMissingSubClaim() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + a := assertion.ToMapClaims() + delete(a, oidc.ClaimSubject) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, a) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The claim 'sub' from the client_assertion JSON Web Token is undefined.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithInvalidExpClaim() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + a := assertion.ToMapClaims() + a[oidc.ClaimExpirationTime] = "not a number" + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, a) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Unable to verify the integrity of the 'client_assertion' value. The token is expired.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithMissingIssClaim() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + a := assertion.ToMapClaims() + delete(a, oidc.ClaimIssuer) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, a) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Claim 'iss' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithInvalidAudClaim() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertion.Audience = []string{"notvalid"} + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + sig := fmt.Sprintf("%x", sha256.Sum256([]byte(assertion.ID))) + + ctx := s.GetCtx() + + gomock.InOrder( + s.store. + EXPECT().LoadOAuth2BlacklistedJTI(ctx, sig). + Return(nil, sql.ErrNoRows), + + s.store. + EXPECT().SaveOAuth2BlacklistedJTI(ctx, model.OAuth2BlacklistedJTI{Signature: sig, ExpiresAt: assertion.ExpiresAt.Time}). + Return(nil), + ) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Claim 'audience' from 'client_assertion' must match the authorization server's token endpoint 'https://auth.example.com/api/oidc/token'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithInvalidAssertionType() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + v := s.GetAssertionValues(token) + + v.Set(oidc.FormParameterClientAssertionType, "not_valid") + + r := s.GetRequest(v) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Unknown client_assertion_type 'not_valid'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithMissingJTIClaim() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + a := assertion.ToMapClaims() + delete(a, oidc.ClaimJWTID) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, a) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Claim 'jti' from 'client_assertion' must be set but is not.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithMismatchedIssClaim() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertion.Issuer = "hs256" + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Claim 'iss' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldValidateClientSecretPost() { + r := s.GetClientSecretPostRequest(oidc.ClientAuthMethodClientSecretPost, "client-secret") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.NoError(err) + s.Require().NotNil(client) + s.Equal(oidc.ClientAuthMethodClientSecretPost, client.GetID()) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldErrorClientSecretPostOnClientSecretBasicClient() { + r := s.GetClientSecretPostRequest(oidc.ClientAuthMethodClientSecretBasic, "client-secret") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client supports client authentication method 'client_secret_basic', but method 'client_secret_post' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_post'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldErrorClientSecretPostWrongSecret() { + r := s.GetClientSecretPostRequest(oidc.ClientAuthMethodClientSecretPost, "client-secret-bad") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The provided client secret did not match the registered client secret.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldValidateClientSecretBasic() { + r := s.GetClientSecretBasicRequest(oidc.ClientAuthMethodClientSecretBasic, "client-secret") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.NoError(err) + s.Require().NotNil(client) + s.Equal(oidc.ClientAuthMethodClientSecretBasic, client.GetID()) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnClientSecretPostWithoutClientID() { + r := s.GetRequest(&url.Values{oidc.FormParameterClientSecret: []string{"client-secret"}}) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_request") + s.EqualError(ErrorToRFC6749ErrorTest(err), "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Client credentials missing or malformed in both HTTP Authorization header and HTTP POST body.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnClientSecretBasicWithMalformedClientID() { + r := s.GetRequest(&url.Values{oidc.FormParameterRequestURI: []string{"not applicable"}}) + + r.Header.Set(fasthttp.HeaderAuthorization, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("abc@#%!@#(*%)#@!:client-secret")))) + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_request") + s.EqualError(ErrorToRFC6749ErrorTest(err), "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The client id in the HTTP authorization header could not be decoded from 'application/x-www-form-urlencoded'. invalid URL escape '%!@'") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldRaiseErrorOnClientSecretBasicWithMalformedClientSecret() { + r := s.GetRequest(&url.Values{oidc.FormParameterRequestURI: []string{"not applicable"}}) + + r.Header.Set(fasthttp.HeaderAuthorization, fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte("hs512:abc@#%!@#(*%)#@!")))) + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_request") + s.EqualError(ErrorToRFC6749ErrorTest(err), "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The client secret in the HTTP authorization header could not be decoded from 'application/x-www-form-urlencoded'. invalid URL escape '%!@'") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldErrorClientSecretBasicOnClientSecretPostClient() { + r := s.GetClientSecretBasicRequest(oidc.ClientAuthMethodClientSecretPost, "client-secret") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client supports client authentication method 'client_secret_post', but method 'client_secret_basic' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_basic'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldErrorClientSecretBasicWrongSecret() { + r := s.GetClientSecretBasicRequest(oidc.ClientAuthMethodClientSecretBasic, "client-secret-bad") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The provided client secret did not match the registered client secret.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldErrorClientSecretBasicOnPublic() { + r := s.GetClientSecretBasicRequest("public", "client-secret") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client supports client authentication method 'none', but method 'client_secret_basic' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_basic'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldErrorClientSecretBasicOnPublicWithBasic() { + r := s.GetClientSecretBasicRequest("public-basic", "client-secret") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). The OAuth 2.0 Client supports client authentication method 'client_secret_basic', but method 'none' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'none'.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldErrorClientSecretBasicOnInvalidClient() { + r := s.GetClientSecretBasicRequest("not-a-client", "client-secret") + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). invalid_client") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldValidatePublic() { + v := s.GetClientValues("public") + + r := s.GetRequest(v) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.NoError(err) + s.Require().NotNil(client) + s.Equal("public", client.GetID()) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithMismatchedFormClientID() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertion.Issuer = "hs5122" + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + values := s.GetAssertionValues(token) + + values.Set(oidc.FormParameterClientID, "hs5122") + + r := s.GetRequest(values) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Claim 'sub' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithMismatchedFormClientIDWithIss() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + values := s.GetAssertionValues(token) + + values.Set(oidc.FormParameterClientID, "hs5122") + + r := s.GetRequest(values) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Claim 'iss' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailWithMissingClient() { + assertion := NewAssertion("noclient", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + client, err := s.provider.DefaultClientAuthenticationStrategy(s.GetCtx(), r, r.PostForm) + + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). invalid_client") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailBadSecret() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret-wrong")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Unable to verify the integrity of the 'client_assertion' value. The signature is invalid.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailMethodNone() { + assertion := NewAssertion(oidc.ClientAuthMethodNone, s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). This requested OAuth 2.0 client does not support client authentication, however 'client_assertion' was provided in the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailAssertionMethodClientSecretPost() { + assertion := NewAssertion(oidc.ClientAuthMethodClientSecretPost, s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). This requested OAuth 2.0 client only supports client authentication method 'client_secret_post', however 'client_assertion' was provided in the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailAssertionMethodBad() { + assertion := NewAssertion("bad_method", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). This requested OAuth 2.0 client only supports client authentication method 'bad_method', however that method is not supported by this server.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailAssertionBaseClient() { + assertion := NewAssertion("base", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_request") + s.EqualError(ErrorToRFC6749ErrorTest(err), "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The client configuration does not support OpenID Connect specific authentication methods.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailAssertionMethodClientSecretBasic() { + assertion := NewAssertion(oidc.ClientAuthMethodClientSecretBasic, s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). This requested OAuth 2.0 client only supports client authentication method 'client_secret_basic', however 'client_assertion' was provided in the request.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailHashedSecret() { + assertion := NewAssertion("hashed", s.GetTokenURL(), time.Now().Add(time.Second*-3), time.Unix(time.Now().Add(time.Minute).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). This client does not support authentication method 'client_secret_jwt' as the client secret is not in plaintext.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailExpiredToken() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Minute*-3), time.Unix(time.Now().Add(time.Minute*-1).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Unable to verify the integrity of the 'client_assertion' value. The token is expired.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailNotYetValid() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Minute*-3), time.Unix(time.Now().Add(time.Minute*1).Unix(), 0)) + + assertion.NotBefore = jwt.NewNumericDate(time.Now().Add(time.Second * 10)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + s.NoError(r.ParseForm()) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Unable to verify the integrity of the 'client_assertion' value. The token isn't valid yet.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailTokenUsedBeforeIssued() { + assertion := NewAssertion("hs512", s.GetTokenURL(), time.Now().Add(time.Minute*3), time.Unix(time.Now().Add(time.Minute*8).Unix(), 0)) + + assertionJWT := jwt.NewWithClaims(jwt.SigningMethodHS512, assertion) + + token, err := assertionJWT.SignedString([]byte("client-secret")) + + s.Require().NoError(err) + s.Require().NotEqual("", token) + + r := s.GetAssertionRequest(token) + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Unable to verify the integrity of the 'client_assertion' value. The token was used before it was issued.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailMalformed() { + r := s.GetAssertionRequest("bad token") + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_client") + s.EqualError(ErrorToRFC6749ErrorTest(err), "Client authentication failed (e.g., unknown client, no client authentication included, or unsupported authentication method). Unable to verify the integrity of the 'client_assertion' value. The token is malformed.") + s.Nil(client) +} + +func (s *ClientAuthenticationStrategySuite) TestShouldFailMissingAssertion() { + r := s.GetAssertionRequest("") + + ctx := s.GetCtx() + + client, err := s.provider.DefaultClientAuthenticationStrategy(ctx, r, r.PostForm) + + s.EqualError(err, "invalid_request") + s.EqualError(ErrorToRFC6749ErrorTest(err), "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. The client_assertion request parameter must be set when using client_assertion_type of 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer'.") + s.Nil(client) +} + +type RegisteredClaims struct { + jwt.RegisteredClaims +} + +func (r *RegisteredClaims) ToMapClaims() jwt.MapClaims { + claims := jwt.MapClaims{} + + if r.ID != "" { + claims[oidc.ClaimJWTID] = r.ID + } + + if r.Subject != "" { + claims[oidc.ClaimSubject] = r.Subject + } + + if r.Issuer != "" { + claims[oidc.ClaimIssuer] = r.Issuer + } + + if len(r.Audience) != 0 { + claims[oidc.ClaimAudience] = r.Audience + } + + if r.NotBefore != nil { + claims[oidc.ClaimNotBefore] = r.NotBefore + } + + if r.ExpiresAt != nil { + claims[oidc.ClaimExpirationTime] = r.ExpiresAt + } + + if r.IssuedAt != nil { + claims[oidc.ClaimIssuedAt] = r.IssuedAt + } + + return claims +} + +func NewAssertion(clientID string, tokenURL *url.URL, iat, exp time.Time) RegisteredClaims { + return RegisteredClaims{ + jwt.RegisteredClaims{ + ID: uuid.Must(uuid.NewRandom()).String(), + Issuer: clientID, + Audience: []string{ + tokenURL.String(), + }, + Subject: clientID, + IssuedAt: jwt.NewNumericDate(iat), + ExpiresAt: jwt.NewNumericDate(exp), + }, + } +} + +type RFC6749ErrorTest struct { + *fosite.RFC6749Error +} + +func (err *RFC6749ErrorTest) Error() string { + return err.WithExposeDebug(true).GetDescription() +} + +func ErrorToRFC6749ErrorTest(err error) (rfc error) { + if err == nil { + return nil + } + + ferr := fosite.ErrorToRFC6749Error(err) + + return &RFC6749ErrorTest{ferr} +} + +func MustDecodeSecret(value string) *schema.PasswordDigest { + if secret, err := schema.DecodePasswordDigest(value); err != nil { + panic(err) + } else { + return secret + } +} + +func MustParseRequestURI(input string) *url.URL { + if requestURI, err := url.ParseRequestURI(input); err != nil { + panic(err) + } else { + return requestURI + } +} + +func MustParseRSAPrivateKey(data string) *rsa.PrivateKey { + block, _ := pem.Decode([]byte(data)) + if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { + panic("not pem encoded") + } + + if block.Type != "RSA PRIVATE KEY" { + panic("not private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + + return key +} + +func MustParseECPrivateKey(data string) *ecdsa.PrivateKey { + block, _ := pem.Decode([]byte(data)) + if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { + panic("not pem encoded") + } + + if block.Type != "EC PRIVATE KEY" { + panic("not private key") + } + + key, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + panic(err) + } + + return key +} + +const exampleRSAPrivateKey = ` +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA60Vuz1N1wUHiCDIlbz8gE0dWPCmHEWnXKchEEISqIJ6j5Eah +Q/GwX3WK0UV5ATRvWhg6o7/WfrLYcAsi4w79TgMjJHLWIY/jzAS3quEtzOLlLSWZ +9FR9SomQm3T/ETOS8IvSGrksIj0WgX35jB1NnbqSTRnYx7Cg/TBJjmiaqd0b9G/8 +LlReaihwGf8tvPgnteWIdon3EI2MKDBkaesRjpL98Cz7VvD7dajseAlUh9jQWVge +sN8qnm8pNPFAYsgxf//Jf0RfsND6H70zKKybDmyct4T4o/8qjivw4ly0XkArDCUj +Qx2KUF7nN+Bo9wwnNppjdnsOPUbus8o1a9vY1QIDAQABAoIBAQDl1SBY3PlN36SF +yScUtCALdUbi4taVxkVxBbioQlFIKHGGkRD9JN/dgSApK6r36FdXNhAi40cQ4nnZ +iqd8FKqTSTFNa/mPM9ee+ITMI8nwOz8SiYcKTndPF2/yzapXDYDgCFcpz/czQ2X2 +/i+IFyA5k4dUVomVGhFLBZ71xW5BvGUBMUH0XkeR5+c4gLvgR209BlpBHlkX4tUQ ++RQoxbKpkntl0mjqf91zcOe4LJVsXZFyN+NVSzLEbGC3lVSSiyjVQH3s7ExnTaHi +PpwSoXzu5QJj5xRit/1B3/LEGpIlPGFrkhMzBDTN+HYV/VLbCHJzjg5GVJawA82E +h2BY6YWJAoGBAPmGaZL5ggnTVR2XVBLDKbwL/sesqiPZk45B+I5eObHl+v236JH9 +RPMjdE10jOR1TzfQdmE2/RboKhiVn+osS+2W6VXSo7sMsSM1bLBPYhnwrNIqzrX8 +Vgi2bCl2S8ZhVo2R8c5WUaD0Gpxs6hwPIMOQWWwxDlsbg/UoLrhD3X4XAoGBAPFg +VSvaWQdDVAqjM42ObhZtWxeLfEAcxRQDMQq7btrTwBZSrtP3S3Egu66cp/4PT4VD +Hc8tYyT2rNETiqT6b2Rm1MgeoJ8wRqte6ZXSQVVQUOd42VG04O3aaleAGhXjEkM2 +avctRdKHDhQdIt+riPgaNj4FdYpmQ5zIrcZtBr/zAoGBAOBXzBX7xMHmwxEe3NUd +qSlMM579C9+9oF/3ymzeJMtgtcBmGHEhoFtmVgvJrV8+ZaIOCFExam2tASQnaqbV +etK7q0ChaNok+CJqxzThupcN/6PaHw4aOJQOx8KjfE95dqNEQ367txqaPk7D0dy2 +cUPDRdLzbC/X1lWV8iNzyPGzAoGBAN4R2epRpYz4Fa7/vWNkAcaib6c2zmaR0YN6 ++Di+ftvW6yfehDhBkWgQTHv2ZtxoK6oYOKmuQUP1qsNkbi8gtTEzJlrDStWKbcom +tVMAsNkT3otHdPEmL7bFNwcvtVAjrF6oBztHrLBnTr2UnMwZnhdczkC7dwuQ0G3D +d5VSI16fAoGAY7eeVDkic73GbZmtZibuodvPJ/z85RIBOrzf3ColO4jGI6Ej/EnD +rMEe/mRC27CJzS9L9Jc0Kt66mGSvodDGl0nBsXGNfPog0cGwweCVN0Eo2VJZbRTT +UoU05/Pvu2h3/E8gGTBY0/WPSo06YUsICjVDWNuOIa/7IY7SyE6Xxn0= +-----END RSA PRIVATE KEY-----` + +const exampleECP256PrivateKey = ` +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEID1fSsJ8qyEqj2DVkrshaNiXqaSDX7qViASRkyGGJFbEoAoGCCqGSM49 +AwEHoUQDQgAENnBG+bBJIaIa+bRlHaLiXD86RAy+Ef9CVdAfpPGoNRfkOTcrrIV7 +2wv3Y5e0he63Tn9iVAFYRFexK1mjFw7TfA== +-----END EC PRIVATE KEY-----` + +const exampleECP384PrivateKey = ` +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDBPoOfapxtgZ8XNE7Wwdlw+9oDc6x4m57MITZyWzN62jkFUAYsvPJDF +9+g+e8CT5yqgBwYFK4EEACKhZANiAAQ2uZ0HIIxIavyjGyX13tIZVOaRB4+D64dF +s3DXDrpXcuDTSohw9xBW5sLDqRVu2LkBsCUFXtEJUHgC+O7wToNw8nh+KdDrcu/J +miNqbvEHuvlSlHWyx9HH8kAEuu1+SZg= +-----END EC PRIVATE KEY-----` + +const exampleECP521PrivateKey = ` +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIBT07AnitDd1Z01bl5W5VW8/vTWyu7w3MSqEmCeKcM19p/TAJAeS8L +6UOig2fTUeuMeA2PoOUjI2Bid927VsWcxE2gBwYFK4EEACOhgYkDgYYABAGnV9mu +xY0E7/k8b+glOOMaN0+Qt70H9OmSz6tC8tU3EayRwFlNPch9TlvEpbCS3MsDE9dN +78EpFx45MUqzzdZcOgAu+EUC9Zas1YVK+WMo0GFy+XtFq3kxubOclBb52M/63mcd +zZnA8aAu9iTK9YPfcw1YWTJliNdKUoxmGVV5Ca1W4w== +-----END EC PRIVATE KEY-----` diff --git a/internal/handlers/handler_oidc_userinfo.go b/internal/handlers/handler_oidc_userinfo.go index c3dc4f9e6..33b827bbe 100644 --- a/internal/handlers/handler_oidc_userinfo.go +++ b/internal/handlers/handler_oidc_userinfo.go @@ -100,7 +100,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, ctx.Logger.Tracef("UserInfo Response with id '%s' on client with id '%s' is being sent with the following claims: %+v", requester.GetID(), clientID, claims) switch client.GetUserinfoSigningAlgorithm() { - case oidc.SigningAlgorithmRSAWithSHA256: + case oidc.SigningAlgRSAUsingSHA256: var jti uuid.UUID if jti, err = uuid.NewRandom(); err != nil { @@ -126,7 +126,7 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, rw.Header().Set(fasthttp.HeaderContentType, "application/jwt") _, _ = rw.Write([]byte(token)) - case oidc.SigningAlgorithmNone, "": + case oidc.SigningAlgNone, "": ctx.Providers.OpenIDConnect.Write(rw, req, claims) default: ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHintf("Unsupported UserInfo signing algorithm '%s'.", client.GetUserinfoSigningAlgorithm()))) diff --git a/internal/oidc/client.go b/internal/oidc/client.go index 71b43d867..6a100cdbe 100644 --- a/internal/oidc/client.go +++ b/internal/oidc/client.go @@ -47,8 +47,9 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client Client) { if config.TokenEndpointAuthMethod != "" && config.TokenEndpointAuthMethod != "auto" { client = &FullClient{ - BaseClient: base, - TokenEndpointAuthMethod: config.TokenEndpointAuthMethod, + BaseClient: base, + TokenEndpointAuthMethod: config.TokenEndpointAuthMethod, + TokenEndpointAuthSigningAlgorithm: config.TokenEndpointAuthSigningAlg, } } else { client = base @@ -133,7 +134,7 @@ func (c *BaseClient) GetResponseModes() []fosite.ResponseModeType { // GetUserinfoSigningAlgorithm returns the UserinfoSigningAlgorithm. func (c *BaseClient) GetUserinfoSigningAlgorithm() string { if c.UserinfoSigningAlgorithm == "" { - c.UserinfoSigningAlgorithm = SigningAlgorithmNone + c.UserinfoSigningAlgorithm = SigningAlgNone } return c.UserinfoSigningAlgorithm @@ -306,7 +307,7 @@ func (c *FullClient) GetTokenEndpointAuthMethod() string { // authentication methods. func (c *FullClient) GetTokenEndpointAuthSigningAlgorithm() string { if c.TokenEndpointAuthSigningAlgorithm == "" { - c.TokenEndpointAuthSigningAlgorithm = SigningAlgorithmRSAWithSHA256 + c.TokenEndpointAuthSigningAlgorithm = SigningAlgRSAUsingSHA256 } return c.TokenEndpointAuthSigningAlgorithm diff --git a/internal/oidc/client_auth.go b/internal/oidc/client_auth.go new file mode 100644 index 000000000..c852153bc --- /dev/null +++ b/internal/oidc/client_auth.go @@ -0,0 +1,331 @@ +package oidc + +import ( + "context" + "crypto/ecdsa" + "crypto/rsa" + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/go-crypt/crypt/algorithm/plaintext" + "github.com/golang-jwt/jwt/v4" + "github.com/ory/fosite" + "github.com/ory/x/errorsx" + "github.com/pkg/errors" + "gopkg.in/square/go-jose.v2" + + "github.com/authelia/authelia/v4/internal/configuration/schema" +) + +// DefaultClientAuthenticationStrategy is a copy of fosite's with the addition of the client_secret_jwt method and some +// minor superficial changes. +// +//nolint:gocyclo // Complexity is necessary to remain in feature parity. +func (p *OpenIDConnectProvider) DefaultClientAuthenticationStrategy(ctx context.Context, r *http.Request, form url.Values) (client fosite.Client, err error) { + if assertionType := form.Get(FormParameterClientAssertionType); assertionType == ClientAssertionJWTBearerType { + assertion := form.Get(FormParameterClientAssertion) + if len(assertion) == 0 { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The client_assertion request parameter must be set when using client_assertion_type of '%s'.", ClientAssertionJWTBearerType)) + } + + var ( + token *jwt.Token + clientID string + ) + + token, err = jwt.ParseWithClaims(assertion, jwt.MapClaims{}, func(t *jwt.Token) (any, error) { + clientID, _, err = clientCredentialsFromRequestBody(form, false) + if err != nil { + return nil, err + } + + if clientID == "" { + claims := t.Claims.(jwt.MapClaims) + + if sub, ok := claims[ClaimSubject].(string); !ok { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("The claim 'sub' from the client_assertion JSON Web Token is undefined.")) + } else { + clientID = sub + } + } + + if client, err = p.Store.GetClient(ctx, clientID); err != nil { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithDebug(err.Error())) + } + + oidcClient, ok := client.(*FullClient) + if !ok { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("The client configuration does not support OpenID Connect specific authentication methods.")) + } + + switch oidcClient.GetTokenEndpointAuthMethod() { + case ClientAuthMethodPrivateKeyJWT, ClientAuthMethodClientSecretJWT: + break + case ClientAuthMethodNone: + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("This requested OAuth 2.0 client does not support client authentication, however 'client_assertion' was provided in the request.")) + case ClientAuthMethodClientSecretPost: + fallthrough + case ClientAuthMethodClientSecretBasic: + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however 'client_assertion' was provided in the request.", oidcClient.GetTokenEndpointAuthMethod())) + default: + return nil, errorsx.WithStack(fosite.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 oidcClient.GetTokenEndpointAuthSigningAlgorithm() != fmt.Sprintf("%s", t.Header[JWTHeaderKeyAlgorithm]) { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' uses signing algorithm '%s' but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", t.Header[JWTHeaderKeyAlgorithm], oidcClient.GetTokenEndpointAuthSigningAlgorithm())) + } + + switch t.Method { + case jwt.SigningMethodRS256, jwt.SigningMethodRS384, jwt.SigningMethodRS512: + return p.findClientPublicJWK(ctx, oidcClient, t, true) + case jwt.SigningMethodES256, jwt.SigningMethodES384, jwt.SigningMethodES512: + return p.findClientPublicJWK(ctx, oidcClient, t, false) + case jwt.SigningMethodPS256, jwt.SigningMethodPS384, jwt.SigningMethodPS512: + return p.findClientPublicJWK(ctx, oidcClient, t, true) + case jwt.SigningMethodHS256, jwt.SigningMethodHS384, jwt.SigningMethodHS512: + if spd, ok := oidcClient.Secret.(*schema.PasswordDigest); ok { + if secret, ok := spd.Digest.(*plaintext.Digest); ok { + return secret.Key(), nil + } + } + + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("This client does not support authentication method 'client_secret_jwt' as the client secret is not in plaintext.")) + default: + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' request parameter uses unsupported signing algorithm '%s'.", t.Header[JWTHeaderKeyAlgorithm])) + } + }) + + if err != nil { + var r *fosite.RFC6749Error + + if errors.As(err, &r) { + return nil, err + } + + var e *jwt.ValidationError + + if errors.As(err, &e) { + rfc := fosite.ErrInvalidClient.WithHint("Unable to verify the integrity of the 'client_assertion' value.").WithWrap(err) + + switch { + case e.Errors&jwt.ValidationErrorMalformed != 0: + return nil, errorsx.WithStack(rfc.WithDebug("The token is malformed.")) + case e.Errors&jwt.ValidationErrorIssuedAt != 0: + return nil, errorsx.WithStack(rfc.WithDebug("The token was used before it was issued.")) + case e.Errors&jwt.ValidationErrorExpired != 0: + return nil, errorsx.WithStack(rfc.WithDebug("The token is expired.")) + case e.Errors&jwt.ValidationErrorNotValidYet != 0: + return nil, errorsx.WithStack(rfc.WithDebug("The token isn't valid yet.")) + case e.Errors&jwt.ValidationErrorSignatureInvalid != 0: + return nil, errorsx.WithStack(rfc.WithDebug("The signature is invalid.")) + } + + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Unable to verify the integrity of the 'client_assertion' value.").WithWrap(err).WithDebug(err.Error())) + } + + return nil, err + } else if err = token.Claims.Valid(); err != nil { + return nil, errorsx.WithStack(fosite.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.(jwt.MapClaims) + + tokenURL := p.Config.GetTokenURL(ctx) + + var jti string + + if !claims.VerifyIssuer(clientID, true) { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Claim 'iss' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client.")) + } else if tokenURL == "" { + return nil, errorsx.WithStack(fosite.ErrMisconfiguration.WithHint("The authorization server's token endpoint URL has not been set.")) + } else if sub, ok := claims[ClaimSubject].(string); !ok || sub != clientID { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Claim 'sub' from 'client_assertion' must match the 'client_id' of the OAuth 2.0 Client.")) + } else if jti, ok = claims[ClaimJWTID].(string); !ok || len(jti) == 0 { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("Claim 'jti' from 'client_assertion' must be set but is not.")) + } else if p.Store.ClientAssertionJWTValid(ctx, jti) != nil { + return nil, errorsx.WithStack(fosite.ErrJTIKnown.WithHint("Claim 'jti' from 'client_assertion' MUST only be used once.")) + } + + err = nil + + var expiry int64 + + switch exp := claims[ClaimExpirationTime].(type) { + case float64: + expiry = int64(exp) + case int64: + expiry = exp + case json.Number: + expiry, err = exp.Int64() + default: + err = fosite.ErrInvalidClient.WithHint("Unable to type assert the expiry time from claims. This should not happen as we validate the expiry time already earlier with token.Claims.Valid()") + } + + if err != nil { + return nil, errorsx.WithStack(err) + } + + if err = p.Store.SetClientAssertionJWT(ctx, jti, time.Unix(expiry, 0)); err != nil { + return nil, err + } + + var found bool + + if auds, ok := claims[ClaimAudience].([]any); ok { + for _, aud := range auds { + if a, ok := aud.(string); ok && a == tokenURL { + found = true + break + } + } + } + + if !found { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("Claim 'audience' from 'client_assertion' must match the authorization server's token endpoint '%s'.", tokenURL)) + } + + return client, nil + } else if len(assertionType) > 0 { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unknown client_assertion_type '%s'.", assertionType)) + } + + clientID, clientSecret, err := clientCredentialsFromRequest(r, form) + if err != nil { + return nil, err + } + + if client, err = p.Store.GetClient(ctx, clientID); err != nil { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithDebug(err.Error())) + } + + if oidcClient, ok := client.(fosite.OpenIDConnectClient); ok { + method := oidcClient.GetTokenEndpointAuthMethod() + + if form.Get(FormParameterClientID) != "" && form.Get(FormParameterClientSecret) != "" && method != ClientAuthMethodClientSecretPost { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The OAuth 2.0 Client supports client authentication method '%s', but method 'client_secret_post' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_post'.", method)) + } else if _, secret, basicOk := r.BasicAuth(); basicOk && secret != "" && method != ClientAuthMethodClientSecretBasic { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The OAuth 2.0 Client supports client authentication method '%s', but method 'client_secret_basic' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'client_secret_basic'.", method)) + } else if method != ClientAuthMethodNone && client.IsPublic() { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The OAuth 2.0 Client supports client authentication method '%s', but method 'none' was requested. You must configure the OAuth 2.0 client's 'token_endpoint_auth_method' value to accept 'none'.", method)) + } + } + + if client.IsPublic() { + return client, nil + } + + if err = p.checkClientSecret(ctx, client, []byte(clientSecret)); err != nil { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithWrap(err).WithDebug(err.Error())) + } + + return client, nil +} + +func (p *OpenIDConnectProvider) checkClientSecret(ctx context.Context, client fosite.Client, clientSecret []byte) (err error) { + if err = p.Config.GetSecretsHasher(ctx).Compare(ctx, client.GetHashedSecret(), clientSecret); err == nil { + return nil + } + + cc, ok := client.(fosite.ClientWithSecretRotation) + if !ok { + return err + } + + for _, hash := range cc.GetRotatedHashes() { + if err = p.Config.GetSecretsHasher(ctx).Compare(ctx, hash, clientSecret); err == nil { + return nil + } + } + + return err +} + +func (p *OpenIDConnectProvider) findClientPublicJWK(ctx context.Context, oidcClient fosite.OpenIDConnectClient, t *jwt.Token, expectsRSAKey bool) (any, error) { + if set := oidcClient.GetJSONWebKeys(); set != nil { + return findPublicKey(t, set, expectsRSAKey) + } + + if location := oidcClient.GetJSONWebKeysURI(); len(location) > 0 { + keys, err := p.Config.GetJWKSFetcherStrategy(ctx).Resolve(ctx, location, false) + if err != nil { + return nil, err + } + + if key, err := findPublicKey(t, keys, expectsRSAKey); err == nil { + return key, nil + } + + keys, err = p.Config.GetJWKSFetcherStrategy(ctx).Resolve(ctx, location, true) + if err != nil { + return nil, err + } + + return findPublicKey(t, keys, expectsRSAKey) + } + + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("The OAuth 2.0 Client has no JSON Web Keys set registered, but they are needed to complete the request.")) +} + +func findPublicKey(t *jwt.Token, set *jose.JSONWebKeySet, expectsRSAKey bool) (any, error) { + keys := set.Keys + if len(keys) == 0 { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The retrieved JSON Web Key Set does not contain any key.")) + } + + kid, ok := t.Header[JWTHeaderKeyIdentifier].(string) + if ok { + keys = set.Key(kid) + } + + if len(keys) == 0 { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("The JSON Web Token uses signing key with kid '%s', which could not be found.", kid)) + } + + for _, key := range keys { + if key.Use != KeyUseSignature { + continue + } + + if expectsRSAKey { + if k, ok := key.Key.(*rsa.PublicKey); ok { + return k, nil + } + } else { + if k, ok := key.Key.(*ecdsa.PublicKey); ok { + return k, nil + } + } + } + + if expectsRSAKey { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find RSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid)) + } else { + return nil, errorsx.WithStack(fosite.ErrInvalidRequest.WithHintf("Unable to find ECDSA public key with use='sig' for kid '%s' in JSON Web Key Set.", kid)) + } +} + +func clientCredentialsFromRequest(r *http.Request, form url.Values) (clientID, clientSecret string, err error) { + if id, secret, ok := r.BasicAuth(); !ok { + return clientCredentialsFromRequestBody(form, true) + } else if clientID, err = url.QueryUnescape(id); err != nil { + return "", "", errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("The client id in the HTTP authorization header could not be decoded from 'application/x-www-form-urlencoded'.").WithWrap(err).WithDebug(err.Error())) + } else if clientSecret, err = url.QueryUnescape(secret); err != nil { + return "", "", errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("The client secret in the HTTP authorization header could not be decoded from 'application/x-www-form-urlencoded'.").WithWrap(err).WithDebug(err.Error())) + } + + return clientID, clientSecret, nil +} + +func clientCredentialsFromRequestBody(form url.Values, forceID bool) (clientID, clientSecret string, err error) { + clientID = form.Get(FormParameterClientID) + clientSecret = form.Get(FormParameterClientSecret) + + if clientID == "" && forceID { + return "", "", errorsx.WithStack(fosite.ErrInvalidRequest.WithHint("Client credentials missing or malformed in both HTTP Authorization header and HTTP POST body.")) + } + + return clientID, clientSecret, nil +} diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go index a4be63346..ece18b278 100644 --- a/internal/oidc/client_test.go +++ b/internal/oidc/client_test.go @@ -27,7 +27,7 @@ func TestNewClient(t *testing.T) { bclient, ok := client.(*BaseClient) require.True(t, ok) assert.Equal(t, "", bclient.UserinfoSigningAlgorithm) - assert.Equal(t, SigningAlgorithmNone, client.GetUserinfoSigningAlgorithm()) + assert.Equal(t, SigningAlgNone, client.GetUserinfoSigningAlgorithm()) _, ok = client.(*FullClient) assert.False(t, ok) @@ -64,9 +64,9 @@ func TestNewClient(t *testing.T) { assert.Equal(t, "", fclient.UserinfoSigningAlgorithm) assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.TokenEndpointAuthMethod) assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.GetTokenEndpointAuthMethod()) - assert.Equal(t, SigningAlgorithmNone, client.GetUserinfoSigningAlgorithm()) + assert.Equal(t, SigningAlgNone, client.GetUserinfoSigningAlgorithm()) assert.Equal(t, "", fclient.TokenEndpointAuthSigningAlgorithm) - assert.Equal(t, SigningAlgorithmRSAWithSHA256, fclient.GetTokenEndpointAuthSigningAlgorithm()) + assert.Equal(t, SigningAlgRSAUsingSHA256, fclient.GetTokenEndpointAuthSigningAlgorithm()) assert.Equal(t, "", fclient.RequestObjectSigningAlgorithm) assert.Equal(t, "", fclient.GetRequestObjectSigningAlgorithm()) assert.Equal(t, "", fclient.JSONWebKeysURI) @@ -545,11 +545,3 @@ func TestClient_IsPublic(t *testing.T) { c.Public = true assert.True(t, c.IsPublic()) } - -func MustDecodeSecret(value string) *schema.PasswordDigest { - if secret, err := schema.DecodePasswordDigest(value); err != nil { - panic(err) - } else { - return secret - } -} diff --git a/internal/oidc/const.go b/internal/oidc/const.go index 3680923c3..2143236af 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -40,6 +40,11 @@ const ( ClaimClientIdentifier = "client_id" ) +const ( + // ClientAssertionJWTBearerType is the JWT bearer assertion. + ClientAssertionJWTBearerType = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" //nolint:gosec // False Positive. +) + const ( lifespanTokenDefault = time.Hour lifespanRefreshTokenDefault = time.Hour * 24 * 30 @@ -75,6 +80,8 @@ const ( const ( ClientAuthMethodClientSecretBasic = "client_secret_basic" ClientAuthMethodClientSecretPost = "client_secret_post" + ClientAuthMethodClientSecretJWT = "client_secret_jwt" + ClientAuthMethodPrivateKeyJWT = "private_key_jwt" ClientAuthMethodNone = "none" ) @@ -89,10 +96,30 @@ const ( ResponseTypeHybridFlowBoth = "code id_token token" ) -// Signing Algorithm strings. +// JWS Algorithm strings. +// See: https://datatracker.ietf.org/doc/html/rfc7518#section-3.1 const ( - SigningAlgorithmNone = none - SigningAlgorithmRSAWithSHA256 = "RS256" + SigningAlgNone = none + + SigningAlgRSAUsingSHA256 = "RS256" + SigningAlgRSAUsingSHA384 = "RS384" + SigningAlgRSAUsingSHA512 = "RS512" + + SigningAlgRSAPSSUsingSHA256 = "PS256" + SigningAlgRSAPSSUsingSHA384 = "PS384" + SigningAlgRSAPSSUsingSHA512 = "PS512" + + SigningAlgECDSAUsingP256AndSHA256 = "ES256" + SigningAlgECDSAUsingP384AndSHA384 = "ES384" + SigningAlgECDSAUsingP521AndSHA512 = "ES512" + + SigningAlgHMACUsingSHA256 = "HS256" + SigningAlgHMACUsingSHA384 = "HS384" + SigningAlgHMACUsingSHA512 = "HS512" +) + +const ( + KeyUseSignature = "sig" ) // Subject Type strings. @@ -108,10 +135,14 @@ const ( ) const ( + FormParameterClientID = "client_id" + FormParameterClientSecret = "client_secret" FormParameterRequestURI = "request_uri" FormParameterResponseMode = "response_mode" FormParameterCodeChallenge = "code_challenge" FormParameterCodeChallengeMethod = "code_challenge_method" + FormParameterClientAssertionType = "client_assertion_type" + FormParameterClientAssertion = "client_assertion" ) const ( @@ -135,6 +166,9 @@ const ( const ( // JWTHeaderKeyIdentifier is the JWT Header referencing the JWS Key Identifier used to sign a token. JWTHeaderKeyIdentifier = "kid" + + // JWTHeaderKeyAlgorithm is the JWT Header referencing the JWS Key algorithm used to sign a token. + JWTHeaderKeyAlgorithm = "alg" ) const ( diff --git a/internal/oidc/const_test.go b/internal/oidc/const_test.go index b5e3d915b..0208df15f 100644 --- a/internal/oidc/const_test.go +++ b/internal/oidc/const_test.go @@ -1,12 +1,56 @@ package oidc -const ( - myclient = "myclient" - myclientdesc = "My Client" - onefactor = "one_factor" - twofactor = "two_factor" - examplecom = "https://example.com" - examplecomsid = "example.com" - badsecret = "$plaintext$a_bad_secret" - badhmac = "asbdhaaskmdlkamdklasmdlkams" +import ( + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "net/url" + + "github.com/authelia/authelia/v4/internal/configuration/schema" ) + +const ( + myclient = "myclient" + myclientdesc = "My Client" + onefactor = "one_factor" + twofactor = "two_factor" + examplecom = "https://example.com" + examplecomsid = "example.com" + badsecret = "$plaintext$a_bad_secret" + badhmac = "asbdhaaskmdlkamdklasmdlkams" + exampleIssuerPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S\nKcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X\nyEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M\nlqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE\nlgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R\ncMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn\nV3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv\nB7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd\nzV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036\nUxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1\n/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI\nF4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd\n7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs\nhcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA\n06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh\nIlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75\nHmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/\nrW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE\nZrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b\nbx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq\n0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS\nqfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2\nqSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L\nzqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2\nHEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==\n-----END RSA PRIVATE KEY-----" +) + +func MustDecodeSecret(value string) *schema.PasswordDigest { + if secret, err := schema.DecodePasswordDigest(value); err != nil { + panic(err) + } else { + return secret + } +} + +func MustParseRequestURI(input string) *url.URL { + if requestURI, err := url.ParseRequestURI(input); err != nil { + panic(err) + } else { + return requestURI + } +} + +func MustParseRSAPrivateKey(data string) *rsa.PrivateKey { + block, _ := pem.Decode([]byte(data)) + if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { + panic("not pem encoded") + } + + if block.Type != "RSA PRIVATE KEY" { + panic("not private key") + } + + key, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + panic(err) + } + + return key +} diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index e57bad146..7ce180fbf 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -62,8 +62,14 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration TokenEndpointAuthMethodsSupported: []string{ ClientAuthMethodClientSecretBasic, ClientAuthMethodClientSecretPost, + ClientAuthMethodClientSecretJWT, ClientAuthMethodNone, }, + TokenEndpointAuthSigningAlgValuesSupported: []string{ + SigningAlgHMACUsingSHA256, + SigningAlgHMACUsingSHA384, + SigningAlgHMACUsingSHA512, + }, }, OAuth2DiscoveryOptions: OAuth2DiscoveryOptions{ CodeChallengeMethodsSupported: []string{ @@ -77,11 +83,11 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration OpenIDConnectDiscoveryOptions: OpenIDConnectDiscoveryOptions{ IDTokenSigningAlgValuesSupported: []string{ - SigningAlgorithmRSAWithSHA256, + SigningAlgRSAUsingSHA256, }, UserinfoSigningAlgValuesSupported: []string{ - SigningAlgorithmNone, - SigningAlgorithmRSAWithSHA256, + SigningAlgNone, + SigningAlgRSAUsingSHA256, }, }, OpenIDConnectFrontChannelLogoutDiscoveryOptions: &OpenIDConnectFrontChannelLogoutDiscoveryOptions{}, diff --git a/internal/oidc/errors.go b/internal/oidc/errors.go index 58464568c..4041bbda4 100644 --- a/internal/oidc/errors.go +++ b/internal/oidc/errors.go @@ -6,7 +6,7 @@ import ( "github.com/ory/fosite" ) -var errPasswordsDoNotMatch = errors.New("the passwords don't match") +var errPasswordsDoNotMatch = errors.New("The provided client secret did not match the registered client secret.") var ( // ErrIssuerCouldNotDerive is sent when the issuer couldn't be determined from the headers. diff --git a/internal/oidc/keys.go b/internal/oidc/keys.go index f3b0928cc..3ca47d414 100644 --- a/internal/oidc/keys.go +++ b/internal/oidc/keys.go @@ -125,8 +125,8 @@ func NewJWK(chain schema.X509CertificateChain, key *rsa.PrivateKey) (j *JWK, err } jwk := &jose.JSONWebKey{ - Algorithm: SigningAlgorithmRSAWithSHA256, - Use: "sig", + Algorithm: SigningAlgRSAUsingSHA256, + Use: KeyUseSignature, Key: &key.PublicKey, } @@ -170,8 +170,8 @@ func (j *JWK) JSONWebKey() (jwk *jose.JSONWebKey) { jwk = &jose.JSONWebKey{ Key: &j.key.PublicKey, KeyID: j.id, - Algorithm: "RS256", - Use: "sig", + Algorithm: SigningAlgRSAUsingSHA256, + Use: KeyUseSignature, Certificates: j.chain.Certificates(), } diff --git a/internal/oidc/keys_test.go b/internal/oidc/keys_test.go index 1e15973ea..0405227b3 100644 --- a/internal/oidc/keys_test.go +++ b/internal/oidc/keys_test.go @@ -17,7 +17,7 @@ func TestKeyManager_AddActiveJWK(t *testing.T) { assert.Nil(t, manager.jwk) assert.Nil(t, manager.Strategy()) - j, err := manager.AddActiveJWK(schema.X509CertificateChain{}, mustParseRSAPrivateKey(exampleIssuerPrivateKey)) + j, err := manager.AddActiveJWK(schema.X509CertificateChain{}, MustParseRSAPrivateKey(exampleIssuerPrivateKey)) require.NoError(t, err) require.NotNil(t, j) require.NotNil(t, manager.jwk) diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index b8333351b..3238a1c83 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -36,6 +36,7 @@ func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store s } provider.Config.LoadHandlers(provider.Store, provider.KeyManager.Strategy()) + provider.Config.Strategy.ClientAuthentication = provider.DefaultClientAuthenticationStrategy provider.discovery = NewOpenIDConnectWellKnownConfiguration(config) diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go index 4a1a0b115..51ed6e4e6 100644 --- a/internal/oidc/provider_test.go +++ b/internal/oidc/provider_test.go @@ -1,9 +1,6 @@ package oidc import ( - "crypto/rsa" - "crypto/x509" - "encoding/pem" "net/url" "testing" @@ -13,8 +10,6 @@ import ( "github.com/authelia/authelia/v4/internal/configuration/schema" ) -var exampleIssuerPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S\nKcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X\nyEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M\nlqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE\nlgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R\ncMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn\nV3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv\nB7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd\nzV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036\nUxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1\n/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI\nF4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd\n7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs\nhcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA\n06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh\nIlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75\nHmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/\nrW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE\nZrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b\nbx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq\n0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS\nqfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2\nqSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L\nzqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2\nHEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==\n-----END RSA PRIVATE KEY-----" - func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing.T) { provider, err := NewOpenIDConnectProvider(nil, nil, nil) @@ -25,7 +20,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), EnablePKCEPlainChallenge: true, HMACSecret: badhmac, Clients: []schema.OpenIDConnectClientConfiguration{ @@ -57,7 +52,7 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), HMACSecret: badhmac, Clients: []schema.OpenIDConnectClientConfiguration{ { @@ -97,7 +92,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ { @@ -152,9 +147,10 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowToken) assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowBoth) - assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 3) + assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 4) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretPost) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodNone) assert.Len(t, disco.GrantTypesSupported, 3) @@ -163,11 +159,11 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Contains(t, disco.GrantTypesSupported, GrantTypeImplicit) assert.Len(t, disco.IDTokenSigningAlgValuesSupported, 1) - assert.Contains(t, disco.IDTokenSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256) + assert.Contains(t, disco.IDTokenSigningAlgValuesSupported, SigningAlgRSAUsingSHA256) assert.Len(t, disco.UserinfoSigningAlgValuesSupported, 2) - assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgorithmRSAWithSHA256) - assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgorithmNone) + assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgRSAUsingSHA256) + assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgNone) assert.Len(t, disco.RequestObjectSigningAlgValuesSupported, 0) @@ -195,7 +191,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ { @@ -249,9 +245,10 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowToken) assert.Contains(t, disco.ResponseTypesSupported, ResponseTypeHybridFlowBoth) - assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 3) + assert.Len(t, disco.TokenEndpointAuthMethodsSupported, 4) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretPost) + assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodNone) assert.Len(t, disco.GrantTypesSupported, 3) @@ -283,7 +280,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfigurationWithPlainPKCE(t *testing.T) { provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), HMACSecret: "asbdhaaskmdlkamdklasmdlkams", EnablePKCEPlainChallenge: true, Clients: []schema.OpenIDConnectClientConfiguration{ @@ -306,21 +303,3 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Equal(t, PKCEChallengeMethodSHA256, disco.CodeChallengeMethodsSupported[0]) assert.Equal(t, PKCEChallengeMethodPlain, disco.CodeChallengeMethodsSupported[1]) } - -func mustParseRSAPrivateKey(data string) *rsa.PrivateKey { - block, _ := pem.Decode([]byte(data)) - if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { - panic("not pem encoded") - } - - if block.Type != "RSA PRIVATE KEY" { - panic("not private key") - } - - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - panic(err) - } - - return key -} diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go index def1d4e8e..47e15c424 100644 --- a/internal/oidc/store_test.go +++ b/internal/oidc/store_test.go @@ -15,7 +15,7 @@ import ( func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { ID: myclient, @@ -47,7 +47,7 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { ID: myclient, @@ -82,7 +82,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{c1}, }, nil) @@ -110,7 +110,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{c1}, }, nil) @@ -122,7 +122,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { func TestOpenIDConnectStore_IsValidClientID(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: mustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), Clients: []schema.OpenIDConnectClientConfiguration{ { ID: myclient, diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 8606fe3a3..6346a3ae2 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -958,3 +958,15 @@ type OpenIDConnectContext interface { IssuerURL() (issuerURL *url.URL, err error) } + +// MockOpenIDConnectContext is a minimal implementation of OpenIDConnectContext for the purpose of testing. +type MockOpenIDConnectContext struct { + context.Context + + MockIssuerURL *url.URL +} + +// IssuerURL returns the MockIssuerURL. +func (m *MockOpenIDConnectContext) IssuerURL() (issuerURL *url.URL, err error) { + return m.MockIssuerURL, nil +} diff --git a/internal/oidc/types_test.go b/internal/oidc/types_test.go index d604ad009..a17f3e3ad 100644 --- a/internal/oidc/types_test.go +++ b/internal/oidc/types_test.go @@ -96,11 +96,3 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) { assert.NotNil(t, session.Claims.Extra) assert.Nil(t, session.Claims.AuthenticationMethodsReferences) } - -func MustParseRequestURI(input string) *url.URL { - if requestURI, err := url.ParseRequestURI(input); err != nil { - panic(err) - } else { - return requestURI - } -}