OpenID Connect - Access Token Authentication Methods

Access token requests must be authenticated using any of the client authentication strategies specified in the OpenID Connect discovery. The following methods are supported by CAS:

Method Description
client_secret_basic Default. The client id and client secret are used to create a HTTP Basic authentication scheme.
client_secret_post NOT RECOMMENDED. The client_id and client_secret are only supplied and accepted in the request body.
client_secret_jwt Clients with a client secret can create a JWT using an HMAC SHA algorithm, which is calculated using the client secret as the shared key. CAS verifies it with the clientSecret of the application, when its tokenEndpointAuthenticationMethod is client_secret_jwt or left blank, or with a raw secret defined in its jwks (this fallback is deprecated for removal). The secret must be at least as long as the hash output of the algorithm, i.e. 32 bytes for HS256 and 64 bytes for HS512; a shorter secret is skipped with a warning. The JWT is passed as a client_assertion request parameter and client_assertion_type parameter MUST be urn:ietf:params:oauth:client-assertion-type:jwt-bearer.
private_key_jwt Clients with a registered public key build and sign a JWT using that key. The JWT is passed as a client_assertion request parameter and client_assertion_type parameter MUST be urn:ietf:params:oauth:client-assertion-type:jwt-bearer.
tls_client_auth Mutual TLS utilizing the PKI method of associating a certificate to a client.
attest_jwt_client_auth A client attestation, such as a wallet attestation, in the OAuth-Client-Attestation header with its proof of possession in the OAuth-Client-Attestation-PoP header. See below.
attest_jwt_client_auth_dpop A client attestation in the OAuth-Client-Attestation header, with a DPoP proof made with the attested key as its proof of possession (DPoP combined mode). See below.

Please study the specification to learn more.

Enforced and required client authentication methods may be tuned and controlled for each relying party:

1
2
3
4
5
6
7
8
9
{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "client-id",
  "clientSecret": "secret",
  "serviceId": "^https://app.example.org/oidc",
  "name": "MyApplication",
  "id": 1,
  "tokenEndpointAuthenticationMethod": "client_secret_basic"
}

If the tokenEndpointAuthenticationMethod field is left blank, all available authentication methods are evaluated for access token requests and authentication method enforcement should effectively be disabled.

Attestation-Based Client Authentication

Clients such as native wallet apps may authenticate with a client attestation, following OAuth 2.0 Attestation-Based Client Authentication. This is how wallets authenticate with a wallet attestation, as described by OpenID4VCI 1.0 Appendix E and required by HAIP 1.0, at the token and the pushed authorization request endpoints.

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

cas.authn.oidc.client-attestation.challenge.enabledWhether CAS hands out challenges at its challenge endpoint, advertised as challenge_endpoint , and requires the challenge claim of every client attestation proof of possession to carry one.
false

Whether CAS hands out challenges at its challenge endpoint, advertised as challenge_endpoint, and requires the challenge claim of every client attestation proof of possession to carry one. A missing, unknown or expired challenge is answered with use_attestation_challenge and a fresh challenge in the OAuth-Client-Attestation-Challenge header.

Type
Boolean
Default
false
Defined by
OidcClientAttestationChallengeProperties
cas.authn.oidc.client-attestation.challenge.time-to-liveHow long a challenge may be used.
PT5M
Duration

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

Type
String
Default
PT5M
Defined by
OidcClientAttestationChallengeProperties
cas.authn.oidc.client-attestation.signing-alg-values-supportedSigning algorithms accepted for client attestations and their proofs of possession, advertised as client_attestation_signing_alg_values_supported and client_attestation_pop_signing_alg_values_supported .
no default

Signing algorithms accepted for client attestations and their proofs of possession, advertised as client_attestation_signing_alg_values_supported and client_attestation_pop_signing_alg_values_supported. Only the EC (ES*) and RSA (RS*, PS*) algorithms can be verified.

Type
List<String>
Default
none
Defined by
OidcClientAttestationProperties
cas.authn.oidc.client-attestation.trust-anchorsLocations of PEM-encoded certificates of the trust anchors that client attestations must chain to, such as those of wallet providers.
no default

