/* * 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; using System.Security.Claims; using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using static OpenIddict.Validation.OpenIddictValidationEvents; namespace OpenIddict.Validation { public class OpenIddictValidationService { private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// /// The service provider. public OpenIddictValidationService([NotNull] IServiceProvider provider) => _provider = provider; /// /// 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( [NotNull] Uri address, CancellationToken cancellationToken = default) { if (address == null) { throw new ArgumentNullException(nameof(address)); } if (!address.IsAbsoluteUri) { throw new ArgumentException("The address must be an absolute URI.", nameof(address)); } 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 provider = scope.ServiceProvider.GetRequiredService(); var transaction = await provider.CreateTransactionAsync(); var request = new OpenIddictRequest(); request = await PrepareConfigurationRequestAsync(); request = await ApplyConfigurationRequestAsync(); var response = await ExtractConfigurationResponseAsync(); var configuration = await HandleConfigurationResponseAsync(); if (configuration == null) { throw new InvalidOperationException("The OpenID Connect server configuration couldn't be retrieved."); } return configuration; async ValueTask PrepareConfigurationRequestAsync() { var context = new PrepareConfigurationRequestContext(transaction) { Address = address, Request = request }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while preparing the configuration request.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ApplyConfigurationRequestAsync() { var context = new ApplyConfigurationRequestContext(transaction) { Request = request }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while sending the configuration request.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ExtractConfigurationResponseAsync() { var context = new ExtractConfigurationResponseContext(transaction) { Request = request }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while extracting the configuration response.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, context.Error, context.ErrorDescription, context.ErrorUri); } return context.Response; } async ValueTask HandleConfigurationResponseAsync() { var context = new HandleConfigurationResponseContext(transaction) { Request = request, Response = response }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while handling the configuration response.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, 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 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( [NotNull] Uri address, CancellationToken cancellationToken = default) { if (address == null) { throw new ArgumentNullException(nameof(address)); } if (!address.IsAbsoluteUri) { throw new ArgumentException("The address must be an absolute URI.", nameof(address)); } 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 provider = scope.ServiceProvider.GetRequiredService(); var transaction = await provider.CreateTransactionAsync(); var request = new OpenIddictRequest(); request = await PrepareCryptographyRequestAsync(); request = await ApplyCryptographyRequestAsync(); var response = await ExtractCryptographyResponseAsync(); var keys = await HandleCryptographyResponseAsync(); if (keys == null) { throw new InvalidOperationException("An unknown error occurred while retrieving the JWK set."); } return keys; async ValueTask PrepareCryptographyRequestAsync() { var context = new PrepareCryptographyRequestContext(transaction) { Address = address, Request = request }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while preparing the cryptography request.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ApplyCryptographyRequestAsync() { var context = new ApplyCryptographyRequestContext(transaction) { Request = request }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while sending the cryptography request.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ExtractCryptographyResponseAsync() { var context = new ExtractCryptographyResponseContext(transaction) { Request = request }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while extracting the cryptography response.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, context.Error, context.ErrorDescription, context.ErrorUri); } return context.Response; } async ValueTask HandleCryptographyResponseAsync() { var context = new HandleCryptographyResponseContext(transaction) { Request = request, Response = response }; await provider.DispatchAsync(context); if (context.IsRejected) { var message = new StringBuilder() .AppendLine("An error occurred while handling the cryptography response.") .AppendFormat("Error: {0}", context.Error ?? "(not available)") .AppendFormat("Error description: {0}", context.ErrorDescription ?? "(not available)") .AppendFormat("Error URI: {0}", context.ErrorUri ?? "(not available)") .ToString(); throw new OpenIddictExceptions.GenericException(message, 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 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( [NotNull] Uri address, [NotNull] string token, CancellationToken cancellationToken = default) => IntrospectTokenAsync(address, token, type: null, cancellationToken); /// /// 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 token type to introspect. /// 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( [NotNull] Uri address, [NotNull] string token, [CanBeNull] string type, CancellationToken cancellationToken = default) { if (address == null) { throw new ArgumentNullException(nameof(address)); } if (!address.IsAbsoluteUri) { throw new ArgumentException("The address must be an absolute URI.", nameof(address)); } if (string.IsNullOrEmpty(token)) { throw new ArgumentException("The token cannot be null or empty.", 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 provider = scope.ServiceProvider.GetRequiredService(); var transaction = await provider.CreateTransactionAsync(); var request = new OpenIddictRequest(); request = await PrepareIntrospectionRequestAsync(); request = await ApplyIntrospectionRequestAsync(); var response = await ExtractIntrospectionResponseAsync(); var principal = await HandleIntrospectionResponseAsync(); if (principal == null) { throw new InvalidOperationException("An unknown error occurred while introspecting the token."); } return principal; async ValueTask PrepareIntrospectionRequestAsync() { var context = new PrepareIntrospectionRequestContext(transaction) { Address = address, Request = request, Token = token, TokenType = type }; await provider.DispatchAsync(context); return context.Request; } async ValueTask ApplyIntrospectionRequestAsync() { var context = new ApplyIntrospectionRequestContext(transaction) { Request = request }; await provider.DispatchAsync(context); return context.Request; } async ValueTask ExtractIntrospectionResponseAsync() { var context = new ExtractIntrospectionResponseContext(transaction) { Request = request }; await provider.DispatchAsync(context); return context.Response; } async ValueTask HandleIntrospectionResponseAsync() { var context = new HandleIntrospectionResponseContext(transaction) { Request = request, Response = response, Token = token, TokenType = type }; await provider.DispatchAsync(context); return context.Principal; } } finally { if (scope is IAsyncDisposable disposable) { await disposable.DisposeAsync(); } else { scope.Dispose(); } } } } }