Skip to content

Commit

Permalink
feat(oidc): pushed authorization requests (#4546)
Browse files Browse the repository at this point in the history
This implements RFC9126 OAuth 2.0 Pushed Authorization Requests. See https://datatracker.ietf.org/doc/html/rfc9126 for the specification details.
  • Loading branch information
james-d-elliott committed Mar 6, 2023
1 parent 42671d3 commit ff6be40
Show file tree
Hide file tree
Showing 42 changed files with 871 additions and 240 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,23 @@ Allows [PKCE] `plain` challenges when set to `true`.
*__Security Notice:__* Changing this value is generally discouraged. Applications should use the `S256` [PKCE] challenge
method instead.

### pushed_authorizations

Controls the behaviour of [Pushed Authorization Requests].

#### enforce

{{< confkey type="boolean" default="false" required="no" >}}

When enabled all authorization requests must use the [Pushed Authorization Requests] flow.

#### context_lifespan

{{< confkey type="duration" default="5m" required="no" >}}

The maximum amount of time between the [Pushed Authorization Requests] flow being initiated and the generated
`request_uri` being utilized by a client.

### cors

Some [OpenID Connect 1.0] Endpoints need to allow cross-origin resource sharing, however some are optional. This section allows
Expand All @@ -285,6 +302,7 @@ A list of endpoints to configure with cross-origin resource sharing headers. It
option is at least in this list. The potential endpoints which this can be enabled on are as follows:

* authorization
* pushed-authorization-request
* token
* revocation
* introspection
Expand Down Expand Up @@ -472,6 +490,12 @@ See the [Response Modes](../../integration/openid-connect/introduction.md#respon

The authorization policy for this client: either `one_factor` or `two_factor`.

#### enforce_par

{{< confkey type="boolean" default="false" required="no" >}}

Enforces the use of a [Pushed Authorization Requests] flow for this client.

#### enforce_pkce

{{< confkey type="bool" default="false" required="no" >}}
Expand Down Expand Up @@ -550,3 +574,4 @@ To integrate Authelia's [OpenID Connect 1.0] implementation with a relying party
[Authorization Code Flow]: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth
[Subject Identifier Type]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
[Pairwise Identifier Algorithm]: https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
[Pushed Authorization Requests]: https://datatracker.ietf.org/doc/html/rfc9126
1 change: 1 addition & 0 deletions docs/content/en/configuration/storage/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ this instance if you wanted to downgrade to pre1 you would need to use an Authel
| 5 | 4.35.1 | Fixed the oauth2_consent_session table to accept NULL subjects for users who are not yet signed in |
| 6 | 4.37.0 | Adjusted the OpenID Connect tables to allow pre-configured consent improvements |
| 7 | 4.37.3 | Fixed some schema inconsistencies most notably the MySQL/MariaDB Engine and Collation |
| 8 | 4.38.0 | OpenID Connect 1.0 Pushed Authorization Requests |
88 changes: 77 additions & 11 deletions docs/content/en/integration/openid-connect/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,71 @@ These endpoints can be utilized to discover other endpoints and metadata about t

These endpoints implement OpenID Connect elements.

| Endpoint | Path | Discovery Attribute |
|:-------------------:|:-----------------------------------------------:|:----------------------:|
| [JSON Web Key Sets] | https://auth.example.com/jwks.json | jwks_uri |
| [Authorization] | https://auth.example.com/api/oidc/authorization | authorization_endpoint |
| [Token] | https://auth.example.com/api/oidc/token | token_endpoint |
| [UserInfo] | https://auth.example.com/api/oidc/userinfo | userinfo_endpoint |
| [Introspection] | https://auth.example.com/api/oidc/introspection | introspection_endpoint |
| [Revocation] | https://auth.example.com/api/oidc/revocation | revocation_endpoint |
| Endpoint | Path | Discovery Attribute |
|:-------------------------------:|:--------------------------------------------------------------:|:-------------------------------------:|
| [JSON Web Key Set] | https://auth.example.com/jwks.json | jwks_uri |
| [Authorization] | https://auth.example.com/api/oidc/authorization | authorization_endpoint |
| [Pushed Authorization Requests] | https://auth.example.com/api/oidc/pushed-authorization-request | pushed_authorization_request_endpoint |
| [Token] | https://auth.example.com/api/oidc/token | token_endpoint |
| [UserInfo] | https://auth.example.com/api/oidc/userinfo | userinfo_endpoint |
| [Introspection] | https://auth.example.com/api/oidc/introspection | introspection_endpoint |
| [Revocation] | https://auth.example.com/api/oidc/revocation | revocation_endpoint |

## Security

The following information covers some security topics some users may wish to be familiar with.

#### Pushed Authorization Requests Endpoint

The [Pushed Authorization Requests] endpoint is discussed in depth in [RFC9126] as well as in the
[OAuth 2.0 Pushed Authorization Requests](https://oauth.net/2/pushed-authorization-requests/) documentation.

Essentially it's a special endpoint that takes the same parameters as the [Authorization] endpoint (including
[Proof Key Code Exchange](#proof-key-code-exchange)) with a few caveats:

1. The same [Client Authentication] mechanism required by the [Token] endpoint **MUST** be used.
2. The request **MUST** use the [HTTP POST method].
3. The request **MUST** use the `application/x-www-form-urlencoded` content type (i.e. the parameters **MUST** be in the
body, not the URI).
4. The request **MUST** occur over the back-channel.

The response of this endpoint is a JSON Object with two key-value pairs:
- `request_uri`
- `expires_in`

The `expires_in` indicates how long the `request_uri` is valid for. The `request_uri` is used as a parameter to the
[Authorization] endpoint instead of the standard parameters (as the `request_uri` parameter).

The advantages of this approach are as follows:

1. [Pushed Authorization Requests] cannot be created or influenced by any party other than the Relying Party (client).
2. Since you can force all [Authorization] requests to be initiated via [Pushed Authorization Requests] you drastically
improve the authorization flows resistance to phishing attacks (this can be done globally or on a per-client basis).
3. Since the [Pushed Authorization Requests] endpoint requires all of the same [Client Authentication] mechanisms as the
[Token] endpoint:
1. Clients using the confidential [Client Type] can't have [Pushed Authorization Requests] generated by parties who do not
have the credentials.
2. Clients using the public [Client Type] and utilizing [Proof Key Code Exchange](#proof-key-code-exchange) never
transmit the verifier over any front-channel making even the `plain` challenge method relatively secure.

#### Proof Key Code Exchange

The [Proof Key Code Exchange] mechanism is discussed in depth in [RFC7636] as well as in the
[OAuth 2.0 Proof Key Code Exchange](https://oauth.net/2/pkce/) documentation.

Essentially a random opaque value is generated by the Relying Party and optionally (but recommended) passed through a
SHA256 hash. The original value is saved by the Relying Party, and the hashed value is sent in the [Authorization]
request in the `code_verifier` parameter with the `code_challenge_method` set to `S256` (or `plain` using a bad practice
of not hashing the opaque value).

When the Relying Party requests the token from the [Token] endpoint, they must include the `code_verifier` parameter
again (in the body), but this time they send the value without it being hashed.

The advantages of this approach are as follows:

1. Provided the value was hashed it's certain that the Relying Party which generated the authorization request is the
same party as the one requesting the token or is permitted by the Relying Party to make this request.
2. Even when using the public [Client Type] there is a form of authentication on the [Token] endpoint.

[ID Token]: https://openid.net/specs/openid-connect-core-1_0.html#IDToken
[Access Token]: https://datatracker.ietf.org/doc/html/rfc6749#section-1.4
Expand All @@ -230,14 +287,23 @@ These endpoints implement OpenID Connect elements.
[OpenID Connect Discovery]: https://openid.net/specs/openid-connect-discovery-1_0.html
[OAuth 2.0 Authorization Server Metadata]: https://datatracker.ietf.org/doc/html/rfc8414

[JSON Web Key Sets]: https://datatracker.ietf.org/doc/html/rfc7517#section-5
[JSON Web Key Set]: https://datatracker.ietf.org/doc/html/rfc7517#section-5

[Authorization]: https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint
[Pushed Authorization Requests]: https://datatracker.ietf.org/doc/html/rfc9126
[Token]: https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint
[UserInfo]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
[Introspection]: https://datatracker.ietf.org/doc/html/rfc7662
[Revocation]: https://datatracker.ietf.org/doc/html/rfc7009
[Proof Key Code Exchange]: https://www.rfc-editor.org/rfc/rfc7636.html

[RFC8176]: https://datatracker.ietf.org/doc/html/rfc8176
[RFC4122]: https://datatracker.ietf.org/doc/html/rfc4122
[Subject Identifier Types]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
[Client Authentication]: https://datatracker.ietf.org/doc/html/rfc6749#section-2.3
[Client Type]: https://oauth.net/2/client-types/
[HTTP POST method]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
[Proof Key Code Exchange]: #proof-key-code-exchange

[RFC4122]: https://datatracker.ietf.org/doc/html/rfc4122
[RFC7636]: https://datatracker.ietf.org/doc/html/rfc7636
[RFC8176]: https://datatracker.ietf.org/doc/html/rfc8176
[RFC9126]: https://datatracker.ietf.org/doc/html/rfc9126
2 changes: 1 addition & 1 deletion docs/data/configkeys.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions internal/configuration/schema/identity_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,17 @@ type OpenIDConnectConfiguration struct {
EnablePKCEPlainChallenge bool `koanf:"enable_pkce_plain_challenge"`

CORS OpenIDConnectCORSConfiguration `koanf:"cors"`
PAR OpenIDConnectPARConfiguration `koanf:"pushed_authorizations"`

Clients []OpenIDConnectClientConfiguration `koanf:"clients"`
}

// OpenIDConnectPARConfiguration represents an OpenID Connect PAR config.
type OpenIDConnectPARConfiguration struct {
Enforce bool `koanf:"enforce"`
ContextLifespan time.Duration `koanf:"context_lifespan"`
}

// OpenIDConnectCORSConfiguration represents an OpenID Connect CORS config.
type OpenIDConnectCORSConfiguration struct {
Endpoints []string `koanf:"endpoints"`
Expand All @@ -59,6 +66,7 @@ type OpenIDConnectClientConfiguration struct {

Policy string `koanf:"authorization_policy"`

EnforcePAR bool `koanf:"enforce_par"`
EnforcePKCE bool `koanf:"enforce_pkce"`

PKCEChallengeMethod string `koanf:"pkce_challenge_method"`
Expand Down
3 changes: 3 additions & 0 deletions internal/configuration/schema/keys.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion internal/configuration/validator/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ var (
validOIDCGrantTypes = []string{oidc.GrantTypeImplicit, oidc.GrantTypeRefreshToken, oidc.GrantTypeAuthorizationCode, oidc.GrantTypePassword, oidc.GrantTypeClientCredentials}
validOIDCResponseModes = []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment}
validOIDCUserinfoAlgorithms = []string{oidc.SigningAlgorithmNone, oidc.SigningAlgorithmRSAWithSHA256}
validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo}
validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointPushedAuthorizationRequest, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo}
validOIDCClientConsentModes = []string{"auto", oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()}
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) {

require.Len(t, validator.Errors(), 1)

assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'endpoints' contains an invalid value 'invalid_endpoint': must be one of 'authorization', 'token', 'introspection', 'revocation', 'userinfo'")
assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: cors: option 'endpoints' contains an invalid value 'invalid_endpoint': must be one of 'authorization', 'pushed-authorization-request', 'token', 'introspection', 'revocation', 'userinfo'")
}

func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) {
Expand Down
79 changes: 74 additions & 5 deletions internal/handlers/handler_oidc_authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,20 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
return
}

if err = client.ValidateAuthorizationPolicy(requester); err != nil {
if err = client.ValidatePARPolicy(requester, ctx.Providers.OpenIDConnect.GetPushedAuthorizeRequestURIPrefix(ctx)); 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.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the PAR policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription())

ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)

return
}

if err = client.ValidatePKCEPolicy(requester); err != nil {
rfc := fosite.ErrorToRFC6749Error(err)

ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the PKCE policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription())

ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)

Expand Down Expand Up @@ -95,13 +105,13 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr

ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID)