Locations of PEM-encoded certificates of the trust anchors that client attestations must chain to, such as those of wallet providers. A client attestation carries its signing certificate, and any intermediate certificates, in its x5c header, which must lead to one of these anchors. Attestation-based client authentication is not accepted, nor advertised, until at least one trust anchor is configured.

Type
List<String>
Default
none
Defined by
OidcClientAttestationProperties

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.

The client attestation is signed by the client attester, such as the wallet provider, and is sent in the OAuth-Client-Attestation header. It carries its signing certificate, and any intermediate certificates, in its x5c header; the chain must lead to one of the trust anchors defined in CAS settings, without including it, and the signer must not be self-signed. Its sub is the client identifier and must match the client_id request parameter when one is sent, it must carry an exp that has not passed, and its cnf claim names the key of the client instance. A status claim is accepted and logged as a warning, since its status is not checked.

The proof of possession is sent in the OAuth-Client-Attestation-PoP header, signed by the key in cnf. Its aud must be the issuer, it must carry an iat within the last few minutes and a jti that may be used only once. Each header must be sent exactly once.

Challenges may be turned on in CAS settings. The discovery document then lists a challenge_endpoint, where a client fetches a challenge to put in the challenge claim of its proof of possession:

1
POST /oidc/oidcAttestationChallenge
1
2
3
{
  "attestation_challenge": "TST-1-..."
}

Every proof of possession must then carry a challenge that CAS handed out and that has not expired; a challenge may be used more than once until it expires. A missing, unknown or expired challenge is answered with 400 and use_attestation_challenge, along with a fresh challenge in the OAuth-Client-Attestation-Challenge header for the client to retry with.

In the DPoP combined mode (attest_jwt_client_auth_dpop), the request carries no OAuth-Client-Attestation-PoP header; a single DPoP proof stands for the proof of possession instead. It must be valid for the request per RFC 9449, signed with one of the dpop_signing_alg_values_supported, made with the key in the attestation’s cnf and not used before. Challenges do not apply to this mode; once DPoP nonces are turned on, the proof must carry one instead, or the request is answered with 400 and use_dpop_nonce. A request that carries both headers is authenticated by the proof of possession; its DPoP proof, if any, is left to the endpoint, and binds the access token at the token endpoint.

Once trust anchors are configured, the discovery document lists attest_jwt_client_auth and attest_jwt_client_auth_dpop among token_endpoint_auth_methods_supported, along with the accepted signing algorithms:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic",
    "private_key_jwt",
    "attest_jwt_client_auth",
    "attest_jwt_client_auth_dpop"
  ],
  "client_attestation_signing_alg_values_supported": [
    "ES256",
    "ES384",
    "ES512"
  ],
  "client_attestation_pop_signing_alg_values_supported": [
    "ES256",
    "ES384",
    "ES512"
  ]
}

A relying party that must authenticate this way at the token endpoint names the method, attest_jwt_client_auth or attest_jwt_client_auth_dpop:

1
2
3
4
5
6
7
8
{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "https://wallet.example.org",
  "serviceId": "^https://wallet.example.org/.*",
  "name": "Wallet",
  "id": 1,
  "tokenEndpointAuthenticationMethod": "attest_jwt_client_auth"
}

Mutual TLS Client Authentication

In order to utilize TLS for client authentication, the TLS connection between the client and CAS MUST have been established or re-established with mutual-TLS X.509 certificate authentication during the TLS handshake. For all requests to CAS utilizing mutual-TLS client authentication, the client MUST include the client_id parameter which enables the CAS server to easily identify the client independently from the content of the certificate and locate the client configuration using the client identifier and check the certificate presented in the TLS handshake against the expected credentials for that client.

