OpenID Connect Authentication - DPoP

DPoP is an OAuth security extension for binding tokens to a private key that belongs to the client. The binding makes the DPoP access token sender-constrained and its replay, if leaked or stolen token, can be effectively detected and prevented, as opposed to the common Bearer token. DPoP is intended for securing the tokens of public clients, such as single-page applications (SPA) and mobile applications.

Single-page applications (SPA) can now request the issue of DPoP access tokens from CAS when it is acting as an OpenID Connect provider. This is a new kind of token, with stronger security properties than the default Bearer access tokens. The DPoP token comes with a protection against unauthorised use in case it suffers an accidental or malicious leak. This is achieved by binding the token to a private key held by the client. To prevent a leak of the key itself the client should store it behind an API that renders its private parameters inaccessible to application code.

The SPA authentication flow with a DPoP token can be summarized as such:

  • The SPA generates a new RSA or EC key pair in such a way so the private key parameters cannot be exported from the browser.
  • To request a DPoP access token the SPA generates a one-time-use JWT signed with the private key. The function of this JWT is to demonstrate possession of the key. Its header includes the public parameters of the signing key in JWK format.
  • The SPA makes the usual token request to CAS but to trigger issue of a DPoP access token the proof JWT must be included in an HTTP request header called DPoP.
  • If the DPoP proof is valid and signed with a supported JWS algorithms the token response will appear in the usual format, but with the token type set to DPoP.

To access a protected resource with a DPoP token (such as the profile endpoint in CAS) the client needs to generate a new DPoP proof, with one additional string claim - ath, set to the BASE64URL-encoded SHA-256 hash of the access token value. The htm (HTTP method) and htu (HTTP URI) claims must match those of the resource.

Note that there is no special configuration required in CAS to enable support for DPoP tokens; however you should note that at this time, support for DPoP only covers access tokens. Support for refresh tokens may be worked out in future versions.

Single-Use Checking

DPoP proofs are designed to be used exactly once. Each proof JWT carries a unique jti (JWT ID) claim alongside its iat timestamp, and any endpoint validating the proof such as the token or profile endpoints are expected to track previously-seen jti values and reject a proof whose jti has already been presented. To enforce single-use DPoP proofs are tracked in the CAS ticket registry as CAS tickets and will auto-expire.

Server-Provided Nonces

CAS can require DPoP proofs to carry a nonce it handed out, as RFC 9449 allows, which limits how long a proof that a compromised client generated in advance stays usable. Once turned on in CAS settings, every DPoP proof must carry, in its nonce claim, a nonce that CAS handed out and that has not expired. A proof without one, or with an unknown or expired one, is refused with use_dpop_nonce and a fresh nonce in the DPoP-Nonce header, which the client puts in a new proof to retry the request. The token endpoint, and client authentication in the DPoP combined mode, answer with 400:

1
2
3
HTTP/1.1 400 Bad Request
DPoP-Nonce: TST-1-mD3m...
Cache-Control: no-store
1
2
3
4
{
  "error": "use_dpop_nonce",
  "error_description": "DPoP proof carries no valid server-provided nonce"
}

Protected resources, such as the profile endpoint, the verifiable credential endpoint and Heimdall, answer with 401 and a DPoP challenge:

1
2
3
HTTP/1.1 401 Unauthorized
WWW-Authenticate: DPoP error="use_dpop_nonce", error_description="Use of DPoP nonce required"
DPoP-Nonce: TST-1-mD3m...

Nonces are also handed out ahead of time, in the DPoP-Nonce header of the OpenID4VCI nonce endpoint and of the client attestation challenge endpoint. A nonce may be used for any number of proofs until it expires; each proof still carries its own jti, which may be used only once. Nonces are kept in the ticket registry, so they are shared by all CAS nodes that share it. Browser-based clients can only read the DPoP-Nonce header when CORS settings list it among the exposed headers.

