Skip to content

Commit 29c6668

Browse files
committed
Change how authentication demands are marshalled and support token binding certificates/additional token request parameters for interactive flows
1 parent ef0092a commit 29c6668

File tree

9 files changed

+459
-315
lines changed

9 files changed

+459
-315
lines changed

sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
4444

4545
if (await AuthenticateUserInteractivelyAsync(registration, configuration, stoppingToken))
4646
{
47+
// Note: the OpenIddict server stack supports mTLS-based token binding for public clients:
48+
// while these clients cannot authenticate using a TLS client certificate, the certificate
49+
// can be used to bind the refresh (and access) tokens returned by the authorization server
50+
// to the client application, which prevents such tokens from being used without providing a
51+
// proof-of-possession matching the TLS client certificate used when the token was acquired.
52+
//
53+
// While this sample deliberately doesn't store the generated certificate in a persistent
54+
// location, the certificate used for token binding should typically be stored in the user
55+
// certificate store to be reloaded across application restarts in a real-world application.
56+
var certificate = configuration.TlsClientCertificateBoundAccessTokens is true
57+
? GenerateEphemeralTlsClientCertificate()
58+
: null;
59+
4760
var flow = await GetSelectedFlowAsync(registration, configuration, stoppingToken);
4861

4962
AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]");
@@ -64,7 +77,8 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
6477
var response = await _service.AuthenticateInteractivelyAsync(new()
6578
{
6679
CancellationToken = stoppingToken,
67-
Nonce = result.Nonce
80+
Nonce = result.Nonce,
81+
TokenBindingCertificate = certificate
6882
});
6983

7084
AnsiConsole.MarkupLine("[green]Interactive authentication successful:[/]");
@@ -112,7 +126,8 @@ await _service.RevokeTokenAsync(new()
112126
{
113127
CancellationToken = stoppingToken,
114128
ProviderName = provider,
115-
RefreshToken = response.RefreshToken
129+
RefreshToken = response.RefreshToken,
130+
TokenBindingCertificate = certificate
116131
})).Principal));
117132
}
118133

