diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index 023e228c..5aa91f05 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1355,6 +1355,9 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
The nonce cannot be resolved from the state token.
+
+ No issuer was specified in the authentication context.
+
The security token is missing.
diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs
index 100e9be2..c5e775fa 100644
--- a/src/OpenIddict.Client/OpenIddictClientEvents.cs
+++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs
@@ -294,6 +294,11 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value;
}
+ ///
+ /// Gets the user-defined authentication properties, if available.
+ ///
+ public Dictionary Properties { get; } = new(StringComparer.Ordinal);
+
///
/// Gets or sets the grant type used for the authentication demand, if applicable.
///
@@ -309,6 +314,11 @@ public static partial class OpenIddictClientEvents
///
public string? RequestForgeryProtection { get; set; }
+ ///
+ /// Gets the scopes that will be sent to the authorization server, if applicable.
+ ///
+ public HashSet Scopes { get; } = new(StringComparer.Ordinal);
+
///
/// Gets or sets the address of the token endpoint, if applicable.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index ea66b47d..a663a72b 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/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
}
}
+ ///
+ /// Contains the logic responsible for resolving the client registration applicable to the authentication demand.
+ ///
+ public sealed class ResolveClientRegistrationFromAuthenticationContext : IOpenIddictClientHandler
+ {
+ ///
+ /// Gets the default descriptor definition assigned to this handler.
+ ///
+ public static OpenIddictClientHandlerDescriptor Descriptor { get; }
+ = OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .UseSingletonHandler()
+ .SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000)
+ .SetType(OpenIddictClientHandlerType.BuiltIn)
+ .Build();
+
+ ///
+ 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));
+ }
+ }
+ }
+
///
/// Contains the logic responsible for determining the types of tokens to validate upfront.
///
@@ -225,7 +287,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
.UseSingletonHandler()
- .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;
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs
index 2b8ddf09..8f162e6f 100644
--- a/src/OpenIddict.Client/OpenIddictClientService.cs
+++ b/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));
+ ///
+ /// Authenticates using the client credentials grant and resolves the corresponding tokens.
+ ///
+ /// The issuer.
+ /// The scopes to request to the authorization server.
+ /// The additional parameters to send as part of the token request.
+ /// The application-specific properties that will be added to the authentication context.
+ /// The that can be used to abort the operation.
+ /// The response and a merged principal containing the claims extracted from the tokens and userinfo response.
+ public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithClientCredentialsAsync(
+ Uri issuer, string[]? scopes = null,
+ Dictionary? parameters = null,
+ Dictionary? 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>();
+ 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();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ 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();
+ }
+ }
+ }
+
+ ///
+ /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
+ ///
+ /// The issuer.
+ /// The username to use.
+ /// The password to use.
+ /// The scopes to request to the authorization server.
+ /// The additional parameters to send as part of the token request.
+ /// The application-specific properties that will be added to the authentication context.
+ /// The that can be used to abort the operation.
+ /// The response and a merged principal containing the claims extracted from the tokens and userinfo response.
+ public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithPasswordAsync(
+ Uri issuer, string username, string password, string[]? scopes = null,
+ Dictionary? parameters = null,
+ Dictionary? 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>();
+ 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();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ 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();
+ }
+ }
+ }
+
+ ///
+ /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
+ ///
+ /// The issuer.
+ /// The refresh token to use.
+ /// The scopes to request to the authorization server.
+ /// The additional parameters to send as part of the token request.
+ /// The application-specific properties that will be added to the authentication context.
+ /// The that can be used to abort the operation.
+ /// The response and a merged principal containing the claims extracted from the tokens and userinfo response.
+ public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithRefreshTokenAsync(
+ Uri issuer, string token, string[]? scopes = null,
+ Dictionary? parameters = null,
+ Dictionary? 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>();
+ 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();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ 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();
+ }
+ }
+ }
+
///
/// Retrieves the OpenID Connect server configuration from the specified address.
///
@@ -32,7 +372,7 @@ public sealed class OpenIddictClientService
/// The address of the remote metadata endpoint.
/// The that can be used to abort the operation.
/// The OpenID Connect server configuration retrieved from the remote server.
- public async ValueTask GetConfigurationAsync(
+ internal async ValueTask GetConfigurationAsync(
OpenIddictClientRegistration registration, Uri address, CancellationToken cancellationToken = default)
{
if (registration is null)
@@ -190,7 +530,7 @@ public sealed class OpenIddictClientService
/// The address of the remote metadata endpoint.
/// The that can be used to abort the operation.
/// The security keys retrieved from the remote server.
- public async ValueTask GetSecurityKeysAsync(
+ internal async ValueTask GetSecurityKeysAsync(
OpenIddictClientRegistration registration, Uri address, CancellationToken cancellationToken = default)
{
if (registration is null)
@@ -342,277 +682,28 @@ public sealed class OpenIddictClientService
}
}
- ///
- /// Authenticates using the client credentials grant and resolves the corresponding tokens.
- ///
- /// The client registration.
- /// The that can be used to abort the operation.
- /// The response and a merged principal containing the claims extracted from the tokens and userinfo response.
- 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();
- var factory = scope.ServiceProvider.GetRequiredService();
- 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();
- }
- }
- }
-
- ///
- /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
- ///
- /// The client registration.
- /// The username to use.
- /// The password to use.
- /// The that can be used to abort the operation.
- /// The response and a merged principal containing the claims extracted from the tokens and userinfo response.
- 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();
- var factory = scope.ServiceProvider.GetRequiredService();
- 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();
- }
- }
- }
-
- ///
- /// Authenticates using the refresh token grant and resolves the corresponding tokens.
- ///
- /// The client registration.
- /// The refresh token to use.
- /// The that can be used to abort the operation.
- /// The response and a merged principal containing the claims extracted from the tokens and userinfo response.
- 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();
- var factory = scope.ServiceProvider.GetRequiredService();
- 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();
- }
- }
- }
-
///
/// Sends the token request and retrieves the corresponding response.
///
/// The client registration.
- /// The address of the token endpoint.
/// The token request.
+ /// The address of the remote token endpoint.
/// The that can be used to abort the operation.
/// The token response.
- public async ValueTask SendTokenRequestAsync(
- OpenIddictClientRegistration registration, Uri address, OpenIddictRequest request, CancellationToken cancellationToken = default)
+ internal async ValueTask 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.
///
/// The client registration.
- /// The address of the userinfo endpoint.
/// The userinfo request.
+ /// The address of the remote userinfo endpoint.
/// The that can be used to abort the operation.
/// The response and the principal extracted from the userinfo response or the userinfo token.
- 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
diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs
index 546af2c3..a19247f1 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationService.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs
@@ -24,13 +24,75 @@ public sealed class OpenIddictValidationService
public OpenIddictValidationService(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
+ ///
+ /// Validates the specified access token and returns the principal extracted from the token.
+ ///
+ /// The access token to validate.
+ /// The that can be used to abort the operation.
+ /// The principal containing the claims extracted from the token.
+ public async ValueTask 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();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ 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();
+ }
+ }
+ }
+
///
/// Retrieves the OpenID Connect server configuration from the specified address.
///
/// The address of the remote metadata endpoint.
/// The that can be used to abort the operation.
/// The OpenID Connect server configuration retrieved from the remote server.
- public async ValueTask GetConfigurationAsync(Uri address, CancellationToken cancellationToken = default)
+ internal async ValueTask GetConfigurationAsync(Uri address, CancellationToken cancellationToken = default)
{
if (address is null)
{
@@ -173,7 +235,7 @@ public sealed class OpenIddictValidationService
/// The address of the remote metadata endpoint.
/// The that can be used to abort the operation.
/// The security keys retrieved from the remote server.
- public async ValueTask GetSecurityKeysAsync(Uri address, CancellationToken cancellationToken = default)
+ internal async ValueTask GetSecurityKeysAsync(Uri address, CancellationToken cancellationToken = default)
{
if (address is null)
{
@@ -310,16 +372,6 @@ public sealed class OpenIddictValidationService
}
}
- ///
- /// Sends an introspection request to the specified address and returns the corresponding principal.
- ///
- /// The address of the remote metadata endpoint.
- /// The token to introspect.
- /// The that can be used to abort the operation.
- /// The claims principal created from the claim retrieved from the remote server.
- public ValueTask IntrospectTokenAsync(Uri address, string token, CancellationToken cancellationToken = default)
- => IntrospectTokenAsync(address, token, hint: null, cancellationToken);
-
///
/// Sends an introspection request to the specified address and returns the corresponding principal.
///
@@ -328,7 +380,7 @@ public sealed class OpenIddictValidationService
/// The token type to introspect, used as a hint by the authorization server.
/// The that can be used to abort the operation.
/// The claims principal created from the claim retrieved from the remote server.
- public async ValueTask IntrospectTokenAsync(
+ internal async ValueTask IntrospectTokenAsync(
Uri address, string token, string? hint, CancellationToken cancellationToken = default)
{
if (address is null)
@@ -475,66 +527,4 @@ public sealed class OpenIddictValidationService
}
}
}
-
- ///
- /// Validates the specified access token and returns the principal extracted from the token.
- ///
- /// The access token to validate.
- /// The that can be used to abort the operation.
- /// The principal containing the claims extracted from the token.
- public async ValueTask 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();
- var factory = scope.ServiceProvider.GetRequiredService();
- 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();
- }
- }
- }
}