In order to convey the expected subject of the certificate and other validation requirements, the following parameters can be assigned to a service definition in support of the PKI method of mutual-TLS client authentication. Such parameters may also be passed at the time of registering the client dynamically via dynamic client registration. A relying party using the tls_client_auth authentication method MUST use exactly one of the below metadata parameters to indicate the certificate subject value that the authorization server is to expect when authenticating the respective client.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "client-id",
  "clientSecret": "secret",
  "serviceId": "^https://app.example.org/oidc",
  "name": "MyApplication",
  "id": 1,
  "tokenEndpointAuthenticationMethod": "tls_client_auth",
  "tlsClientAuthSubjectDn": "...",
  "tlsClientAuthSanDns": "...",
  "tlsClientAuthSanUri": "...",
  "tlsClientAuthSanIp": "...",
  "tlsClientAuthSanEmail": "..."
}

The following parameters are supported:

Field Description
tlsClientAuthSubjectDn The expected subject distinguished name of the certificate that the client will use in mutual-TLS authentication.
tlsClientAuthSanDns The expected dNSName SAN entry in the certificate that the client will use in mutual-TLS authentication.
tlsClientAuthSanUri The expected uniformResourceIdentifier SAN entry in the certificate.
tlsClientAuthSanIp The expected iPAddress SAN entry in the certificate in either for IPv4 or IPv6.
tlsClientAuthSanEmail The expected rfc822Name SAN entry in the certificate.

Access tokens issued to a client that authenticates with mutual TLS are bound to its certificate, whatever the grant, as described in RFC 8705. JWT access tokens and introspection responses carry the certificate thumbprint in the cnf claim as x5t#S256, the base64url-encoded SHA-256 hash of the DER-encoded certificate. The profile and userinfo endpoints, the verifiable credential endpoint and Heimdall accept such a token only over a connection that presents the same client certificate; otherwise the request is answered with 401 and invalid_token:

1
2
3
4
{
  "error": "invalid_token",
  "error_description": "Access token is bound to a client certificate that the request does not present"
}

A refresh token is not bound to the certificate itself: the client authenticates with mutual TLS when it uses it, and the access token it receives is bound to the certificate presented then.

A client may also be required to always receive certificate-bound access tokens, as the tls_client_certificate_bound_access_tokens client metadata of RFC 8705 defines. Every token request from such a client must then be authenticated with mutual TLS, whatever the grant; one that is not is answered with 400 and invalid_request, and the grant it presents, such as an authorization code, is not redeemed. The setting is turned on in the client’s service definition:

1
2
3
4
5
6
7
8
9
{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "client",
  "serviceId": "^https://app.example.org/.*",
  "name": "Sample",
  "id": 1,
  "tokenEndpointAuthenticationMethod": "tls_client_auth",
  "tlsClientCertificateBoundAccessTokens": true
}

A client may also ask for it by sending "tls_client_certificate_bound_access_tokens": true in its dynamic registration request; the registration response echoes the setting when it is on.

SPIFFE

CAS supports the SPIFFE standard for mutual TLS client authentication. To use SPIFFE, the client must present a certificate with a URI SAN entry that follows the SPIFFE format, i.e. spiffe://.... CAS will validate the certificate and extract the client identifier from the SPIFFE URI. The client identifier is then used to locate the service definition.

1
2
3
4
5
6
7
8
9
10
{
  "@class": "org.apereo.cas.services.OidcRegisteredService",
  "clientId": "spiffe://example.org/ns/payments/sa/service-sample",
  "serviceId" : "^https://localhost:9859/anything/cas.*",
  "name": "MyApplication",
  "id": 1,
  "tokenEndpointAuthenticationMethod": "tls_client_auth",
  "tlsClientAuthSubjectDn": "(.+)",
  "supportedGrantTypes": [ "java.util.HashSet", [ "client_credentials" ] ]
}

An example request with curl would look like the following:

1
2
3
4
5
curl --cert /var/run/secrets/svid.pem \
     --key /var/run/secrets/key.pem \
     --cacert /var/run/secrets/bundle.pem \
     -X POST https://sso.example.org/cas/oidc/token \
     -d "grant_type=client_credentials&scope=xyz"

You can verify SPIFFE ID is present in your certificate:

1
openssl x509 -in svid.pem -text -noout

Look for:

1
2
X509v3 Subject Alternative Name:
    URI:spiffe://example.org/ns/payments/sa/service-sample