/* * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) * See https://github.com/openiddict/openiddict-core for more information concerning * the license and the contributors participating to this project. */ using System.Diagnostics; using System.Security.Claims; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; using OpenIddict.Extensions; using static OpenIddict.Abstractions.OpenIddictExceptions; namespace OpenIddict.Validation; /// /// Provides high-level APIs for performing various authentication operations. /// public class OpenIddictValidationService { private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// /// The service provider. 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. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); var context = new ProcessAuthenticationContext(transaction) { AccessToken = token }; 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.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); return context.AccessTokenPrincipal; } /// /// Retrieves the OpenID Connect server configuration from the specified URI. /// /// The URI of the remote metadata endpoint. /// The that can be used to abort the operation. /// The OpenID Connect server configuration retrieved from the remote server. internal async ValueTask GetConfigurationAsync(Uri uri, CancellationToken cancellationToken = default) { if (uri is null) { throw new ArgumentNullException(nameof(uri)); } if (!uri.IsAbsoluteUri) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); } 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. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); var request = new OpenIddictRequest(); request = await PrepareConfigurationRequestAsync(); request = await ApplyConfigurationRequestAsync(); var response = await ExtractConfigurationResponseAsync(); return await HandleConfigurationResponseAsync() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0145)); async ValueTask PrepareConfigurationRequestAsync() { var context = new PrepareConfigurationRequestContext(transaction) { RemoteUri = uri, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0148(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ApplyConfigurationRequestAsync() { var context = new ApplyConfigurationRequestContext(transaction) { RemoteUri = uri, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0149(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } context.Logger.LogInformation(SR.GetResourceString(SR.ID6186), context.RemoteUri, context.Request); return context.Request; } async ValueTask ExtractConfigurationResponseAsync() { var context = new ExtractConfigurationResponseContext(transaction) { RemoteUri = uri, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0150(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); context.Logger.LogInformation(SR.GetResourceString(SR.ID6187), context.RemoteUri, context.Response); return context.Response; } async ValueTask HandleConfigurationResponseAsync() { var context = new HandleConfigurationResponseContext(transaction) { RemoteUri = uri, Request = request, Response = response }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0151(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.Configuration; } } /// /// Retrieves the security keys exposed by the specified JSON Web Key Set endpoint. /// /// The URI of the remote metadata endpoint. /// The that can be used to abort the operation. /// The security keys retrieved from the remote server. internal async ValueTask GetSecurityKeysAsync(Uri uri, CancellationToken cancellationToken = default) { if (uri is null) { throw new ArgumentNullException(nameof(uri)); } if (!uri.IsAbsoluteUri) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); } 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. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); var request = new OpenIddictRequest(); request = await PrepareJsonWebKeySetRequestAsync(); request = await ApplyJsonWebKeySetRequestAsync(); var response = await ExtractJsonWebKeySetResponseAsync(); return await HandleJsonWebKeySetResponseAsync() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0147)); async ValueTask PrepareJsonWebKeySetRequestAsync() { var context = new PrepareJsonWebKeySetRequestContext(transaction) { RemoteUri = uri, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0152(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ApplyJsonWebKeySetRequestAsync() { var context = new ApplyJsonWebKeySetRequestContext(transaction) { RemoteUri = uri, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0153(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } context.Logger.LogInformation(SR.GetResourceString(SR.ID6188), context.RemoteUri, context.Request); return context.Request; } async ValueTask ExtractJsonWebKeySetResponseAsync() { var context = new ExtractJsonWebKeySetResponseContext(transaction) { RemoteUri = uri, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0154(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); context.Logger.LogInformation(SR.GetResourceString(SR.ID6189), context.RemoteUri, context.Response); return context.Response; } async ValueTask HandleJsonWebKeySetResponseAsync() { var context = new HandleJsonWebKeySetResponseContext(transaction) { Request = request, Response = response }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0155(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.SecurityKeys; } } /// /// Sends the introspection request and retrieves the corresponding response. /// /// The server configuration. /// The token request. /// The uri of the remote token endpoint. /// The client authentication method, if applicable. /// The that can be used to abort the operation. /// The response and the principal extracted from the introspection response. internal async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> SendIntrospectionRequestAsync( OpenIddictConfiguration configuration, OpenIddictRequest request, Uri uri, string? method, CancellationToken cancellationToken = default) { if (configuration is null) { throw new ArgumentNullException(nameof(configuration)); } if (request is null) { throw new ArgumentNullException(nameof(request)); } if (uri is null) { throw new ArgumentNullException(nameof(uri)); } if (!uri.IsAbsoluteUri || OpenIddictHelpers.IsImplicitFileUri(uri)) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); } 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. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); request = await PrepareIntrospectionRequestAsync(); request = await ApplyIntrospectionRequestAsync(); var response = await ExtractIntrospectionResponseAsync(); return await HandleIntrospectionResponseAsync(); async ValueTask PrepareIntrospectionRequestAsync() { var context = new PrepareIntrospectionRequestContext(transaction) { CancellationToken = cancellationToken, ClientAuthenticationMethod = method, RemoteUri = uri, Configuration = configuration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0158(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ApplyIntrospectionRequestAsync() { var context = new ApplyIntrospectionRequestContext(transaction) { CancellationToken = cancellationToken, RemoteUri = uri, Configuration = configuration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0159(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } context.Logger.LogInformation(SR.GetResourceString(SR.ID6192), context.RemoteUri, context.Request); return context.Request; } async ValueTask ExtractIntrospectionResponseAsync() { var context = new ExtractIntrospectionResponseContext(transaction) { CancellationToken = cancellationToken, RemoteUri = uri, Configuration = configuration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0160(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } Debug.Assert(context.Response is not null, SR.GetResourceString(SR.ID4007)); context.Logger.LogInformation(SR.GetResourceString(SR.ID6193), context.RemoteUri, context.Response); return context.Response; } async ValueTask<(OpenIddictResponse, ClaimsPrincipal)> HandleIntrospectionResponseAsync() { var context = new HandleIntrospectionResponseContext(transaction) { CancellationToken = cancellationToken, RemoteUri = uri, Configuration = configuration, Request = request, Response = response }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0161(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.Response, context.Principal); } } }