Skip to content

Commit

Permalink
feat(oidc): client credentials grant type (#5729)
Browse files Browse the repository at this point in the history
Adds support for the client credentials grant type.

Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
  • Loading branch information
james-d-elliott committed Jul 30, 2023
1 parent 65cfb69 commit b829e1b
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 36 deletions.
16 changes: 8 additions & 8 deletions docs/content/en/integration/openid-connect/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,14 @@ The following describes the various [OAuth 2.0] and [OpenID Connect 1.0] grant t
field is both the required value for the `grant_type` parameter in the authorization request and the `grant_types`
configuration option.

| Grant Type | Supported | Value | Notes |
|:-----------------------------------------------:|:---------:|:----------------------------------------------:|:-----------------------------------------------------------------------------------------------:|
| [OAuth 2.0 Authorization Code] | Yes | `authorization_code` | |
| [OAuth 2.0 Resource Owner Password Credentials] | No | `password` | This Grant Type has been deprecated and should not normally be used |
| [OAuth 2.0 Client Credentials] | No | `client_credentials` | |
| [OAuth 2.0 Implicit] | Yes | `implicit` | This Grant Type has been deprecated and should not normally be used |
| [OAuth 2.0 Refresh Token] | Yes | `refresh_token` | This Grant Type should genreally only be used for clients which have the `offline_access` scope |
| [OAuth 2.0 Device Code] | No | `urn:ietf:params:oauth:grant-type:device_code` | |
| Grant Type | Supported | Value | Notes |
|:-----------------------------------------------:|:---------:|:----------------------------------------------:|:-------------------------------------------------------------------------------------------:|
| [OAuth 2.0 Authorization Code] | Yes | `authorization_code` | |
| [OAuth 2.0 Resource Owner Password Credentials] | No | `password` | This Grant Type has been deprecated as it's highly insecure and should not normally be used |
| [OAuth 2.0 Client Credentials] | Yes | `client_credentials` | |
| [OAuth 2.0 Implicit] | Yes | `implicit` | This Grant Type has been deprecated and should not normally be used |
| [OAuth 2.0 Refresh Token] | Yes | `refresh_token` | This Grant Type should only be used for clients which have the `offline_access` scope |
| [OAuth 2.0 Device Code] | No | `urn:ietf:params:oauth:grant-type:device_code` | |
|

[OAuth 2.0 Authorization Code]: https://datatracker.ietf.org/doc/html/rfc6749#section-1.3.1
Expand Down
37 changes: 23 additions & 14 deletions docs/content/en/roadmap/active/openid-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ Feature List:
* Client Auth Method `client_secret_jwt`
* Client Auth Method `private_key_jwt`
* Per-Client [RFC7636: Proof Key for Code Exchange (PKCE)] Policy
* [OAuth 2.0 Client Credentials Grant](https://datatracker.ietf.org/doc/html/rfc6749#section-4.4)
* Multiple Issuer JWKs:
* `RS256`, `RS384`, `RS512`
* `PS256`, `PS384`, `PS512`
Expand Down Expand Up @@ -168,39 +169,45 @@ Feature List:

This stage lists features which individually do not fit into a specific stage and may or may not be implemented.

#### OpenID Connect Dynamic Client Registration
#### OAuth 2.0 Authorization Server Metadata

{{< roadmap-status stage="complete" version="v4.34.0" >}}

See the [RFC8414: OAuth 2.0 Authorization Server Metadata] specification for more information.

#### OpenID Connect Dynamic Client Registration 1.0

{{< roadmap-status >}}

See the [OpenID Connect 1.0] website for the [OpenID Connect Dynamic Client Registration 1.0] specification.

#### OpenID Connect Back-Channel Logout
#### OpenID Connect Session Management 1.0

{{< roadmap-status >}}

See the [OpenID Connect 1.0] website for the [OpenID Connect Session Management 1.0] specification.

#### OpenID Connect Back-Channel Logout 1.0

{{< roadmap-status >}}

See the [OpenID Connect 1.0] website for the [OpenID Connect Back-Channel Logout 1.0] specification.

Should be implemented alongside [Dynamic Client Registration](#openid-connect-dynamic-client-registration).

#### OpenID Connect Front-Channel Logout
#### OpenID Connect Front-Channel Logout 1.0

{{< roadmap-status >}}

See the [OpenID Connect 1.0] website for the [OpenID Connect Front-Channel Logout 1.0] specification.

Should be implemented alongside [Dynamic Client Registration](#openid-connect-dynamic-client-registration).

#### OAuth 2.0 Authorization Server Metadata

{{< roadmap-status stage="complete" version="v4.34.0" >}}

See the [IETF Specification RFC8414](https://datatracker.ietf.org/doc/html/rfc8414) for more information.

#### OpenID Connect Session Management
#### OpenID Connect RP-Initiated Logout 1.0

{{< roadmap-status >}}

See the [OpenID Connect 1.0] website for the [OpenID Connect Session Management 1.0] specification.
See the [OpenID Connect 1.0] website for the [OpenID Connect RP-Initiated Logout 1.0] specification.

#### End-User Scope Grants

Expand All @@ -227,10 +234,11 @@ The `preferred_username` claim was missing and was fixed.
[RFC4122]: https://datatracker.ietf.org/doc/html/rfc4122

[OpenID Connect 1.0]: https://openid.net/connect/
[OpenID Connect Front-Channel Logout 1.0]: https://openid.net/specs/openid-connect-frontchannel-1_0.html
[OpenID Connect Back-Channel Logout 1.0]: https://openid.net/specs/openid-connect-backchannel-1_0.html
[OpenID Connect Session Management 1.0]: https://openid.net/specs/openid-connect-session-1_0.html
[OpenID Connect Dynamic Client Registration 1.0]: https://openid.net/specs/openid-connect-registration-1_0.html
[OpenID Connect Session Management 1.0]: https://openid.net/specs/openid-connect-session-1_0.html
[OpenID Connect Back-Channel Logout 1.0]: https://openid.net/specs/openid-connect-backchannel-1_0.html
[OpenID Connect Front-Channel Logout 1.0]: https://openid.net/specs/openid-connect-frontchannel-1_0.html
[OpenID Connect RP-Initiated Logout 1.0]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html

[OpenID Connect Core 1.0 (ID Token)]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
[OpenID Connect Core 1.0 (Subject Identifier Types)]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
Expand All @@ -240,3 +248,4 @@ The `preferred_username` claim was missing and was fixed.
[RFC7636: Proof Key for Code Exchange (PKCE)]: https://datatracker.ietf.org/doc/html/rfc7636
[RFC7523: JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants]: https://datatracker.ietf.org/doc/html/rfc7523
[RFC9126: OAuth 2.0 Pushed Authorization Requests]: https://datatracker.ietf.org/doc/html/rfc9126
[RFC8414: OAuth 2.0 Authorization Server Metadata]: https://datatracker.ietf.org/doc/html/rfc8414
5 changes: 4 additions & 1 deletion internal/configuration/validator/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ const (
"'grant_types' should only have grant type values which are valid with the configured 'response_types' for the client but '%s' expects a response type %s such as %s but the response types are %s"
errFmtOIDCClientInvalidGrantTypeRefresh = "identity_providers: oidc: clients: client '%s': option " +
"'grant_types' should only have the 'refresh_token' value if the client is also configured with the 'offline_access' scope"
errFmtOIDCClientInvalidGrantTypePublic = "identity_providers: oidc: clients: client '%s': option 'grant_types' " +
"should only have the '%s' value if it is of the confidential client type but it's of the public client type"

errFmtOIDCClientInvalidRefreshTokenOptionWithoutCodeResponseType = "identity_providers: oidc: clients: client '%s': option " +
"'%s' should only have the values %s if the client is also configured with a 'response_type' such as %s which respond with authorization codes"

Expand Down Expand Up @@ -464,7 +467,7 @@ var (
validOIDCClientResponseTypesImplicitFlow = []string{oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth}
validOIDCClientResponseTypesHybridFlow = []string{oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth}
validOIDCClientResponseTypesRefreshToken = []string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth}
validOIDCClientGrantTypes = []string{oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeAuthorizationCode}
validOIDCClientGrantTypes = []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeClientCredentials, oidc.GrantTypeRefreshToken}

validOIDCClientTokenEndpointAuthMethods = []string{oidc.ClientAuthMethodNone, oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodPrivateKeyJWT, oidc.ClientAuthMethodClientSecretJWT}
validOIDCClientTokenEndpointAuthMethodsConfidential = []string{oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodPrivateKeyJWT}
Expand Down
14 changes: 9 additions & 5 deletions internal/configuration/validator/identity_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -654,17 +654,21 @@ func validateOIDCClientGrantTypesSetDefaults(c int, config *schema.OpenIDConnect
func validateOIDCClientGrantTypesCheckRelated(c int, config *schema.OpenIDConnect, val *schema.StructValidator, errDeprecatedFunc func()) {
for _, grantType := range config.Clients[c].GrantTypes {
switch grantType {
case oidc.GrantTypeAuthorizationCode:
if !utils.IsStringInSlice(oidc.ResponseTypeAuthorizationCodeFlow, config.Clients[c].ResponseTypes) && !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesHybridFlow, config.Clients[c].ResponseTypes) {
errDeprecatedFunc()

val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidGrantTypeMatch, config.Clients[c].ID, grantType, "for either the authorization code or hybrid flow", strJoinOr(append([]string{oidc.ResponseTypeAuthorizationCodeFlow}, validOIDCClientResponseTypesHybridFlow...)), strJoinAnd(config.Clients[c].ResponseTypes)))
}
case oidc.GrantTypeImplicit:
if !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesImplicitFlow, config.Clients[c].ResponseTypes) && !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesHybridFlow, config.Clients[c].ResponseTypes) {
errDeprecatedFunc()

val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidGrantTypeMatch, config.Clients[c].ID, grantType, "for either the implicit or hybrid flow", strJoinOr(append(append([]string{}, validOIDCClientResponseTypesImplicitFlow...), validOIDCClientResponseTypesHybridFlow...)), strJoinAnd(config.Clients[c].ResponseTypes)))
}
case oidc.GrantTypeAuthorizationCode:
if !utils.IsStringInSlice(oidc.ResponseTypeAuthorizationCodeFlow, config.Clients[c].ResponseTypes) && !utils.IsStringSliceContainsAny(validOIDCClientResponseTypesHybridFlow, config.Clients[c].ResponseTypes) {
errDeprecatedFunc()

val.PushWarning(fmt.Errorf(errFmtOIDCClientInvalidGrantTypeMatch, config.Clients[c].ID, grantType, "for either the authorization code or hybrid flow", strJoinOr(append([]string{oidc.ResponseTypeAuthorizationCodeFlow}, validOIDCClientResponseTypesHybridFlow...)), strJoinAnd(config.Clients[c].ResponseTypes)))
case oidc.GrantTypeClientCredentials:
if config.Clients[c].Public {
val.Push(fmt.Errorf(errFmtOIDCClientInvalidGrantTypePublic, config.Clients[c].ID, oidc.GrantTypeClientCredentials))
}
case oidc.GrantTypeRefreshToken:
if !utils.IsStringSliceContainsAny([]string{oidc.ScopeOfflineAccess, oidc.ScopeOffline}, config.Clients[c].Scopes) {
Expand Down
53 changes: 49 additions & 4 deletions internal/configuration/validator/identity_providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T)
ValidateIdentityProviders(config, validator)

require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: clients: client 'good_id': option 'grant_types' must only have the values 'implicit', 'refresh_token', or 'authorization_code' but the values 'bad_grant_type' are present")
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: clients: client 'good_id': option 'grant_types' must only have the values 'authorization_code', 'implicit', 'client_credentials', or 'refresh_token' but the values 'bad_grant_type' are present")
}

func TestShouldNotErrorOnCertificateValid(t *testing.T) {
Expand Down Expand Up @@ -1136,7 +1136,7 @@ func TestValidateOIDCClients(t *testing.T) {
},
nil,
[]string{
"identity_providers: oidc: clients: client 'test': option 'grant_types' must only have the values 'implicit', 'refresh_token', or 'authorization_code' but the values 'invalid' are present",
"identity_providers: oidc: clients: client 'test': option 'grant_types' must only have the values 'authorization_code', 'implicit', 'client_credentials', or 'refresh_token' but the values 'invalid' are present",
},
},
{
Expand All @@ -1160,6 +1160,51 @@ func TestValidateOIDCClients(t *testing.T) {
},
nil,
},
{
"ShouldRaiseErrorOnInvalidGrantTypesForPublicClient",
func(have *schema.OpenIDConnect) {
have.Clients[0].Public = true
have.Clients[0].Secret = nil
},
nil,
tcv{
nil,
nil,
nil,
[]string{oidc.GrantTypeClientCredentials},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeClientCredentials},
},
nil,
[]string{
"identity_providers: oidc: clients: client 'test': option 'grant_types' should only have the 'client_credentials' value if it is of the confidential client type but it's of the public client type",
},
},
{
"ShouldNotRaiseErrorOnValidGrantTypesForConfidentialClient",
func(have *schema.OpenIDConnect) {
have.Clients[0].Public = false
},
nil,
tcv{
nil,
nil,
nil,
[]string{oidc.GrantTypeClientCredentials},
},
tcv{
[]string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail},
[]string{oidc.ResponseTypeAuthorizationCodeFlow},
[]string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery},
[]string{oidc.GrantTypeClientCredentials},
},
nil,
nil,
},
{
"ShouldRaiseErrorOnGrantTypeRefreshTokenWithoutScopeOfflineAccess",
nil,
Expand Down Expand Up @@ -1431,10 +1476,10 @@ func TestValidateOIDCClients(t *testing.T) {
{
"ShouldRaiseErrorOnInvalidClientAuthMethod",
func(have *schema.OpenIDConnect) {
have.Clients[0].TokenEndpointAuthMethod = "client_credentials"
have.Clients[0].TokenEndpointAuthMethod = oidc.GrantTypeClientCredentials
},
func(t *testing.T, have *schema.OpenIDConnect) {
assert.Equal(t, "client_credentials", have.Clients[0].TokenEndpointAuthMethod)
assert.Equal(t, oidc.GrantTypeClientCredentials, have.Clients[0].TokenEndpointAuthMethod)
},
tcv{
nil,
Expand Down
2 changes: 1 addition & 1 deletion internal/handlers/handler_oidc_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func OpenIDConnectTokenPOST(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter
ctx.Logger.Debugf("Access Request with id '%s' on client with id '%s' is being processed", requester.GetID(), client.GetID())

// If this is a client_credentials grant, grant all scopes the client is allowed to perform.
if requester.GetGrantTypes().ExactOne("client_credentials") {
if requester.GetGrantTypes().ExactOne(oidc.GrantTypeClientCredentials) {
for _, scope := range requester.GetRequestedScopes() {
if fosite.HierarchicScopeStrategy(client.GetScopes(), scope) {
requester.GrantScope(scope)
Expand Down
1 change: 1 addition & 0 deletions internal/oidc/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const (
GrantTypeImplicit = implicit
GrantTypeRefreshToken = "refresh_token"
GrantTypeAuthorizationCode = "authorization_code"
GrantTypeClientCredentials = "client_credentials"
)

// Client Auth Method strings.
Expand Down
1 change: 1 addition & 0 deletions internal/oidc/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnect) (config Ope
GrantTypesSupported: []string{
GrantTypeAuthorizationCode,
GrantTypeImplicit,
GrantTypeClientCredentials,
GrantTypeRefreshToken,
},
ResponseModesSupported: []string{
Expand Down
7 changes: 4 additions & 3 deletions internal/oidc/discovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func TestNewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *test
assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone)

assert.Equal(t, []string{oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodNone}, disco.IntrospectionEndpointAuthMethodsSupported)
assert.Equal(t, []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken}, disco.GrantTypesSupported)
assert.Equal(t, []string{oidc.GrantTypeAuthorizationCode, oidc.GrantTypeImplicit, oidc.GrantTypeClientCredentials, oidc.GrantTypeRefreshToken}, disco.GrantTypesSupported)
assert.Equal(t, []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512}, disco.RevocationEndpointAuthSigningAlgValuesSupported)
assert.Equal(t, []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512, oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA512}, disco.TokenEndpointAuthSigningAlgValuesSupported)
assert.Equal(t, []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgNone}, disco.IDTokenSigningAlgValuesSupported)
Expand Down Expand Up @@ -311,10 +311,11 @@ func TestNewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T)
assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, oidc.ClientAuthMethodPrivateKeyJWT)
assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, oidc.ClientAuthMethodNone)

assert.Len(t, disco.GrantTypesSupported, 3)
assert.Len(t, disco.GrantTypesSupported, 4)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeAuthorizationCode)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeRefreshToken)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeImplicit)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeClientCredentials)
assert.Contains(t, disco.GrantTypesSupported, oidc.GrantTypeRefreshToken)

assert.Len(t, disco.ClaimsSupported, 18)
assert.Contains(t, disco.ClaimsSupported, oidc.ClaimAuthenticationMethodsReference)
Expand Down

0 comments on commit b829e1b

Please sign in to comment.