oidcSession := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(),
userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester)

ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v",
requester.GetID(), oidcSession.ClientID, oidcSession.Subject, oidcSession.Username, oidcSession.Claims)
requester.GetID(), session.ClientID, session.Subject, session.Username, session.Claims)

if responder, err = ctx.Providers.OpenIDConnect.NewAuthorizeResponse(ctx, requester, oidcSession); err != nil {
if responder, err = ctx.Providers.OpenIDConnect.NewAuthorizeResponse(ctx, requester, session); err != nil {
rfc := fosite.ErrorToRFC6749Error(err)

ctx.Logger.Errorf("Authorization Response for Request with id '%s' on client with id '%s' could not be created: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription())
Expand All @@ -125,3 +135,62 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr

ctx.Providers.OpenIDConnect.WriteAuthorizeResponse(ctx, rw, requester, responder)
}

// OpenIDConnectPushedAuthorizationRequest handles POST requests to the OAuth 2.0 Pushed Authorization Requests endpoint.
//
// RFC9126 https://www.rfc-editor.org/rfc/rfc9126.html
func OpenIDConnectPushedAuthorizationRequest(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, r *http.Request) {
var (
requester fosite.AuthorizeRequester
responder fosite.PushedAuthorizeResponder
err error
)

if requester, err = ctx.Providers.OpenIDConnect.NewPushedAuthorizeRequest(ctx, r); err != nil {
rfc := fosite.ErrorToRFC6749Error(err)

ctx.Logger.Errorf("Pushed Authorization Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription())

ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err)

return
}

var client *oidc.Client

clientID := requester.GetClient().GetID()

if client, err = ctx.Providers.OpenIDConnect.GetFullClient(clientID); err != nil {
if errors.Is(err, fosite.ErrNotFound) {
ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' could not be processed: client was not found", requester.GetID(), clientID)
} else {
ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' could not be processed: failed to find client: %+v", requester.GetID(), clientID, err)
}

ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err)

return
}

if err = client.ValidatePKCEPolicy(requester); err != nil {
rfc := fosite.ErrorToRFC6749Error(err)

ctx.Logger.Errorf("Pushed Authorization Request with id '%s' on client with id '%s' failed to validate the PKCE policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription())

ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err)

return
}

if responder, err = ctx.Providers.OpenIDConnect.NewPushedAuthorizeResponse(ctx, requester, oidc.NewSession()); err != nil {
rfc := fosite.ErrorToRFC6749Error(err)

ctx.Logger.Errorf("Pushed Authorization Request failed with error: %s", rfc.WithExposeDebug(true).GetDescription())

ctx.Providers.OpenIDConnect.WritePushedAuthorizeError(ctx, rw, requester, err)

return
}

ctx.Providers.OpenIDConnect.WritePushedAuthorizeResponse(ctx, rw, requester, responder)
}
3 changes: 1 addition & 2 deletions internal/mocks/notifier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ff6be40

Please sign in to comment.