/* * 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.Client; public sealed class OpenIddictClientService { private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// /// The service provider. public OpenIddictClientService(IServiceProvider provider) => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// /// Retrieves the OpenID Connect server configuration from the specified address. /// /// The client registration. /// 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( OpenIddictClientRegistration registration, Uri address, CancellationToken cancellationToken = default) { if (registration is null) { throw new ArgumentNullException(nameof(address)); } if (address is null) { throw new ArgumentNullException(nameof(address)); } if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), 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 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) { Address = address, Issuer = registration.Issuer, Registration = registration, 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) { Address = address, Issuer = registration.Issuer, Registration = registration, 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.Address, context.Request); return context.Request; } async ValueTask ExtractConfigurationResponseAsync() { var context = new ExtractConfigurationResponseContext(transaction) { Address = address, Issuer = registration.Issuer, Registration = registration, 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.Address, context.Response); return context.Response; } async ValueTask HandleConfigurationResponseAsync() { var context = new HandleConfigurationResponseContext(transaction) { Address = address, Issuer = registration.Issuer, Registration = registration, 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 client registration. /// 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( OpenIddictClientRegistration registration, Uri address, CancellationToken cancellationToken = default) { if (registration is null) { throw new ArgumentNullException(nameof(registration)); } if (address is null) { throw new ArgumentNullException(nameof(address)); } if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), 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 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) { Address = address, Issuer = registration.Issuer, Registration = registration, 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) { Address = address, Issuer = registration.Issuer, Registration = registration, 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.Address, context.Request); return context.Request; } async ValueTask ExtractCryptographyResponseAsync() { var context = new ExtractCryptographyResponseContext(transaction) { Address = address, Issuer = registration.Issuer, Registration = registration, 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.Address, context.Response); return context.Response; } async ValueTask HandleCryptographyResponseAsync() { var context = new HandleCryptographyResponseContext(transaction) { Address = address, Issuer = registration.Issuer, Registration = registration, 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(); } } } /// /// 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 that can be used to abort the operation. /// The token response. public async ValueTask SendTokenRequestAsync( OpenIddictClientRegistration registration, Uri address, OpenIddictRequest request, CancellationToken cancellationToken = default) { if (registration is null) { throw new ArgumentNullException(nameof(registration)); } if (address is null) { throw new ArgumentNullException(nameof(address)); } if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); } 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(); request = await PrepareTokenRequestAsync(); request = await ApplyTokenRequestAsync(); var response = await ExtractTokenResponseAsync(); return await HandleTokenResponseAsync(); async ValueTask PrepareTokenRequestAsync() { var context = new PrepareTokenRequestContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0320(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ApplyTokenRequestAsync() { var context = new ApplyTokenRequestContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0321(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } context.Logger.LogInformation(SR.GetResourceString(SR.ID6192), context.Address, context.Request); return context.Request; } async ValueTask ExtractTokenResponseAsync() { var context = new ExtractTokenResponseContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0322(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.Address, context.Response); return context.Response; } async ValueTask HandleTokenResponseAsync() { var context = new HandleTokenResponseContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request, Response = response }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0323(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.Response; } } finally { if (scope is IAsyncDisposable disposable) { await disposable.DisposeAsync(); } else { scope.Dispose(); } } } /// /// Sends the userinfo request and retrieves the corresponding response. /// /// The client registration. /// The address of the userinfo endpoint. /// The userinfo request. /// 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) { if (registration is null) { throw new ArgumentNullException(nameof(registration)); } if (address is null) { throw new ArgumentNullException(nameof(address)); } if (!address.IsAbsoluteUri || !address.IsWellFormedOriginalString()) { throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(address)); } 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(); request = await PrepareUserinfoRequestAsync(); request = await ApplyUserinfoRequestAsync(); var (response, token) = await ExtractUserinfoResponseAsync(); return await HandleUserinfoResponseAsync(); async ValueTask PrepareUserinfoRequestAsync() { var context = new PrepareUserinfoRequestContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0324(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return context.Request; } async ValueTask ApplyUserinfoRequestAsync() { var context = new ApplyUserinfoRequestContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0325(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } context.Logger.LogInformation(SR.GetResourceString(SR.ID6194), context.Address, context.Request); return context.Request; } async ValueTask<(OpenIddictResponse, string?)> ExtractUserinfoResponseAsync() { var context = new ExtractUserinfoResponseContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0326(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.ID6195), context.Address, context.Response); return (context.Response, context.UserinfoToken); } async ValueTask<(OpenIddictResponse, (ClaimsPrincipal?, string?))> HandleUserinfoResponseAsync() { var context = new HandleUserinfoResponseContext(transaction) { Address = address, Configuration = configuration, Issuer = registration.Issuer, Registration = registration, Request = request, Response = response, UserinfoToken = token }; await dispatcher.DispatchAsync(context); if (context.IsRejected) { throw new ProtocolException( SR.FormatID0327(context.Error, context.ErrorDescription, context.ErrorUri), context.Error, context.ErrorDescription, context.ErrorUri); } return (context.Response, (context.Principal, context.UserinfoToken)); } } finally { if (scope is IAsyncDisposable disposable) { await disposable.DisposeAsync(); } else { scope.Dispose(); } } } }