Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DPoP design doc #254

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
259 changes: 259 additions & 0 deletions design/dpop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
# OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer (DPoP)

* **Status**: Notes
* **JIRA**: TBD

## Motivation

DPoP is a mechanism to prevent illegal API calls from succeeding only with a stolen access token. This is done via augmenting the calls to OAuth 2.0 endpoints as well as resource servers with a special DPoP header that proves possession of a private key, and binding the issued tokens to that key. Please refer to the Resources section for more info on DPoP.

## Implementation Details

DPoP is a cross-cutting concept that would affect multiple Keycloak components, namely core libraries, OAuth/OIDC endpoints, adapters, models and admin UI.

dteleguin marked this conversation as resolved.
Show resolved Hide resolved
It is suggested that DPoP should be set up on a per-client basis, with the three options available that should be interpreted as follows:
mposolda marked this conversation as resolved.
Show resolved Hide resolved

|**Value** | **Keycloak Server** | **Keycloak Adaptors** |
| --- | --- | --- |
| Required | OAuth endpoints (where applicable) should require a DPoP header and a DPoP-bound token; should return an error otherwise. | Protected resources should require a DPoP header and a DPoP-bound token, or should return an error otherwise. |
| Optional | _Token Endpoint_: if DPoP header is present, the returned token should be DPoP-bound and should contain a `cnf` claim with a JWK thumbprint; should be a regular token otherwise <br/><br/>_UserInfo endpoint_: if the access token is DPoP-bound, a matching DPoP header should be required; no header should be required otherwise. | Protected resources should require a matching DPoP header if the supplied access token is DPoP-bound; should ignore the header otherwise. |
| Disabled | DPoP headers should be ignored; issued tokens shouldn’t contain JWK thumbprints. | DPoP headers and token JWK thumbprints should be ignored. |

dteleguin marked this conversation as resolved.
Show resolved Hide resolved
Other DPoP configuration options should include:

| **Option** | **Description** |
| --- | --- |
| DPoP Proof Lifetime | Time window in which the respective DPoP proof JWT would be accepted |
| DPoP Allowed Clock Skew | To accommodate for clock offsets, the server MAY accept DPoP proofs that carry an `iat` time in the reasonably near future (e.g., a few seconds in the future). |
Copy link
Contributor

