Skip to content

Commit 06bc479

Browse files
authored
Merge pull request #1037 from DuendeSoftware/client-assertions
Add documentation for improved client assertions support
2 parents f5240b4 + c455bdb commit 06bc479

3 files changed

Lines changed: 120 additions & 1 deletion

File tree

astro/src/content/docs/accesstokenmanagement/advanced/client-assertions.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,6 @@ You need to explicitly set the `Audience` to the authorization server's issuer U
154154

155155
Don't set the audience to the `TokenUrl`. Setting the `Audience` value to the token endpoint leaves
156156
you vulnerable to these vulnerabilities: (CVE-2025-27370/CVE-2025-27371).
157-
:::
157+
:::
158+
159+
For a complete working example, see the [WebClientAssertions sample](https://github.com/DuendeSoftware/foss/tree/main/access-token-management/samples/WebClientAssertions).

astro/src/content/docs/identitymodel-oidcclient/advanced/dpop.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ access token were to leak, it cannot be used without also having access to the p
1212

1313
The `Duende.IdentityModel.OidcClient.Extensions` library adds supports for DPoP to OidcClient.
1414

15+
:::note
16+
Duende.IdentityModel.OidcClient Version 7.1.0 now supports combining DPoP with [Client Assertions](/identitymodel/endpoints/client-assertions/).
17+
:::
18+
1519
## DPoP Key
1620

1721
Before we begin, your application needs to have a DPoP key in the form of a
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
---
2+
title: Client Assertions
3+
description: How to use client assertions (private_key_jwt / client_secret_jwt) for client authentication in protocol requests.
4+
sidebar:
5+
order: 8
6+
label: Client Assertions
7+
---
8+
9+
Client assertions are an alternative to client secrets for authenticating
10+
confidential clients at token endpoints. Instead of sending a shared secret,
11+
the client creates a signed JWT (or SAML assertion) and includes it in the
12+
request. This is defined in
13+
[RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication](https://datatracker.ietf.org/doc/html/rfc7523)
14+
and is commonly known as the `private_key_jwt` or `client_secret_jwt`
15+
authentication methods defined in
16+
[OpenID Connect Core §9](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication).
17+
18+
All protocol request types that derive from `ProtocolRequest` expose two
19+
properties for setting client assertions: `ClientAssertion` and
20+
`ClientAssertionFactory`.
21+
22+
## ClientAssertion
23+
24+
The `ClientAssertion` property lets you attach a pre-built assertion to any
25+
protocol request. Set its `Type` and `Value` and they will be included as the
26+
`client_assertion_type` and `client_assertion` parameters:
27+
28+
```csharp
29+
var response = await client.RequestClientCredentialsTokenAsync(
30+
new ClientCredentialsTokenRequest
31+
{
32+
Address = "https://demo.duendesoftware.com/connect/token",
33+
ClientId = "client",
34+
35+
ClientAssertion =
36+
{
37+
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
38+
Value = mySignedJwt
39+
},
40+
41+
ClientCredentialStyle = ClientCredentialStyle.PostBody
42+
});
43+
```
44+
45+
:::note
46+
When using a client assertion, set `ClientCredentialStyle` to
47+
`ClientCredentialStyle.PostBody`. Client assertions are not compatible with
48+
`AuthorizationHeader` style and an `InvalidOperationException` will be thrown if
49+
both are combined with a `ClientId`.
50+
:::
51+
52+
## ClientAssertionFactory
53+
54+
*Added in `Duende.IdentityModel` 7.2.0*
55+
56+
The `ClientAssertionFactory` property accepts a `Func<Task<ClientAssertion>>`
57+
— a factory function that creates a **fresh** `ClientAssertion` on demand. This
58+
was introduced to support scenarios where a protocol request may need to be
59+
**retried**, and each attempt requires a new assertion with unique `jti` and
60+
`iat` claims.
61+
62+
The primary motivating scenario is **DPoP** (Demonstrating Proof of Possession).
63+
When a DPoP token request receives a `use_dpop_nonce` error, the HTTP handler
64+
retries the request with an updated DPoP proof. If the client assertion were
65+
static, the server could reject the retry because it has already seen that
66+
assertion's `jti`. The factory solves this by generating a new assertion for
67+
each attempt.
68+
69+
```csharp
70+
var response = await client.RequestClientCredentialsTokenAsync(
71+
new ClientCredentialsTokenRequest
72+
{
73+
Address = "https://demo.duendesoftware.com/connect/token",
74+
ClientId = "client",
75+
76+
ClientAssertionFactory = () => Task.FromResult(new ClientAssertion
77+
{
78+
Type = OidcConstants.ClientAssertionTypes.JwtBearer,
79+
Value = CreateSignedJwt() // generates a fresh JWT each time
80+
}),
81+
82+
ClientCredentialStyle = ClientCredentialStyle.PostBody
83+
});
84+
```
85+
86+
When `ClientAssertionFactory` is set, the factory is stored on the
87+
`HttpRequestMessage.Options` so that DPoP retry handlers (and other delegating
88+
handlers in the pipeline) can invoke it to obtain a new assertion on each
89+
attempt.
90+
91+
:::note
92+
If both `ClientAssertion` and `ClientAssertionFactory` are set, the factory
93+
takes precedence during request preparation.
94+
:::
95+
96+
### Usage with Duende.IdentityModel.OidcClient
97+
98+
Both the `ClientAssertion` and `ClientAssertionFactory` properties exist on
99+
`ProtocolRequest` to support
100+
[`Duende.IdentityModel.OidcClient`](/identitymodel-oidcclient/). The OidcClient
101+
library builds on IdentityModel's protocol requests internally, and when
102+
configured with client assertion-based authentication, it sets these properties
103+
on the underlying requests it creates.
104+
105+
When `ClientAssertionFactory` is set, it is used during both:
106+
107+
- **Pushed Authorization Requests (PAR)** — the factory is invoked to produce a
108+
fresh assertion for the PAR endpoint request.
109+
- **Token requests** — the factory is invoked again to produce a fresh assertion
110+
for the token endpoint request.
111+
112+
This ensures each request carries its own unique assertion, which is essential
113+
when the authorization server enforces `jti` uniqueness across requests.

0 commit comments

Comments
 (0)