Browse Source

Rework the AuthenticateWithClientCredentialsAsync()/AuthenticateWithPasswordAsync()/AuthenticateWithRefreshTokenAsync() APIs to be easier to use and more flexible

pull/1575/head
Kévin Chalet 3 years ago
parent
commit
828b0dbd08
  1. 3
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 10
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  3. 132
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  4. 629
      src/OpenIddict.Client/OpenIddictClientService.cs
  5. 140
      src/OpenIddict.Validation/OpenIddictValidationService.cs

3
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1355,6 +1355,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID0354" xml:space="preserve">
<value>The nonce cannot be resolved from the state token.</value>
</data>
<data name="ID0355" xml:space="preserve">
<value>No issuer was specified in the authentication context.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

10
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -294,6 +294,11 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value;
}
/// <summary>
/// Gets the user-defined authentication properties, if available.
/// </summary>
public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the grant type used for the authentication demand, if applicable.
/// </summary>
@ -309,6 +314,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? RequestForgeryProtection { get; set; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server, if applicable.
/// </summary>
public HashSet<string> Scopes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the address of the token endpoint, if applicable.
/// </summary>

132
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -30,6 +30,7 @@ public static partial class OpenIddictClientHandlers
* Authentication processing:
*/
ValidateAuthenticationDemand.Descriptor,
ResolveClientRegistrationFromAuthenticationContext.Descriptor,
EvaluateValidatedUpfrontTokens.Descriptor,
ResolveValidatedStateToken.Descriptor,
ValidateRequiredStateToken.Descriptor,
@ -205,6 +206,16 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0311));
}
// If no issuer was explicitly attached and a single client is registered, use it.
// Otherwise, throw an exception to indicate that setting an explicit issuer
// is required when multiple clients are registered.
context.Issuer ??= context.Options.Registrations.Count switch
{
0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
1 => context.Options.Registrations[0].Issuer,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0355))
};
break;
default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0290));
@ -214,6 +225,57 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for resolving the client registration applicable to the authentication demand.
/// </summary>
public sealed class ResolveClientRegistrationFromAuthenticationContext : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ResolveClientRegistrationFromAuthenticationContext>()
.SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Note: this handler only applies to authentication demands triggered from unknown endpoints.
//
// Client registrations/configurations that need to be resolved as part of authentication demands
// triggered from the redirection or post-logout redirection requests are handled elsewhere.
if (context.EndpointType is not OpenIddictClientEndpointType.Unknown)
{
return;
}
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Registration ??= context.Options.Registrations.Find(
registration => registration.Issuer == context.Issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
// Resolve and attach the server configuration to the context if none has been set already.
context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (context.Configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
}
}
/// <summary>
/// Contains the logic responsible for determining the types of tokens to validate upfront.
/// </summary>
@ -225,7 +287,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<EvaluateValidatedUpfrontTokens>()
.SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000)
.SetOrder(ResolveClientRegistrationFromAuthenticationContext.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -630,23 +692,19 @@ public static partial class OpenIddictClientHandlers
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
var registration = context.Options.Registrations.Find(registration => registration.Issuer == issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
context.Issuer = issuer;
context.Registration = registration;
context.Registration = context.Options.Registrations.Find(registration => registration.Issuer == issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
// Resolve and attach the server configuration to the context.
var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
context.Configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration.Issuer != context.Issuer)
if (context.Configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
context.Configuration = configuration;
}
}
@ -1776,11 +1834,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// Try to extract the address of the token endpoint from the server configuration.
if (context.Configuration.TokenEndpoint is { IsAbsoluteUri: true })
// If the address of the token endpoint wasn't explicitly set
// at this stage, try to extract it from the server configuration.
context.TokenEndpoint ??= context.Configuration.TokenEndpoint switch
{
context.TokenEndpoint = context.Configuration.TokenEndpoint;
}
{ IsAbsoluteUri: true } address when address.IsWellFormedOriginalString() => address,
_ => null
};
return default;
}
@ -1871,6 +1932,14 @@ public static partial class OpenIddictClientHandlers
string type => type
};
if (context.Scopes.Count > 0)
{
// Note: the final OAuth 2.0 specification requires using a space as the scope separator.
// Clients that need to deal with older or non-compliant implementations can register
// a custom handler to use a different separator (typically, a comma).
context.TokenRequest.Scope = string.Join(" ", context.Scopes);
}
// If the token request uses an authorization code grant, retrieve the code_verifier and
// the redirect_uri from the state token principal and attach them to the request, if available.
if (context.TokenRequest.GrantType is GrantTypes.AuthorizationCode)
@ -2176,7 +2245,7 @@ public static partial class OpenIddictClientHandlers
try
{
context.TokenResponse = await _service.SendTokenRequestAsync(
context.Registration, context.TokenEndpoint, context.TokenRequest);
context.Registration, context.TokenRequest, context.TokenEndpoint);
}
catch (ProtocolException exception)
@ -2858,7 +2927,7 @@ public static partial class OpenIddictClientHandlers
SecurityAlgorithms.RsaSha512 or SecurityAlgorithms.RsaSsaPssSha512
=> OpenIddictHelpers.ComputeSha512Hash(Encoding.ASCII.GetBytes(token)),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0293))
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0295))
};
// Warning: only the left-most half of the access token and authorization code digest is used.
@ -3043,11 +3112,14 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
// Try to extract the address of the userinfo endpoint from the server configuration.
if (context.Configuration.UserinfoEndpoint is { IsAbsoluteUri: true })
// If the address of the userinfo endpoint wasn't explicitly set
// at this stage, try to extract it from the server configuration.
context.UserinfoEndpoint ??= context.Configuration.UserinfoEndpoint switch
{
context.UserinfoEndpoint = context.Configuration.UserinfoEndpoint;
}
{ IsAbsoluteUri: true } address when address.IsWellFormedOriginalString() => address,
_ => null
};
return default;
}
@ -3174,7 +3246,7 @@ public static partial class OpenIddictClientHandlers
try
{
(context.UserinfoResponse, (context.UserinfoTokenPrincipal, context.UserinfoToken)) =
await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoEndpoint, context.UserinfoRequest);
await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoRequest, context.UserinfoEndpoint);
}
catch (ProtocolException exception)
@ -3566,21 +3638,19 @@ public static partial class OpenIddictClientHandlers
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Registration = context.Options.Registrations.Find(
context.Registration ??= context.Options.Registrations.Find(
registration => registration.Issuer == context.Issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
// Resolve and attach the server configuration to the context.
var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
// Resolve and attach the server configuration to the context if none has been set already.
context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration.Issuer != context.Issuer)
if (context.Configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
context.Configuration = configuration;
}
}
@ -4713,21 +4783,19 @@ public static partial class OpenIddictClientHandlers
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Registration = context.Options.Registrations.Find(
context.Registration ??= context.Options.Registrations.Find(
registration => registration.Issuer == context.Issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
// Resolve and attach the server configuration to the context.
var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
// Resolve and attach the server configuration to the context if none has been set already.
context.Configuration ??= await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration.Issuer != context.Issuer)
if (context.Configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
context.Configuration = configuration;
}
}