The following settings and properties are available from the CAS configuration catalog:

cas.authn.oidc.dpop.nonce.enabledWhether DPoP proofs must carry a nonce that CAS handed out.
false

Whether DPoP proofs must carry a nonce that CAS handed out. A proof without one, or with an unknown or expired one, is refused with use_dpop_nonce and a fresh nonce in the DPoP-Nonce header: with 400 at the token endpoint and in the DPoP combined mode of attestation-based client authentication, and with 401 and a WWW-Authenticate: DPoP challenge at protected resources. Nonces are also handed out by the OpenID4VCI nonce endpoint and the client attestation challenge endpoint.

Type
Boolean
Default
false
Defined by
OidcDPoPNonceProperties
cas.authn.oidc.dpop.nonce.time-to-liveHow long a nonce may be used.
PT5M
Duration

How long a nonce may be used. A nonce may be used more than once while it is valid; the jti of each proof still prevents a proof from being replayed.

Type
String
Default
PT5M
Defined by
OidcDPoPNonceProperties

Required settings may be needed to activate or affect the feature; review them even when they have a default. Optional settings only need to be set to change a default or to turn on the behavior they control. Third party settings belong to libraries such as Spring Boot that CAS builds on; their own documentation may have more detail.

Notes on configuration

Configuration Metadata

The collection of configuration properties listed in this section are automatically generated from the CAS source and components that contain the actual field definitions, types, descriptions, modules, etc. This metadata may not always be 100% accurate, or could be lacking details and sufficient explanations.

Be Selective

This section is meant as a guide only. Do NOT copy/paste the entire collection of settings into your CAS configuration; rather pick only the properties that you need. Do NOT enable settings unless you are certain of their purpose and do NOT copy settings into your configuration only to keep them as reference. All these ideas lead to upgrade headaches, maintenance nightmares and premature aging.

YAGNI

Note that for nearly ALL use cases, declaring and configuring properties listed here is sufficient. You should NOT have to explicitly massage a CAS XML/Java/etc configuration file to design an authentication handler, create attribute release policies, etc. CAS at runtime will auto-configure all required changes for you. If you are unsure about the meaning of a given CAS setting, do NOT turn it on without hesitation. Review the codebase or better yet, ask questions to clarify the intended behavior.

Naming Convention

Property names can be specified in very relaxed terms. For instance cas.someProperty, cas.some-property, cas.some_property are all valid names. While all forms are accepted by CAS, there are certain components (in CAS and other frameworks used) whose activation at runtime is conditional on a property value, where this property is required to have been specified in CAS configuration using kebab case. This is both true for properties that are owned by CAS as well as those that might be presented to the system via an external library or framework such as Spring Boot, etc.

:information_source: Note

When possible, properties should be stored in lower-case kebab format, such as cas.property-name=value. The only possible exception to this rule is when naming actuator endpoints; The name of the actuator endpoints (i.e. ssoSessions) MUST remain in camelCase mode.

Settings and properties that are controlled by the CAS platform directly always begin with the prefix cas. All other settings are controlled and provided to CAS via other underlying frameworks and may have their own schemas and syntax. BE CAREFUL with the distinction. Unrecognized properties are rejected by CAS and/or frameworks upon which CAS depends. This means if you somehow misspell a property definition or fail to adhere to the dot-notation syntax and such, your setting is entirely refused by CAS and likely the feature it controls will never be activated in the way you intend.

Validation

Configuration properties are automatically validated on CAS startup to report issues with configuration binding, especially if defined CAS settings cannot be recognized or validated by the configuration schema. Additional validation processes are also handled via Configuration Metadata and property migrations applied automatically on startup by Spring Boot and family.

Indexed Settings

CAS settings able to accept multiple values are typically documented with an index, such as cas.some.setting[0]=value. The index [0] is meant to be incremented by the adopter to allow for distinct multiple configuration blocks.