feat(oidc): per-client pkce enforcement policy (#4692)

This implements a per-client PKCE enforcement policy with the ability to enforce that it's used, and the specific challenge mode.
pull/4694/head
James Elliott 2023-01-04 02:03:23 +11:00 committed by GitHub
parent 1e86cb9ca8
commit adaf069eab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 327 additions and 102 deletions

View File

@ -1394,15 +1394,9 @@ notifier:
## Sets the client to public. This should typically not be set, please see the documentation for usage.
# public: false
## The policy to require for this client; one_factor or two_factor.
# authorization_policy: two_factor
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## Audience this client is allowed to request.
# audience: []
@ -1414,10 +1408,6 @@ notifier:
# - email
# - profile
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## Grant Types configures which grants this client can obtain.
## It's not recommended to define this unless you know what you're doing.
# grant_types:
@ -1435,6 +1425,23 @@ notifier:
# - query
# - fragment
## The policy to require for this client; one_factor or two_factor.
# authorization_policy: two_factor
## Enforces the use of PKCE for this client when set to true.
# enforce_pkce: false
## Enforces the use of PKCE for this client when configured, and enforces the specified challenge method.
## Options are 'plain' and 'S256'.
# pkce_challenge_method: S256
## The algorithm used to sign userinfo endpoint responses for this client, either none or RS256.
# userinfo_signing_algorithm: none
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
...

View File

@ -404,12 +404,92 @@ useful for SPA's and CLI tools. This option requires setting the [client secret]
In addition to the standard rules for redirect URIs, public clients can use the `urn:ietf:wg:oauth:2.0:oob` redirect
URI.
#### redirect_uris
{{< confkey type="list(string)" required="yes" >}}
A list of valid callback URIs this client will redirect to. All other callbacks will be considered unsafe. The URIs are
case-sensitive and they differ from application to application - the community has provided
[a list of URL´s for common applications](../../integration/openid-connect/introduction.md).
Some restrictions that have been placed on clients and
their redirect URIs are as follows:
1. If a client attempts to authorize with Authelia and its redirect URI is not listed in the client configuration the
attempt to authorize will fail and an error will be generated.
2. The redirect URIs are case-sensitive.
3. The URI must include a scheme and that scheme must be one of `http` or `https`.
4. The client can ignore rule 3 and use `urn:ietf:wg:oauth:2.0:oob` if it is a [public](#public) client type.
#### audience
{{< confkey type="list(string)" required="no" >}}
A list of audiences this client is allowed to request.
#### scopes
{{< confkey type="list(string)" default="openid, groups, profile, email" required="no" >}}
A list of scopes to allow this client to consume. See
[scope definitions](../../integration/openid-connect/introduction.md#scope-definitions) for more information. The
documentation for the application you want to use with Authelia will most-likely provide you with the scopes to allow.
#### grant_types
{{< confkey type="list(string)" default="refresh_token, authorization_code" required="no" >}}
A list of grant types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `implicit`, `refresh_token`, `authorization_code`, `password`,
`client_credentials`.
#### response_types
{{< confkey type="list(string)" default="code" required="no" >}}
A list of response types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `code`, `code id_token`, `id_token`, `token id_token`, `token`,
`token id_token code`.
#### response_modes
{{< confkey type="list(string)" default="form_post, query, fragment" required="no" >}}
A list of response modes this client can return. It is recommended that this isn't configured at this time unless you
know what you're doing. Potential values are `form_post`, `query`, and `fragment`.
#### authorization_policy
{{< confkey type="string" default="two_factor" required="no" >}}
The authorization policy for this client: either `one_factor` or `two_factor`.
#### enforce_pkce
{{< confkey type="bool" default="false" required="no" >}}
This setting enforces the use of [PKCE] for this individual client. To enforce it for all clients see the global
[enforce_pkce](#enforcepkce) setting.
#### pkce_challenge_method
{{< confkey type="string" default="" required="no" >}}
This setting enforces the use of the specified [PKCE] challenge method for this individual client. This setting also
effectively enables the [enforce_pkce](#enforcepkce-1) option for this client.
Valid values are an empty string, `plain`, or `S256`. It should be noted that `S256` is strongly recommended if the
relying party supports it.
#### userinfo_signing_algorithm
{{< confkey type="string" default="none" required="no" >}}
The algorithm used to sign the userinfo endpoint responses. This can either be `none` or `RS256`.
See the [integration guide](../../integration/openid-connect/introduction.md#user-information-signing-algorithm) for
more information.
#### consent_mode
{{< confkey type="string" default="auto" required="no" >}}
@ -442,69 +522,6 @@ match exactly with the granted scopes/audience.
[consent_mode]: #consentmode
#### audience
{{< confkey type="list(string)" required="no" >}}
A list of audiences this client is allowed to request.
#### scopes
{{< confkey type="list(string)" default="openid, groups, profile, email" required="no" >}}
A list of scopes to allow this client to consume. See
[scope definitions](../../integration/openid-connect/introduction.md#scope-definitions) for more information. The
documentation for the application you want to use with Authelia will most-likely provide you with the scopes to allow.
#### redirect_uris
{{< confkey type="list(string)" required="yes" >}}
A list of valid callback URIs this client will redirect to. All other callbacks will be considered unsafe. The URIs are
case-sensitive and they differ from application to application - the community has provided
[a list of URL´s for common applications](../../integration/openid-connect/introduction.md).
Some restrictions that have been placed on clients and
their redirect URIs are as follows:
1. If a client attempts to authorize with Authelia and its redirect URI is not listed in the client configuration the
attempt to authorize will fail and an error will be generated.
2. The redirect URIs are case-sensitive.
3. The URI must include a scheme and that scheme must be one of `http` or `https`.
4. The client can ignore rule 3 and use `urn:ietf:wg:oauth:2.0:oob` if it is a [public](#public) client type.
#### grant_types
{{< confkey type="list(string)" default="refresh_token, authorization_code" required="no" >}}
A list of grant types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `implicit`, `refresh_token`, `authorization_code`, `password`,
`client_credentials`.
#### response_types
{{< confkey type="list(string)" default="code" required="no" >}}
A list of response types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `code`, `code id_token`, `id_token`, `token id_token`, `token`,
`token id_token code`.
#### response_modes
{{< confkey type="list(string)" default="form_post, query, fragment" required="no" >}}
A list of response modes this client can return. It is recommended that this isn't configured at this time unless you
know what you're doing. Potential values are `form_post`, `query`, and `fragment`.
#### userinfo_signing_algorithm
{{< confkey type="string" default="none" required="no" >}}
The algorithm used to sign the userinfo endpoint responses. This can either be `none` or `RS256`.
See the [integration guide](../../integration/openid-connect/introduction.md#user-information-signing-algorithm) for
more information.
## Integration
To integrate Authelia's [OpenID Connect] implementation with a relying party please see the

View File

@ -2,7 +2,7 @@
title: "Templating"
description: "A reference guide on the templates system"
lead: "This section contains reference documentation for Authelia's templating capabilities."
date: 2022-12-23T18:31:05+11:00
date: 2022-12-23T21:58:54+11:00
draft: false
images: []
menu:

View File

@ -64,7 +64,7 @@ Feature List:
Feature List:
* [Proof Key Code Exchange (PKCE)](https://www.rfc-editor.org/rfc/rfc7636.html) for Authorization Code Flow
* [Proof Key Code Exchange (PKCE)] for Authorization Code Flow
* Claims:
* `preferred_username` - sending the username in this claim instead of the `sub` claim.
@ -115,8 +115,8 @@ Feature List:
{{< roadmap-status stage="in-progress" version="v4.38.0" >}}
* [OAuth 2.0 Pushed Authorization Requests](https://www.rfc-editor.org/rfc/rfc9126.html)
* Per-Client [Proof Key Code Exchange (PKCE)] Policy
### Beta 7
@ -219,3 +219,4 @@ The `preferred_username` claim was missing and was fixed.
[OpenID Connect Core (Subject Identifier Types)]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
[OpenID Connect Core (Pairwise Identifier Algorithm)]: https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
[OpenID Connect Core (Mandatory to Implement Features for All OpenID Providers)]: https://openid.net/specs/openid-connect-core-1_0.html#ServerMTI
[Proof Key Code Exchange (PKCE)]: https://www.rfc-editor.org/rfc/rfc7636.html

View File

@ -1394,15 +1394,9 @@ notifier:
## Sets the client to public. This should typically not be set, please see the documentation for usage.
# public: false
## The policy to require for this client; one_factor or two_factor.
# authorization_policy: two_factor
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## Audience this client is allowed to request.
# audience: []
@ -1414,10 +1408,6 @@ notifier:
# - email
# - profile
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## Grant Types configures which grants this client can obtain.
## It's not recommended to define this unless you know what you're doing.
# grant_types:
@ -1435,6 +1425,23 @@ notifier:
# - query
# - fragment
## The policy to require for this client; one_factor or two_factor.
# authorization_policy: two_factor
## Enforces the use of PKCE for this client when set to true.
# enforce_pkce: false
## Enforces the use of PKCE for this client when configured, and enforces the specified challenge method.
## Options are 'plain' and 'S256'.
# pkce_challenge_method: S256
## The algorithm used to sign userinfo endpoint responses for this client, either none or RS256.
# userinfo_signing_algorithm: none
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
...

View File

@ -57,10 +57,13 @@ type OpenIDConnectClientConfiguration struct {
ResponseTypes []string `koanf:"response_types"`
ResponseModes []string `koanf:"response_modes"`
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
Policy string `koanf:"authorization_policy"`
EnforcePKCE bool `koanf:"enforce_pkce"`
PKCEChallengeMethod string `koanf:"pkce_challenge_method"`
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
ConsentMode string `koanf:"consent_mode"`
ConsentPreConfiguredDuration *time.Duration `koanf:"pre_configured_consent_duration"`
}

View File

@ -43,8 +43,10 @@ var Keys = []string{
"identity_providers.oidc.clients[].grant_types",
"identity_providers.oidc.clients[].response_types",
"identity_providers.oidc.clients[].response_modes",
"identity_providers.oidc.clients[].userinfo_signing_algorithm",
"identity_providers.oidc.clients[].authorization_policy",
"identity_providers.oidc.clients[].enforce_pkce",
"identity_providers.oidc.clients[].pkce_challenge_method",
"identity_providers.oidc.clients[].userinfo_signing_algorithm",
"identity_providers.oidc.clients[].consent_mode",
"identity_providers.oidc.clients[].pre_configured_consent_duration",
"authentication_backend.password_reset.disable",

View File

@ -6,7 +6,6 @@ import (
"github.com/go-webauthn/webauthn/protocol"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/oidc"
)
@ -172,6 +171,8 @@ const (
"invalid value: redirect uri '%s' must have the scheme but it is absent"
errFmtOIDCClientInvalidPolicy = "identity_providers: oidc: client '%s': option 'policy' must be 'one_factor' " +
"or 'two_factor' but it is configured as '%s'"
errFmtOIDCClientInvalidPKCEChallengeMethod = "identity_providers: oidc: client '%s': option 'pkce_challenge_method' must be 'plain' " +
"or 'S256' but it is configured as '%s'"
errFmtOIDCClientInvalidConsentMode = "identity_providers: oidc: client '%s': consent: option 'mode' must be one of " +
"'%s' but it is configured as '%s'"
errFmtOIDCClientInvalidEntry = "identity_providers: oidc: client '%s': option '%s' must only have the values " +

View File

@ -175,6 +175,13 @@ func validateOIDCClients(config *schema.OpenIDConnectConfiguration, val *schema.
val.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))
}
switch client.PKCEChallengeMethod {
case "", "plain", "S256":
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidPKCEChallengeMethod, client.ID, client.PKCEChallengeMethod))
}
validateOIDCClientConsentMode(c, config, val)
validateOIDCClientSectorIdentifier(client, val)
validateOIDCClientScopes(c, config, val)

View File

@ -331,6 +331,40 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
fmt.Sprintf(errFmtOIDCClientInvalidConsentMode, "client-bad-consent-mode", strings.Join(append(validOIDCClientConsentModes, "auto"), "', '"), "cap"),
},
},
{
Name: "InvalidPKCEChallengeMethod",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-bad-pkce-mode",
Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
PKCEChallengeMethod: "abc",
},
},
Errors: []string{
fmt.Sprintf(errFmtOIDCClientInvalidPKCEChallengeMethod, "client-bad-pkce-mode", "abc"),
},
},
{
Name: "InvalidPKCEChallengeMethodLowerCaseS256",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-bad-pkce-mode-s256",
Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
PKCEChallengeMethod: "s256",
},
},
Errors: []string{
fmt.Sprintf(errFmtOIDCClientInvalidPKCEChallengeMethod, "client-bad-pkce-mode-s256", "s256"),
},
},
}
for _, tc := range testCases {
@ -609,7 +643,7 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCClientRedirectURIPublic, "client-with-bad-redirect-uri", oauth2InstalledApp))
}
func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidPublicClients(t *testing.T) {
func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidClientOptions(t *testing.T) {
validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{
@ -640,6 +674,24 @@ func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidPublicClients(t *te
"http://127.0.0.1",
},
},
{
ID: "client-with-pkce-mode-plain",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://pkce.com",
},
PKCEChallengeMethod: "plain",
},
{
ID: "client-with-pkce-mode-S256",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://pkce.com",
},
PKCEChallengeMethod: "S256",
},
},
},
}