@mposolda mposolda Sep 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this config option really needed? Just wondering that ideal would be to avoid many configuration options as each configuration option is adding some complexity (And Keycloak client already has really big amount of configuration options, but even if we do with client policies, one less config option is always good :)). For example JWTClientAuthenticator does not check that "iat" from the token is in the past (see https://github.com/keycloak/keycloak/blob/15.0.2/services/src/main/java/org/keycloak/authentication/authenticators/client/JWTClientAuthenticator.java) and hence it seems to me to have same behaviour for DPoP as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example JWTClientAuthenticator does not check that "iat" from the token is in the past

Did you perhaps mean "in the future"? The only check that is performed against iat is this:

            // KEYCLOAK-2986
            int currentTime = Time.currentTime();
            if (token.getExpiration() == 0 && token.getIssuedAt() + 10 < currentTime) {
                throw new RuntimeException("Token is not active");
            }

That means, we're accepting iat values that are no more than 10 seconds into the past + values set in indefinite future.

Unfortunately, this might not work for DPoP proofs. Let's revisit section 10.1 "DPoP Proof Replay":

If an adversary is able to get hold of a DPoP proof JWT, the adversary could replay that token at the same endpoint (the HTTP endpoint and method are enforced via the respective claims in the JWTs). To prevent this, servers MUST only accept DPoP proofs for a limited time window after their iat time, preferably only for a relatively brief period (on the order of a few seconds).

Servers SHOULD store, in the context of the request URI, the jti value of each DPoP proof for the time window in which the respective DPoP proof JWT would be accepted and decline HTTP requests to the same URI for which the jti value has been seen before. In order to guard against memory exhaustion attacks a server SHOULD reject DPoP proof JWTs with unnecessarily large jti values or store only a hash thereof.

Note: To accommodate for clock offsets, the server MAY accept DPoP proofs that carry an iat time in the reasonably near future (e.g., a few seconds in the future). Because clock skews between servers and clients may be large, servers may choose to limit DPoP proof lifetimes by using server-provided nonce values rather than clock times, yielding intended results even in the face of arbitrarily large clock skews.

Key points are:

  • in order to prevent DPoP proof replays, we must temporarily store the jti values;
  • we should keep the jti values in memory only for the limited time window (aka DPoP proof lifetime), otherwise we'll quickly exhaust memory;
  • DPoP proofs have neither nbf nor exp claims, only iat which is provided by the client; hence, we should enforce limited window of validity based on client-supplied iat only;
  • clock skews can be significant (and could even exceed proof lifetimes, see this), but
  • we cannot allow iats set in indefinite future, since the server will have to store jti until (iat + lifetime) is reached, which creates possibility of conducting memory exhaustion attacks. Hence, allowed clock skew has to be finite.

That said, we should have two server-side parameters, one for DPoP proof lifetime and another one for allowed clock skew. I decided to make them configurable, in order to better suit particular setups and security requirements, similar to what we have in another parts of Keycloak (configurable token lifetimes; configurable clock skews for adapters and IdPs).

The recent version of the DPoP draft offers an alternative mechanism of limiting DPoP proof lifetimes, which is based on server-supplied nonces rather than clock times (see 8. Authorization Server-Provided Nonce and 9. Resource Server-Provided Nonce) and is said to "yield intended results even in the face of arbitrarily large clock skews". However, this method is more complex and won't be featured in the initial PR. And even with this method we'll have to somehow configure lifetimes of server-supplied nonces.


_([8.1. DPoP Proof Replay][4])_

### Endpoints

#### Token Endpoint

With DPoP enabled, token endpoint must understand the DPoP header and perform binding between the keypair presented thereby and the issued token.

As per the spec,

> To request an access token that is bound to a public key using DPoP,
the client MUST provide a valid DPoP proof JWT in a "DPoP" header
when making an access token request to the authorization server's
token endpoint. This is applicable for all access token requests
regardless of grant type (including, for example, the common
"authorization\_code" and "refresh\_token" grant types but also
extension grants such as the JWT authorization grant \[RFC7523\]).

_([5.5. DPoP Access Token Request][6])_

> :question: As DPoP is primarily meant to protect tokens issued to SPAs, should we also support DPoP for:
>
> * password grant
> * client credentials grant (DPoP-enabled service accounts?)
> * token exchange
> * extension grants (with the future Grant Type SPI)
> * UMA permissions grant?

dteleguin marked this conversation as resolved.
Show resolved Hide resolved
Classes/methods affected:

* org.keycloak.protocol.oidc.endpoints.TokenEndpoint
* processGrantRequest()


#### Introspection Endpoint

For DPoP-bound tokens, token introspection endpoint must return an additional `cnf` claim with the `jkt` member containing JWK thumbprint (a SHA-256 hash).

_([6.2. JWK Thumbprint Confirmation Method in Token Introspection][8])_

Classes/methods affected:

* org.keycloak.protocol.oidc.AccessTokenIntrospectionProvider
* introspect()


#### Metadata Endpoint

With DPoP enabled, a new parameter, `dpop_signing_alg_values_supported`, must be included into the authorization server metadata.

_([5.1. Authorization Server Metadata][10])_

Classes/methods affected:

* org.keycloak.protocol.oidc.OIDCWellKnownProvider
* getConfig()


#### UserInfo

The spec doesn’t directly mention the UserInfo endpoint, but it clearly falls into the category of bearer token protected resources:

> The UserInfo Endpoint is an OAuth 2.0 Protected Resource that returns Claims about the authenticated End-User. To obtain the requested Claims about the End-User, the Client makes a request to the UserInfo Endpoint using an Access Token obtained through OpenID Connect Authentication.

_(OpenID Connect Core 1.0,_ [_5.3. UserInfo Endpoint_][11]_)_

Therefore, the UserInfo endpoint must also be made DPoP-aware.

Classes/methods affected:

* org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint
* issueUserInfo()


#### Other Endpoints

The following endpoints in Keycloak are public:

* Server Discovery
* Server JWK set
* Authorization

The following endpoints don’t use bearer tokens, but rather client credentials:

* Client Registration
* Token Introspection
* Token Revocation

The following endpoints use cookie authentication:

* Logout
* Check Session iframe

Therefore, no other endpoints should be affected.

### Models

#### Access Token

New property, `jkt`, should be recognized as a member of the `cnf` claim.

Classes/methods affected:

* org.keycloak.representations.AccessToken.CertConf
* rename to Conf or Confirmation; introduce `jwkThumbprint` property


#### Client

Properties should be added to the client model to support the following DPoP-related settings:

* DPoP mode (enabled/optional/disabled)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify the implementation, it is only needed to add one client settings:

  • dpop_bound_access_tokens (true/false)
    dpop_bound_access_tokens is a client metadata defined in the DPoP specification.
    The boolean value is easy to handle compared with ternary (enabled/optional/disabled).

As for DPoP proof lifetime and clock skew, it might be better to use default values for all clients. It might be better not to add new client settings because keycloak has already a lot of such settings and these settings are difficult to be managed.

If we add client settings at this time and we add some method (e.g., client policies) to remove necessities of the settings afterwards, we cannnot delete these settings because we need to keep backward compatibility.

Therefore, at this time, it might be preferable to use default values only for DPoP proof lifetime and clock skew, and add some mechanism to change these values per client or set of clients without adding client settings afterwards.

* DPoP proof lifetime (integer)
* DPoP clock skew (integer)

Classes/methods affected:

* org.keycloak.models.ClientModel
* org.keycloak.models.cache.infinispan.ClientAdapter
* org.keycloak.models.cache.infinispan.entities.CachedClient
* org.keycloak.models.jpa.ClientAdapter
* org.keycloak.models.jpa.entities.ClientEntity
* org.keycloak.models.map.client.AbstractClientEntity

### Adapters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TBH, I would leave adapters as the lowest priority unless there is an explicit request for adding DPoP support to them. Do you have a need for adapters support for your deployment?

Copy link
Contributor Author

@dteleguin dteleguin Nov 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed; no, we do not have any requirements for the adapters.

I think we can focus this document solely on the AS side of things, as the RS side should probably belong to another design document. OK if I remove adapter-specific info and mention the current focus?


#### Java Adapters

In the Bearer only mode, Java adapters must:

* enforce DPoP proof validation in the “Enabled/Required” mode;
* allow for DPoP validation of DPoP-bound tokens in the “Optional” mode;
* ignore DPoP proofs in the “Disabled” mode.

The following options should be exposed in the adapter configuration:

* DPoP Mode: required/optional/disabled
* DPoP Proof Lifetime
* DPoP Allowed Clock Skew

> :question: Should we support DPoP for the “traditional” (monolithic) web applications, where adapters themselves handle Authorization Code flow, and the tokens are not exposed to the user agent?

Classes/methods affected:

* org.keycloak.adapters.\*


##### Application Clustering

As per the spec,

> Servers SHOULD store, in the context of the request URI, the `jti` value of each DPoP proof for the time window in which the respective DPoP proof JWT would be accepted and decline HTTP requests to the same URI for which the `jti` value has been seen before.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you pointed, this is quite challenging to support especially in the cluster environment. I would personally not support this requirement for now (Specification says "SHOULD") unless there is very strong requirement of it. I see there is some security gain, however the price to pay store additional data on the resource servers side is the challenge (especially for the clustering environment).

Copy link
Contributor Author

@dteleguin dteleguin Nov 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. This should probably also belong to a RS-specific design document.


> :question: How do we implement DPoP proof replay protection in a clustered environment?
>
> Currently, only SAML adapters for Wildfly/AS/JBossWeb support distributed sessions via Infinispan.

#### JavaScript Adapter

In addition to DPoP handling proper, JavaScript adapter should also expose a method that would allow developers to obtain the DPoP proof header, since the latter would be required to access protected resources. As DPoP proof is bound to a particular HTTP method and URL, it should be recomputed for every protected resource request.

Authors of [axios-keycloak][12], [keycloak-angular][13] and similar HTTP interceptors should be notified so that DPoP support could be introduced in the respective projects.

Files affected:

* adapters/oidc/js/src/main/resources/keycloak.d.ts


#### Other Adapters

Support in Node.JS adapter, mod\_authn\_oidc and Keycloak Gatekeeper could be also considered.

dteleguin marked this conversation as resolved.
Show resolved Hide resolved
### Admin UI

The following configuration options should be exposed in the Admin UI for OIDC clients:

* DPoP Mode: required/optional/disabled
* DPoP Proof Lifetime
* DPoP Allowed Clock Skew

Files/classes affected:

* org.keycloak.services.resources.admin.ClientResource
* org.keycloak.representations.idm.ClientRepresentation
* themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html

### Core

Methods should be added to support [JWK thumbprint (RFC 7638)][14] computation.

Classes affected:

* org.keycloak.jose.jwk.{JWK,RSAPublicJWK,ECPublicJWK}
* add `getThumbprint()` method


### Tests

DPoP should be properly covered by unit and integration tests.

### Documentation

DPoP usage should be properly documented.

Affected documents:

* Securing Applications and Services Guide

## Open Questions

1. Which OAuth grant types should be DPoP-enabled?
2. Should we support Java adapters' redirect mode (for monolithic webapps)?
3. How do we implement adapter-side DPoP proof replay protection in a clustered environment?
4. Should we generate a DPoP keypair per session, or should it be persisted in the user agent (or both)?
dteleguin marked this conversation as resolved.
Show resolved Hide resolved
1. Should we use Web Crypto API to support non-extractable keys?
5. What is the relationship between DPoP and UMA?

## Resources
* [IETF Draft][1]
* [Illustrated DPoP (OAuth Access Token Security Enhancement)][2]

[1]: https://tools.ietf.org/id/draft-ietf-oauth-dpop-02.html
[2]: https://darutk.medium.com/illustrated-dpop-oauth-access-token-security-enhancement-801680d761ff
[4]: https://tools.ietf.org/id/draft-ietf-oauth-dpop-02.html#name-dpop-proof-replay
[6]: https://tools.ietf.org/id/draft-ietf-oauth-dpop-02.html#name-dpop-access-token-request
[8]: https://tools.ietf.org/id/draft-ietf-oauth-dpop-02.html#name-jwk-thumbprint-confirmation-
[10]: https://tools.ietf.org/id/draft-ietf-oauth-dpop-02.html#name-authorization-server-metada
[11]: https://openid.net/specs/openid-connect-core-1_0.html#UserInfo
[12]: https://github.com/herrmannplatz/axios-keycloak
[13]: https://github.com/mauriciovigolo/keycloak-angular
[14]: https://www.rfc-editor.org/rfc/rfc7638.html