Client Assertions
Client assertions are an alternative to client secrets for authenticating
confidential clients at token endpoints. Instead of sending a shared secret,
the client creates a signed JWT (or SAML assertion) and includes it in the
request. This is defined in
RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication
and is commonly known as the private_key_jwt or client_secret_jwt
authentication methods defined in
OpenID Connect Core §9.
All protocol request types that derive from ProtocolRequest expose two
properties for setting client assertions: ClientAssertion and
ClientAssertionFactory.
ClientAssertion
Section titled “ClientAssertion”The ClientAssertion property lets you attach a pre-built assertion to any
protocol request. Set its Type and Value and they will be included as the
client_assertion_type and client_assertion parameters:
var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client",
ClientAssertion = { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = mySignedJwt },
ClientCredentialStyle = ClientCredentialStyle.PostBody });ClientAssertionFactory
Section titled “ClientAssertionFactory”Added in Duende.IdentityModel 7.2.0
The ClientAssertionFactory property accepts a Func<Task<ClientAssertion>>
— a factory function that creates a fresh ClientAssertion on demand. This
was introduced to support scenarios where a protocol request may need to be
retried, and each attempt requires a new assertion with unique jti and
iat claims.
The primary motivating scenario is DPoP (Demonstrating Proof of Possession).
When a DPoP token request receives a use_dpop_nonce error, the HTTP handler
retries the request with an updated DPoP proof. If the client assertion were
static, the server could reject the retry because it has already seen that
assertion’s jti. The factory solves this by generating a new assertion for
each attempt.
var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client",
ClientAssertionFactory = () => Task.FromResult(new ClientAssertion { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = CreateSignedJwt() // generates a fresh JWT each time }),
ClientCredentialStyle = ClientCredentialStyle.PostBody });When ClientAssertionFactory is set, the factory is stored on the
HttpRequestMessage.Options so that DPoP retry handlers (and other delegating
handlers in the pipeline) can invoke it to obtain a new assertion on each
attempt.
Usage with Duende.IdentityModel.OidcClient
Section titled “Usage with Duende.IdentityModel.OidcClient”Both the ClientAssertion and ClientAssertionFactory properties exist on
ProtocolRequest to support
Duende.IdentityModel.OidcClient. The OidcClient
library builds on IdentityModel’s protocol requests internally, and when
configured with client assertion-based authentication, it sets these properties
on the underlying requests it creates.
When ClientAssertionFactory is set, it is used during both:
- Pushed Authorization Requests (PAR) — the factory is invoked to produce a fresh assertion for the PAR endpoint request.
- Token requests — the factory is invoked again to produce a fresh assertion for the token endpoint request.
This ensures each request carries its own unique assertion, which is essential
when the authorization server enforces jti uniqueness across requests.