feat(oidc): client_secret_jwt client auth (#5253)

This adds the authentication machinery for the client_secret_jwt to the Default Client Authentication Strategy.

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
pull/5279/head
James Elliott 2023-05-15 09:51:59 +10:00 committed by GitHub
parent c729d33da6
commit 1dbfbc5f88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2170 additions and 153 deletions

View File

@ -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'

View File

@ -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" >}}

View File

@ -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 |

View File

@ -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'

View File

@ -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"`

View File

@ -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",

View File

@ -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 (

View File

@ -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) {

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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())))

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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 (

View File

@ -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
}

View File

@ -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{},

View File

@ -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.

View File

@ -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(),
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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
}
}