Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,41 @@ public void HandleCodeRedemption(OpenIdConnectMessage tokenEndpointResponse)
{
TokenEndpointResponse = tokenEndpointResponse;
}

/// <summary>
/// Indicates if the <see cref="OpenIdConnectEvents.OnAuthorizationCodeReceived"/> event chose to handle client
/// authentication for the token endpoint request. If <see langword="true"/>, the handler will not set
/// client credentials (such as <c>client_secret</c>) on the <see cref="TokenEndpointRequest"/>,
/// allowing the event to provide alternative authentication, such as <c>private_key_jwt</c> or
/// a federated client assertion.
/// </summary>
public bool HandledClientAuthentication { get; private set; }

/// <summary>
/// Tells the handler to skip setting client authentication properties for the token endpoint request.
/// The handler sets the <c>client_secret</c> from the <see cref="OpenIdConnectClientRegistration"/>
/// returned by <see cref="OpenIdConnectHandler.ResolveClientRegistrationAsync"/> by default,
/// but the <see cref="OpenIdConnectEvents.OnAuthorizationCodeReceived"/> event may replace that with an alternative
/// authentication mode, such as <c>private_key_jwt</c> or a federated client assertion.
/// </summary>
/// <remarks>
/// <para>
/// When this method is called, the handler will remove <c>client_secret</c> from the <see cref="TokenEndpointRequest"/>.
/// The event handler is responsible for setting appropriate authentication parameters on the
/// <see cref="TokenEndpointRequest"/>, such as <c>client_assertion</c> and <c>client_assertion_type</c>.
/// This follows the same pattern as <see cref="PushedAuthorizationContext.HandledClientAuthentication"/>
/// for pushed authorization requests, extending it to the token endpoint code redemption flow.
/// </para>
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Thrown if <see cref="HandleCodeRedemption()"/> has already been called, since both operations are mutually exclusive.
/// </exception>
public void HandleClientAuthentication()
{
if (HandledCodeRedemption)
{
throw new InvalidOperationException("HandleClientAuthentication cannot be called when HandleCodeRedemption has already been called.");
}
HandledClientAuthentication = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.IdentityModel.Tokens;

namespace Microsoft.AspNetCore.Authentication.OpenIdConnect;

/// <summary>
/// Represents the client registration information resolved for a single OpenID Connect authentication request.
/// This class groups the per-request client identity, credentials, and token validation settings used throughout
/// the authentication flow (challenge, token redemption, and protocol validation).
/// </summary>
/// <remarks>
/// <para>
/// Override <see cref="OpenIdConnectHandler.ResolveClientRegistrationAsync"/> to return a custom
/// <see cref="OpenIdConnectClientRegistration"/> per request. This enables multi-tenant scenarios where different
/// client registrations are used depending on the incoming request context.
/// </para>
/// <para>
/// This class is intentionally not sealed. Future versions may add additional properties
/// (such as per-request scopes or resource indicators) without introducing breaking changes.
/// </para>
/// </remarks>
public class OpenIdConnectClientRegistration
{
/// <summary>
/// Gets or sets the <c>client_id</c> to use for this authentication request.
/// </summary>
/// <remarks>
/// This value is used as the <c>client_id</c> parameter in the authorization request, the token endpoint request,
/// and for protocol validation of authentication and token responses. It defaults to
/// <see cref="OpenIdConnectOptions.ClientId"/>.
/// </remarks>
public string ClientId { get; set; } = default!;

/// <summary>
/// Gets or sets the <c>client_secret</c> to use for this authentication request, or <see langword="null"/>
/// if no shared secret is used (for example, when using <c>private_key_jwt</c> or federated credentials).
/// </summary>
/// <remarks>
/// This value is used as the <c>client_secret</c> parameter in the token endpoint request and in pushed
/// authorization requests. It defaults to <see cref="OpenIdConnectOptions.ClientSecret"/>.
/// When set to <see langword="null"/> or empty, the handler will not include a <c>client_secret</c> parameter.
/// For advanced client authentication scenarios (such as client assertions), use
/// <see cref="AuthorizationCodeReceivedContext.HandleClientAuthentication"/> or
/// <see cref="PushedAuthorizationContext.HandleClientAuthentication"/> to take full control.
/// </remarks>
public string? ClientSecret { get; set; }

/// <summary>
/// Gets or sets the <see cref="TokenValidationParameters"/> used to validate tokens received during
/// this authentication request.
/// </summary>
/// <remarks>
/// This is typically a clone of <see cref="OpenIdConnectOptions.TokenValidationParameters"/> with per-request
/// customizations applied, such as setting <see cref="TokenValidationParameters.ValidAudience"/> to match
/// a dynamic <see cref="ClientId"/>.
/// </remarks>
public TokenValidationParameters TokenValidationParameters { get; set; } = default!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,47 @@
/// <inheritdoc />
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new OpenIdConnectEvents());

/// <summary>
/// Resolves the <see cref="OpenIdConnectClientRegistration"/> to use for the current authentication request.
/// </summary>
/// <remarks>
/// <para>
/// By default, this method returns a registration populated from
/// <see cref="OpenIdConnectOptions.ClientId"/>, <see cref="OpenIdConnectOptions.ClientSecret"/>,
/// and a clone of <see cref="OpenIdConnectOptions.TokenValidationParameters"/>.
/// Override this method to dynamically resolve client registration on a per-request basis.
/// This is useful in multi-tenant scenarios where different client registrations (with distinct
/// client identifiers, secrets, and token validation settings) are used depending on the
/// incoming request context.
/// </para>
/// <para>
/// The returned registration is used throughout the authentication flow:
/// <list type="bullet">
/// <item><description><see cref="OpenIdConnectClientRegistration.ClientId"/> sets the <c>client_id</c>
/// in the authorization request (challenge), the token endpoint request (code redemption),
/// pushed authorization requests, and protocol validation.</description></item>
/// <item><description><see cref="OpenIdConnectClientRegistration.ClientSecret"/> sets the <c>client_secret</c>
/// in the token endpoint request and pushed authorization requests. When <see langword="null"/> or empty,
/// no <c>client_secret</c> is sent. For advanced client authentication (e.g., <c>private_key_jwt</c>),
/// use <see cref="AuthorizationCodeReceivedContext.HandleClientAuthentication"/>.</description></item>
/// <item><description><see cref="OpenIdConnectClientRegistration.TokenValidationParameters"/> provides the
/// parameters used for ID token validation. Override to set a per-request
/// <see cref="TokenValidationParameters.ValidAudience"/> matching the dynamic client.</description></item>
/// </list>
/// </para>
/// </remarks>
/// <param name="properties">The <see cref="AuthenticationProperties"/> associated with the current authentication request.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> that resolves to the <see cref="OpenIdConnectClientRegistration"/> to use.</returns>
protected virtual ValueTask<OpenIdConnectClientRegistration> ResolveClientRegistrationAsync(AuthenticationProperties properties)
{
return ValueTask.FromResult(new OpenIdConnectClientRegistration
{
ClientId = Options.ClientId,

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux x64)

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS arm64)

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: Ubuntu x64)

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: Ubuntu x64)

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS x64)

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: macOS)

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: macOS)

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.

