/* * 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.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Extensions; using static OpenIddict.Abstractions.OpenIddictExceptions; namespace OpenIddict.Validation; public sealed 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. 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 options = _provider.GetRequiredService>(); var configuration = await options.CurrentValue.ConfigurationManager .GetConfigurationAsync(cancellationToken) .WaitAsync(cancellationToken) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); var context = new ValidateTokenContext(transaction) { Configuration = configuration, 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 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. 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 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; } } finally { if (scope is IAsyncDisposable disposable) { await disposable.DisposeAsync(); } else { scope.Dispose(); } } } /// /// Retrieves the security keys exposed by the specified JWKS 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. 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 request = new OpenIddictRequest(); request = await PrepareCryptographyRequestAsync(); request = await ApplyCryptographyRequestAsync(); var response = await ExtractCryptographyResponseAsync(); return await HandleCryptographyResponseAsync() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0147)); async ValueTask PrepareCryptographyRequestAsync() { var context = new PrepareCryptographyRequestContext(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 ApplyCryptographyRequestAsync() { var context = new ApplyCryptographyRequestContext(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 ExtractCryptographyResponseAsync() { var context = new ExtractCryptographyResponseContext(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 HandleCryptographyResponseAsync() { var context = new HandleCryptographyResponseContext(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; } } finally { if (scope is IAsyncDisposable disposable) { await disposable.DisposeAsync(); } else { scope.Dispose(); } } } /// /// Sends an introspection request to the specified URI and returns the corresponding principal. /// /// The URI of the remote metadata endpoint. /// The token to introspect. /// 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. internal async ValueTask IntrospectTokenAsync( Uri uri, string token, string? hint, CancellationToken cancellationToken = default) { if (uri is null) { throw new ArgumentNullException(nameof(uri)); } if (!uri.IsAbsoluteUri || !uri.IsWellFormedOriginalString()) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); } if (string.IsNullOrEmpty(token)) { throw new ArgumentException(SR.GetResourceString(SR.ID0156), 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 options = _provider.GetRequiredService>(); var configuration = await options.CurrentValue.ConfigurationManager .GetConfigurationAsync(cancellationToken) .WaitAsync(cancellationToken) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); var dispatcher = scope.ServiceProvider.GetRequiredService(); var factory = scope.ServiceProvider.GetRequiredService(); var transaction = await factory.CreateTransactionAsync(); var request = new OpenIddictRequest(); request = await PrepareIntrospectionRequestAsync(); request = await ApplyIntrospectionRequestAsync(); var response = await ExtractIntrospectionResponseAsync(); return await HandleIntrospectionResponseAsync() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0157)); async ValueTask PrepareIntrospectionRequestAsync() { var context = new PrepareIntrospectionRequestContext(transaction) { RemoteUri = uri, Configuration = configuration, Request = request, Token = token, TokenTypeHint = hint }; 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) { 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.ID6190), context.RemoteUri, context.Request); return context.Request; } async ValueTask ExtractIntrospectionResponseAsync() { var context = new ExtractIntrospectionResponseContext(transaction) { 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.ID6191), context.RemoteUri, context.Response); return context.Response; } async ValueTask HandleIntrospectionResponseAsync() { var context = new HandleIntrospectionResponseContext(transaction) { RemoteUri = uri, Configuration = configuration, Request = request, Response = response, Token = token }; 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.Principal; } } finally { if (scope is IAsyncDisposable disposable) { await disposable.DisposeAsync(); } else { scope.Dispose(); } } } }