629
src/OpenIddict.Client/OpenIddictClientService.cs

@ -8,6 +8,7 @@ using System.Diagnostics;
using System.Security.Claims;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@ -25,6 +26,345 @@ public sealed class OpenIddictClientService
public OpenIddictClientService(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <summary>
/// Authenticates using the client credentials grant and resolves the corresponding tokens.
/// </summary>
/// <param name="issuer">The issuer.</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithClientCredentialsAsync(
Uri issuer, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? properties = null, CancellationToken cancellationToken = default)
{
if (issuer is null)
{
throw new ArgumentNullException(nameof(issuer));
}
if (scopes is not null && scopes.Any(string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
}
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
var registration = options.CurrentValue.Registrations.Find(registration => registration.Issuer == issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } address || !address.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
GrantType = GrantTypes.ClientCredentials,
Issuer = registration.Issuer,
Registration = registration,
TokenEndpoint = address,
TokenRequest = parameters is not null ? new(parameters) : null,
};
if (scopes is { Length: > 0 })
{
context.Scopes.UnionWith(scopes);
}
if (properties is { Count: > 0 })
{
foreach (var property in properties)
{
context.Properties[property.Key] = property.Value;
}
}
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Create a composite principal containing claims resolved from the
// backchannel identity token and the userinfo token, if available.
return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
/// </summary>
/// <param name="issuer">The issuer.</param>
/// <param name="username">The username to use.</param>
/// <param name="password">The password to use.</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithPasswordAsync(
Uri issuer, string username, string password, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? properties = null, CancellationToken cancellationToken = default)
{
if (issuer is null)
{
throw new ArgumentNullException(nameof(issuer));
}
if (string.IsNullOrEmpty(username))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0335), nameof(username));
}
if (string.IsNullOrEmpty(password))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0336), nameof(password));
}
if (scopes is not null && scopes.Any(string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
}
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
var registration = options.CurrentValue.Registrations.Find(registration => registration.Issuer == issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } address || !address.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
GrantType = GrantTypes.Password,
Issuer = registration.Issuer,
Password = password,
Registration = registration,
TokenEndpoint = address,
TokenRequest = parameters is not null ? new(parameters) : null,
Username = username
};
if (scopes is { Length: > 0 })
{
context.Scopes.UnionWith(scopes);
}
if (properties is { Count: > 0 })
{
foreach (var property in properties)
{
context.Properties[property.Key] = property.Value;
}
}
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Create a composite principal containing claims resolved from the
// backchannel identity token and the userinfo token, if available.
return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
/// </summary>
/// <param name="issuer">The issuer.</param>
/// <param name="token">The refresh token to use.</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithRefreshTokenAsync(
Uri issuer, string token, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? properties = null, CancellationToken cancellationToken = default)
{
if (issuer is null)
{
throw new ArgumentNullException(nameof(issuer));
}
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0156), nameof(token));
}
if (scopes is not null && scopes.Any(string.IsNullOrEmpty))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes));
}
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
var registration = options.CurrentValue.Registrations.Find(registration => registration.Issuer == issuer) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0292));
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } address || !address.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
GrantType = GrantTypes.RefreshToken,
Issuer = registration.Issuer,
RefreshToken = token,
Registration = registration,
TokenEndpoint = address,
TokenRequest = parameters is not null ? new(parameters) : null,
};
if (scopes is { Length: > 0 })
{
context.Scopes.UnionWith(scopes);
}
if (properties is { Count: > 0 })
{
foreach (var property in properties)
{
context.Properties[property.Key] = property.Value;
}
}
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Create a composite principal containing claims resolved from the
// backchannel identity token and the userinfo token, if available.
return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Retrieves the OpenID Connect server configuration from the specified address.
/// </summary>
@ -32,7 +372,7 @@ public sealed class OpenIddictClientService
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The OpenID Connect server configuration retrieved from the remote server.</returns>
public async ValueTask<OpenIddictConfiguration> GetConfigurationAsync(
internal async ValueTask<OpenIddictConfiguration> GetConfigurationAsync(
OpenIddictClientRegistration registration, Uri address, CancellationToken cancellationToken = default)
{
if (registration is null)
@ -190,7 +530,7 @@ public sealed class OpenIddictClientService
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The security keys retrieved from the remote server.</returns>
public async ValueTask<JsonWebKeySet> GetSecurityKeysAsync(
internal async ValueTask<JsonWebKeySet> GetSecurityKeysAsync(
OpenIddictClientRegistration registration, Uri address, CancellationToken cancellationToken = default)
{
if (registration is null)
@ -342,277 +682,28 @@ public sealed class OpenIddictClientService
}
}
/// <summary>
/// Authenticates using the client credentials grant and resolves the corresponding tokens.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithClientCredentialsAsync(
OpenIddictClientRegistration registration, CancellationToken cancellationToken = default)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } ||
!configuration.TokenEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
GrantType = GrantTypes.ClientCredentials,
Issuer = registration.Issuer,
Registration = registration
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Create a composite principal containing claims resolved from the
// backchannel identity token and the userinfo token, if available.
return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="username">The username to use.</param>
/// <param name="password">The password to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithPasswordAsync(
OpenIddictClientRegistration registration, string username, string password, CancellationToken cancellationToken = default)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
if (string.IsNullOrEmpty(username))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0335), nameof(username));
}
if (string.IsNullOrEmpty(password))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0336), nameof(password));
}
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } ||
!configuration.TokenEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
GrantType = GrantTypes.Password,
Issuer = registration.Issuer,
Password = password,
Registration = registration,
Username = username
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Create a composite principal containing claims resolved from the
// backchannel identity token and the userinfo token, if available.
return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Authenticates using the refresh token grant and resolves the corresponding tokens.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="token">The refresh token to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithRefreshTokenAsync(
OpenIddictClientRegistration registration, string token, CancellationToken cancellationToken = default)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0156), nameof(token));
}
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } ||
!configuration.TokenEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
GrantType = GrantTypes.RefreshToken,
Issuer = registration.Issuer,
RefreshToken = token,
Registration = registration
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
// Create a composite principal containing claims resolved from the
// backchannel identity token and the userinfo token, if available.
return (context.TokenResponse, OpenIddictHelpers.CreateMergedPrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Sends the token request and retrieves the corresponding response.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="address">The address of the token endpoint.</param>
/// <param name="request">The token request.</param>
/// <param name="address">The address of the remote token endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The token response.</returns>
public async ValueTask<OpenIddictResponse> SendTokenRequestAsync(
OpenIddictClientRegistration registration, Uri address, OpenIddictRequest request, CancellationToken cancellationToken = default)
internal async ValueTask<OpenIddictResponse> SendTokenRequestAsync(
OpenIddictClientRegistration registration, OpenIddictRequest request,
Uri? address = null, CancellationToken cancellationToken = default)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
if (address is null)
{
throw new ArgumentNullException(nameof(address));
@ -626,12 +717,6 @@ public sealed class OpenIddictClientService
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } ||
!configuration.TokenEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
@ -772,12 +857,12 @@ public sealed class OpenIddictClientService
/// Sends the userinfo request and retrieves the corresponding response.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="address">The address of the userinfo endpoint.</param>
/// <param name="request">The userinfo request.</param>
/// <param name="address">The address of the remote userinfo endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and the principal extracted from the userinfo response or the userinfo token.</returns>
public async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserinfoRequestAsync(
OpenIddictClientRegistration registration, Uri address, OpenIddictRequest request, CancellationToken cancellationToken = default)
internal async ValueTask<(OpenIddictResponse Response, (ClaimsPrincipal? Principal, string? Token))> SendUserinfoRequestAsync(
OpenIddictClientRegistration registration, OpenIddictRequest request, Uri address, CancellationToken cancellationToken = default)
{
if (registration is null)
{
@ -797,12 +882,6 @@ public sealed class OpenIddictClientService
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } ||
!configuration.TokenEndpoint.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot

140
src/OpenIddict.Validation/OpenIddictValidationService.cs

@ -24,13 +24,75 @@ public sealed class OpenIddictValidationService
public OpenIddictValidationService(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <summary>
/// Validates the specified access token and returns the principal extracted from the token.
/// </summary>
/// <param name="token">The access token to validate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The principal containing the claims extracted from the token.</returns>
public async ValueTask<ClaimsPrincipal> ValidateAccessTokenAsync(string token, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0162), nameof(token));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ValidateTokenContext(transaction)
{
Token = token,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0163(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
return context.Principal;
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Retrieves the OpenID Connect server configuration from the specified address.
/// </summary>
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The OpenID Connect server configuration retrieved from the remote server.</returns>
public async ValueTask<OpenIddictConfiguration> GetConfigurationAsync(Uri address, CancellationToken cancellationToken = default)
internal async ValueTask<OpenIddictConfiguration> GetConfigurationAsync(Uri address, CancellationToken cancellationToken = default)
{
if (address is null)
{
@ -173,7 +235,7 @@ public sealed class OpenIddictValidationService
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The security keys retrieved from the remote server.</returns>
public async ValueTask<JsonWebKeySet> GetSecurityKeysAsync(Uri address, CancellationToken cancellationToken = default)
internal async ValueTask<JsonWebKeySet> GetSecurityKeysAsync(Uri address, CancellationToken cancellationToken = default)
{
if (address is null)
{
@ -310,16 +372,6 @@ public sealed class OpenIddictValidationService
}
}
/// <summary>
/// Sends an introspection request to the specified address and returns the corresponding principal.
/// </summary>
/// <param name="address">The address of the remote metadata endpoint.</param>
/// <param name="token">The token to introspect.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The claims principal created from the claim retrieved from the remote server.</returns>
public ValueTask<ClaimsPrincipal> IntrospectTokenAsync(Uri address, string token, CancellationToken cancellationToken = default)
=> IntrospectTokenAsync(address, token, hint: null, cancellationToken);
/// <summary>
/// Sends an introspection request to the specified address and returns the corresponding principal.
/// </summary>
@ -328,7 +380,7 @@ public sealed class OpenIddictValidationService
/// <param name="hint">The token type to introspect, used as a hint by the authorization server.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The claims principal created from the claim retrieved from the remote server.</returns>
public async ValueTask<ClaimsPrincipal> IntrospectTokenAsync(
internal async ValueTask<ClaimsPrincipal> IntrospectTokenAsync(
Uri address, string token, string? hint, CancellationToken cancellationToken = default)
{
if (address is null)
@ -475,66 +527,4 @@ public sealed class OpenIddictValidationService
}
}
}
/// <summary>
/// Validates the specified access token and returns the principal extracted from the token.
/// </summary>
/// <param name="token">The access token to validate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The principal containing the claims extracted from the token.</returns>
public async ValueTask<ClaimsPrincipal> ValidateAccessTokenAsync(string token, CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(token))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0162), nameof(token));
}
cancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the validation provider. To work around
// this limitation, a scope is manually created for each method to this service.
var scope = _provider.CreateScope();
// Note: a try/finally block is deliberately used here to ensure the service scope
// can be disposed of asynchronously if it implements IAsyncDisposable.
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ValidateTokenContext(transaction)
{
Token = token,
ValidTokenTypes = { TokenTypeHints.AccessToken }
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0163(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
return context.Principal;
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
}

Loading…
Cancel
Save