Check failure on line 123 in src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs#L123

src/Security/Authentication/OpenIdConnect/src/OpenIdConnectHandler.cs(123,24): error CS8601: (NETCORE_ENGINEERING_TELEMETRY=Build) Possible null reference assignment.
ClientSecret = Options.ClientSecret,
TokenValidationParameters = Options.TokenValidationParameters.Clone(),
});
}

/// <inheritdoc />
public override Task<bool> HandleRequestAsync()
{
Expand Down Expand Up @@ -395,9 +436,11 @@
_configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.RequestAborted);
}

var registration = await ResolveClientRegistrationAsync(properties);

var message = new OpenIdConnectMessage
{
ClientId = Options.ClientId,
ClientId = registration.ClientId,
EnableTelemetryParameters = !Options.DisableTelemetry,
IssuerAddress = _configuration?.AuthorizationEndpoint ?? string.Empty,
RedirectUri = BuildRedirectUri(Options.CallbackPath),
Expand Down Expand Up @@ -493,7 +536,7 @@
// Push if endpoint is in disco
if (!string.IsNullOrEmpty(parEndpoint))
{
await PushAuthorizationRequest(message, properties, parEndpoint);
await PushAuthorizationRequest(message, properties, parEndpoint, registration);
}

break;
Expand All @@ -514,7 +557,7 @@
}

// Otherwise push
await PushAuthorizationRequest(message, properties, parEndpoint);
await PushAuthorizationRequest(message, properties, parEndpoint, registration);
break;
}

Expand Down Expand Up @@ -549,7 +592,7 @@
throw new NotImplementedException($"An unsupported authentication method has been configured: {Options.AuthenticationMethod}");
}

private async Task PushAuthorizationRequest(OpenIdConnectMessage authorizeRequest, AuthenticationProperties properties, string parEndpoint)
private async Task PushAuthorizationRequest(OpenIdConnectMessage authorizeRequest, AuthenticationProperties properties, string parEndpoint, OpenIdConnectClientRegistration registration)
{
ArgumentException.ThrowIfNullOrEmpty(parEndpoint);

Expand All @@ -563,12 +606,12 @@
{
Logger.PushAuthorizationHandledClientAuthentication();
}
// Otherwise, add the client secret to the parameters (if available)
// Otherwise, add the client secret from the registration (if available)
else
{
if (!string.IsNullOrEmpty(Options.ClientSecret))
if (!string.IsNullOrEmpty(registration.ClientSecret))
{
parRequest.Parameters.Add(OpenIdConnectParameterNames.ClientSecret, Options.ClientSecret);
parRequest.Parameters.Add(OpenIdConnectParameterNames.ClientSecret, registration.ClientSecret);
}
}

Expand Down Expand Up @@ -597,7 +640,7 @@
}