View File

@ -52,6 +52,16 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
return
}
if err = client.ValidateAuthorizationPolicy(requester); err != nil {
rfc := fosite.ErrorToRFC6749Error(err)
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the authorization policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription())
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
return
}
issuer = ctx.RootURL()
userSession := ctx.GetSession()

View File

@ -1,7 +1,10 @@
package oidc
import (
"fmt"
"github.com/ory/fosite"
"github.com/ory/x/errorsx"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization"
@ -18,6 +21,10 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
SectorIdentifier: config.SectorIdentifier.String(),
Public: config.Public,
EnforcePKCE: config.EnforcePKCE || config.PKCEChallengeMethod != "",
EnforcePKCEChallengeMethod: config.PKCEChallengeMethod != "",
PKCEChallengeMethod: config.PKCEChallengeMethod,
Audience: config.Audience,
Scopes: config.Scopes,
RedirectURIs: config.RedirectURIs,
@ -39,6 +46,29 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
return client
}
// ValidateAuthorizationPolicy is a helper function to validate additional policy constraints on a per-client basis.
func (c *Client) ValidateAuthorizationPolicy(r fosite.Requester) (err error) {
form := r.GetRequestForm()
if c.EnforcePKCE {
if form.Get("code_challenge") == "" {
return errorsx.WithStack(fosite.ErrInvalidRequest.
WithHint("Clients must include a code_challenge when performing the authorize code flow, but it is missing.").
WithDebug("The server is configured in a way that enforces PKCE for this client."))
}
if c.EnforcePKCEChallengeMethod {
if method := form.Get("code_challenge_method"); method != c.PKCEChallengeMethod {
return errorsx.WithStack(fosite.ErrInvalidRequest.
WithHint(fmt.Sprintf("Client must use code_challenge_method=%s, %s is not allowed.", c.PKCEChallengeMethod, method)).
WithDebug(fmt.Sprintf("The server is configured in a way that enforces PKCE %s as challenge method for this client.", c.PKCEChallengeMethod)))
}
}
}
return nil
}
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool {
if level == authentication.NotAuthenticated {
@ -48,11 +78,6 @@ func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) boo
return authorization.IsAuthLevelSufficient(level, c.Policy)
}
// GetID returns the ID.
func (c *Client) GetID() string {
return c.ID
}
// GetSectorIdentifier returns the SectorIdentifier for this client.
func (c *Client) GetSectorIdentifier() string {
return c.SectorIdentifier
@ -74,6 +99,11 @@ func (c *Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) Con
return body
}
// GetID returns the ID.
func (c *Client) GetID() string {
return c.ID
}
// GetHashedSecret returns the Secret.
func (c *Client) GetHashedSecret() []byte {
if c.Secret == nil {

View File

@ -217,6 +217,90 @@ func TestClient_GetResponseTypes(t *testing.T) {
assert.Equal(t, "id_token", responseTypes[1])
}
func TestNewClientPKCE(t *testing.T) {
testCases := []struct {
name string
have schema.OpenIDConnectClientConfiguration
expectedEnforcePKCE bool
expectedEnforcePKCEChallengeMethod bool
expected string
req *fosite.Request
err string
}{
{
"ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{},
false,
false,
"",
&fosite.Request{},
"",
},
{
"ShouldEnforcePKCEAndErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{EnforcePKCE: true},
true,
false,
"",
&fosite.Request{},
"invalid_request",
},
{
"ShouldEnforcePKCEAndNotErrorOnPKCERequest",
schema.OpenIDConnectClientConfiguration{EnforcePKCE: true},
true,
false,
"",
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
"",
},
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
true,
true,
"S256",
&fosite.Request{},
"invalid_request",
},
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
true,
true,
"S256",
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
"invalid_request",
},
{"ShouldEnforcePKCEFromChallengeMethodAndNotErrorOnValidRequest",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
true,
true,
"S256",
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}, "code_challenge_method": {"S256"}}},
"",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient(tc.have)
assert.Equal(t, tc.expectedEnforcePKCE, client.EnforcePKCE)
assert.Equal(t, tc.expectedEnforcePKCEChallengeMethod, client.EnforcePKCEChallengeMethod)
assert.Equal(t, tc.expected, client.PKCEChallengeMethod)
if tc.req != nil {
err := client.ValidateAuthorizationPolicy(tc.req)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
}
}
})
}
}
func TestClient_IsPublic(t *testing.T) {
c := Client{}

View File

@ -107,6 +107,10 @@ type Client struct {
SectorIdentifier string
Public bool
EnforcePKCE bool
EnforcePKCEChallengeMethod bool
PKCEChallengeMethod string
Audience []string
Scopes []string
RedirectURIs []string