@@ -151,15 +166,6 @@ await _service.AuthenticateInteractivelyAsync(new()
151166
var type = await GetSelectedGrantTypeAsync(registration, configuration, stoppingToken);
152167
if (type is GrantTypes.DeviceCode)
153168
{
154-
// Note: the OpenIddict server stack supports mTLS-based token binding for public clients:
155-
// while these clients cannot authenticate using a TLS client certificate, the certificate
156-
// can be used to bind the refresh (and access) tokens returned by the authorization server
157-
// to the client application, which prevents such tokens from being used without providing a
158-
// proof-of-possession matching the TLS client certificate used when the token was acquired.
159-
//
160-
// While this sample deliberately doesn't store the generated certificate in a persistent
161-
// location, the certificate used for token binding should typically be stored in the user
162-
// certificate store to be reloaded across application restarts in a real-world application.
163169
var certificate = configuration.TlsClientCertificateBoundAccessTokens is true
164170
? GenerateEphemeralTlsClientCertificate()
165171
: null;

src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs

Lines changed: 380 additions & 297 deletions
Large diffs are not rendered by default.

src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,8 +833,9 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
833833
ProviderTypes.PayPal => (false, false, false),
834834

835835
// NetSuite does not return an id_token when using the refresh_token grant type.
836-
// Additionally, the at_hash inside their id_token is not a valid hash of the
837-
// access token, but is instead a copy of the RS256 signature within the access token.
836+
//
837+
// Additionally, the at_hash inside the id_token is not a valid hash of the access
838+
// token, but is instead a copy of the RS256 signature within the access token.
838839
ProviderTypes.NetSuite => (true, false, false),
839840

840841
_ => (context.ExtractBackchannelIdentityToken,

src/OpenIddict.Client/OpenIddictClientEvents.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,12 @@ public OpenIddictRequest Request
441441
[Obsolete("This property is no longer used and will be removed in a future version.")]
442442
public HashSet<string> UserInfoEndpointTokenBindingMethods { get; } = new(StringComparer.Ordinal);
443443

444+
/// <summary>
445+
/// Gets or sets a boolean indicating whether the token entry associated
446+
/// with the state token should be marked as redeemed in the database.
447+
/// </summary>
448+
public bool DisableStateTokenRedeeming { get; set; }
449+
444450
/// <summary>
445451
/// Gets or sets a boolean indicating whether a token request should be sent.
446452
/// </summary>

src/OpenIddict.Client/OpenIddictClientExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public static OpenIddictClientBuilder AddClient(this OpenIddictBuilder builder)
6060
builder.Services.TryAddSingleton<RequireRevocationClientAssertionGenerated>();
6161
builder.Services.TryAddSingleton<RequireRevocationRequest>();
6262
builder.Services.TryAddSingleton<RequireStateTokenPrincipal>();
63+
builder.Services.TryAddSingleton<RequireStateTokenRedeemed>();
6364
builder.Services.TryAddSingleton<RequireStateTokenValidated>();
6465
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
6566
builder.Services.TryAddSingleton<RequireTokenEntryCreated>();

src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,20 @@ public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
406406
}
407407
}
408408

409+
/// <summary>
410+
/// Represents a filter that excludes the associated handlers if the state token should not be redeemed.
411+
/// </summary>
412+
public sealed class RequireStateTokenRedeemed : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
413+
{
414+
/// <inheritdoc/>
415+
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
416+
{
417+
ArgumentNullException.ThrowIfNull(context);
418+
419+
return new(!context.DisableStateTokenRedeeming);
420+
}
421+
}
422+
409423
/// <summary>
410424
/// Represents a filter that excludes the associated handlers if no state token is validated.
411425
/// </summary>

src/OpenIddict.Client/OpenIddictClientHandlers.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,7 @@ public RedeemStateTokenEntry(IOpenIddictTokenManager tokenManager)
824824
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
825825
.AddFilter<RequireTokenStorageEnabled>()
826826
.AddFilter<RequireStateTokenPrincipal>()
827+
.AddFilter<RequireStateTokenRedeemed>()
827828
.AddFilter<RequireStateTokenValidated>()
828829
.UseScopedHandler<RedeemStateTokenEntry>()
829830
// Note: this handler is deliberately executed early in the pipeline to ensure that
@@ -896,7 +897,7 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
896897
// Reject the authentication demand if the expected endpoint type doesn't
897898
// match the current endpoint type as it may indicate a mix-up attack (e.g a
898899
// state token created for a logout operation was used for a login operation).
899-
if (type != context.EndpointType)
900+
if (context.EndpointType is not OpenIddictClientEndpointType.Unknown && context.EndpointType != type)
900901
{
901902
context.Reject(
902903
error: Errors.InvalidRequest,
@@ -995,6 +996,12 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
995996

996997
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
997998

999+
// Only validate the endpoint type if the endpoint is well-known.
1000+
if (context.EndpointType is OpenIddictClientEndpointType.Unknown)
1001+
{
1002+
return ValueTask.CompletedTask;
1003+
}
1004+
9981005
// Resolve the endpoint type allowed to be used with the state token.
9991006
if (!Enum.TryParse(context.StateTokenPrincipal.GetClaim(Claims.Private.EndpointType),
10001007
ignoreCase: true, out OpenIddictClientEndpointType type))
@@ -1174,8 +1181,8 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
11741181
{
11751182
ArgumentNullException.ThrowIfNull(context);
11761183

1177-
// To help mitigate mix-up attacks, the identity of the issuer can be returned by
1178-
// authorization servers that support it as a part of the "iss" parameter, which
1184+
// To help mitigate mix-up attacks, the identity of the issuer can be returned
1185+
// by authorization servers that support it as part of the "iss" parameter, which
11791186
// allows comparing it to the issuer in the state token. Depending on the selected
11801187
// response_type, the same information could be retrieved from the identity token
11811188
// that is expected to contain an "iss" claim containing the issuer identity.
@@ -1217,6 +1224,7 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
12171224

12181225
// Reject authorization responses containing an "iss" parameter if the configuration
12191226
// doesn't indicate this parameter is supported, as recommended by the specification.
1227+
//
12201228
// See https://datatracker.ietf.org/doc/html/draft-ietf-oauth-iss-auth-resp-05#section-2.4
12211229
// for more information.
12221230
else if (!string.IsNullOrEmpty(issuer))
@@ -1467,7 +1475,7 @@ GrantTypes.AuthorizationCode or GrantTypes.Implicit when
14671475
}
14681476

14691477
/// <summary>
1470-
/// Contains the logic responsible for resolving the token from the incoming request.
1478+
/// Contains the logic responsible for resolving the frontchannel tokens from the incoming request.
14711479
/// </summary>
14721480
public sealed class ResolveValidatedFrontchannelTokens : IOpenIddictClientHandler<ProcessAuthenticationContext>
14731481
{

src/OpenIddict.Client/OpenIddictClientModels.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public static class OpenIddictClientModels
2020
/// </summary>
2121
public sealed record class InteractiveAuthenticationRequest
2222
{
23+
/// <summary>
24+
/// Gets or sets the parameters that will be added to the token request, if applicable.
25+
/// </summary>
26+
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
27+
2328
/// <summary>
2429
/// Gets or sets the cancellation token that will be
2530
/// used to determine if the operation was aborted.
@@ -35,6 +40,23 @@ public sealed record class InteractiveAuthenticationRequest
3540
/// Gets or sets the application-specific properties that will be added to the context.
3641
/// </summary>
3742
public Dictionary<string, string?>? Properties { get; init; }
43+
44+
/// <summary>
45+
/// Gets or sets the X.509 client certificate used to bind the access and/or
46+
/// refresh tokens issued by the authorization server, if applicable.
47+
/// </summary>
48+
/// <remarks>
49+
/// <para>
50+
/// Note: when mTLs is also used for OAuth 2.0 client authentication, the
51+
/// certificate set here replaces the client certificate chosen by OpenIddict.
52+
/// </para>
53+
/// <para>
54+
/// Note: if a certificate-based client authentication or token binding method is
55+
/// negotiated, the type of the certificate must match the negotiated methods.
56+
/// </para>
57+
/// </remarks>
58+
[EditorBrowsable(EditorBrowsableState.Advanced)]
59+
public X509Certificate2? TokenBindingCertificate { get; init; }
3860
}
3961

4062
/// <summary>

src/OpenIddict.Client/OpenIddictClientService.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,10 @@ public async ValueTask<InteractiveAuthenticationResult> AuthenticateInteractivel
271271
var context = new ProcessAuthenticationContext(transaction)
272272
{
273273
CancellationToken = request.CancellationToken,
274-
Nonce = request.Nonce
274+
Nonce = request.Nonce,
275+
TokenEndpointClientCertificate = request.TokenBindingCertificate,
276+
TokenRequest = request.AdditionalTokenRequestParameters
277+
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
275278
};
276279

277280
if (request.Properties is { Count: > 0 })

0 commit comments

Comments
 (0)