authorizeRequest.Parameters.Clear();
authorizeRequest.Parameters.Add("client_id", Options.ClientId);
authorizeRequest.Parameters.Add("client_id", registration.ClientId);
authorizeRequest.Parameters.Add("request_uri", requestUri);
}

Expand Down Expand Up @@ -756,7 +799,8 @@
ClaimsPrincipal? user = null;
JwtSecurityToken? jwt = null;
string? nonce = null;
var validationParameters = Options.TokenValidationParameters.Clone();
var registration = await ResolveClientRegistrationAsync(properties);
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

properties is used elsewhere in this method as nullable (e.g., passed as properties! later), but here it's passed as non-null to ResolveClientRegistrationAsync(AuthenticationProperties properties). This is a nullability mismatch and may not compile (or can introduce a nullability warning-as-error). Use properties! here (consistent with the existing usage) or ensure properties is non-nullable by construction before this point.

Suggested change
var registration = await ResolveClientRegistrationAsync(properties);
var registration = await ResolveClientRegistrationAsync(properties!);

Copilot uses AI. Check for mistakes.
var validationParameters = registration.TokenValidationParameters;

// Hybrid or Implicit flow
if (!string.IsNullOrEmpty(authorizationResponse.IdToken))
Expand Down Expand Up @@ -794,7 +838,7 @@

Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext()
{
ClientId = Options.ClientId,
ClientId = registration.ClientId,
ProtocolMessage = authorizationResponse,
ValidatedIdToken = jwt,
Nonce = nonce
Expand All @@ -805,7 +849,7 @@
// Authorization Code or Hybrid flow
if (!string.IsNullOrEmpty(authorizationResponse.Code))
{
var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, user, properties!, jwt);
var authorizationCodeReceivedContext = await RunAuthorizationCodeReceivedEventAsync(authorizationResponse, user, properties!, jwt, registration);
if (authorizationCodeReceivedContext.Result != null)
{
return authorizationCodeReceivedContext.Result;
Expand Down Expand Up @@ -890,7 +934,7 @@
{
Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext()
{
ClientId = Options.ClientId,
ClientId = registration.ClientId,
ProtocolMessage = tokenEndpointResponse,
ValidatedIdToken = jwt,
Nonce = nonce
Expand Down Expand Up @@ -1251,14 +1295,14 @@
return context;
}

private async Task<AuthorizationCodeReceivedContext> RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal? user, AuthenticationProperties properties, JwtSecurityToken? jwt)
private async Task<AuthorizationCodeReceivedContext> RunAuthorizationCodeReceivedEventAsync(OpenIdConnectMessage authorizationResponse, ClaimsPrincipal? user, AuthenticationProperties properties, JwtSecurityToken? jwt, OpenIdConnectClientRegistration registration)
{
Logger.AuthorizationCodeReceived();

var tokenEndpointRequest = new OpenIdConnectMessage()
{
ClientId = Options.ClientId,
ClientSecret = Options.ClientSecret,
ClientId = registration.ClientId,
ClientSecret = registration.ClientSecret,
Code = authorizationResponse.Code,
GrantType = OpenIdConnectGrantTypes.AuthorizationCode,
EnableTelemetryParameters = !Options.DisableTelemetry,
Expand All @@ -1282,6 +1326,14 @@
};

await Events.AuthorizationCodeReceived(context);

// If the event handled client authentication, remove the default client_secret
// that was set on the token endpoint request.
if (context.HandledClientAuthentication)
{
context.TokenEndpointRequest?.Parameters.Remove(OpenIdConnectParameterNames.ClientSecret);
}

if (context.Result != null)
{
if (context.Result.Handled)
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
#nullable enable
Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext.HandleClientAuthentication() -> void
Microsoft.AspNetCore.Authentication.OpenIdConnect.AuthorizationCodeReceivedContext.HandledClientAuthentication.get -> bool
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration.ClientId.get -> string!
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration.ClientId.set -> void
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration.ClientSecret.get -> string?
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration.ClientSecret.set -> void
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration.OpenIdConnectClientRegistration() -> void
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration.TokenValidationParameters.get -> Microsoft.IdentityModel.Tokens.TokenValidationParameters!
Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration.TokenValidationParameters.set -> void
virtual Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.ResolveClientRegistrationAsync(Microsoft.AspNetCore.Authentication.AuthenticationProperties! properties) -> System.Threading.Tasks.ValueTask<Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectClientRegistration!>
Loading
Loading