Browse Source

Update the client stack to support the resource owner password credentials grant

pull/1468/head
Kévin Chalet 4 years ago
parent
commit
b943123a4c
  1. 4
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  2. 18
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 6
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  4. 20
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  5. 10
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  6. 2
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  7. 34
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  8. 4
      src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
  9. 87
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  10. 166
      src/OpenIddict.Client/OpenIddictClientService.cs

4
gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs

@ -1,6 +1,4 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Text;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

18
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1301,6 +1301,18 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID0334" xml:space="preserve">
<value>The '{0}' node cannot be extracted from the response.</value>
</data>
<data name="ID0335" xml:space="preserve">
<value>The username cannot be null or empty.</value>
</data>
<data name="ID0336" xml:space="preserve">
<value>The password cannot be null or empty.</value>
</data>
<data name="ID0337" xml:space="preserve">
<value>A username must be specified when using the resource owner password credentials grant.</value>
</data>
<data name="ID0338" xml:space="preserve">
<value>A password must be specified when using the resource owner password credentials grant.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -1757,6 +1769,12 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID4013" xml:space="preserve">
<value>The issuer should be a valid absolute URL at this point.</value>
</data>
<data name="ID4014" xml:space="preserve">
<value>The username shouldn't be null or empty at this point.</value>
</data>
<data name="ID4015" xml:space="preserve">
<value>The password shouldn't be null or empty at this point.</value>
</data>
<data name="ID6000" xml:space="preserve">
<value>An error occurred while validating the token '{Token}'.</value>
</data>

6
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -133,6 +133,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachNonDefaultResponseMode>()
// Note: this handler MUST be invoked after the scopes have been attached to the
// context to support overriding the response mode based on the requested scopes.
@ -172,6 +173,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<FormatNonStandardScopeParameter>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -189,9 +191,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
{
// The following providers are known to use comma-separated scopes instead of
// the standard format (that requires using a space as the scope separator):
Providers.Reddit
when context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit
=> string.Join(",", context.Scopes),
Providers.Reddit => string.Join(",", context.Scopes),
_ => context.Request.Scope
};

20
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -41,6 +41,16 @@ public class OpenIddictClientConfiguration : IPostConfigureOptions<OpenIddictCli
foreach (var registration in options.Registrations)
{
if (registration.Issuer is not { IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0136));
}
if (!string.IsNullOrEmpty(registration.Issuer.Fragment) || !string.IsNullOrEmpty(registration.Issuer.Query))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0137));
}
if (registration.ConfigurationManager is null)
{
if (registration.Configuration is not null)
@ -65,16 +75,6 @@ public class OpenIddictClientConfiguration : IPostConfigureOptions<OpenIddictCli
if (!registration.MetadataAddress.IsAbsoluteUri)
{
var issuer = registration.Issuer;
if (issuer is not { IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0136));
}
if (!string.IsNullOrEmpty(issuer.Fragment) || !string.IsNullOrEmpty(issuer.Query))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0137));
}
if (!issuer.OriginalString.EndsWith("/", StringComparison.Ordinal))
{
issuer = new Uri(issuer.OriginalString + "/", UriKind.Absolute);

10
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -545,6 +545,16 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? RefreshToken { get; set; }
/// <summary>
/// Gets or sets the username to send to the server, if applicable.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Gets or sets the password to send to the server, if applicable.
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Gets or sets the frontchannel state token to validate, if applicable.
/// </summary>

2
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -36,7 +36,6 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<OpenIddictClientService>();
// Register the built-in filters used by the default OpenIddict client event handlers.
builder.Services.TryAddSingleton<RequireAuthorizationCodeOrImplicitGrantType>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireBackchannelAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireBackchannelIdentityTokenValidated>();
@ -45,6 +44,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireFrontchannelAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireFrontchannelIdentityTokenPrincipal>();
builder.Services.TryAddSingleton<RequireInteractiveGrantType>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequireRedirectionRequest>();
builder.Services.TryAddSingleton<RequireRefreshTokenValidated>();

34
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -11,23 +11,6 @@ namespace OpenIddict.Client;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers if the challenge
/// doesn't correspond to an authorization code or implicit grant operation.
/// </summary>
public class RequireAuthorizationCodeOrImplicitGrantType : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
{
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization code is validated.
/// </summary>
@ -156,6 +139,23 @@ public static class OpenIddictClientHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the challenge
/// doesn't correspond to an authorization code or implicit grant operation.
/// </summary>
public class RequireInteractiveGrantType : IOpenIddictClientHandlerFilter<ProcessChallengeContext>
{
public ValueTask<bool> IsActiveAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token.
/// </summary>

4
src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs

@ -59,7 +59,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseScopedHandler<PrepareAuthorizationRequest>()
.SetOrder(int.MaxValue - 100_000)
.Build();
@ -104,7 +104,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseScopedHandler<ApplyAuthorizationRequest>()
.SetOrder(PrepareAuthorizationRequest.Descriptor.Order + 1_000)
.Build();

87
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -10,8 +10,6 @@ using System.Diagnostics;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
#if !SUPPORTS_TIME_CONSTANT_COMPARISONS
@ -158,12 +156,27 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
}
if (context.GrantType is not GrantTypes.RefreshToken)
if (context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken))
{
throw new InvalidOperationException(SR.FormatID0310(context.GrantType));
}
if (string.IsNullOrEmpty(context.RefreshToken))
if (context.GrantType is GrantTypes.Password)
{
if (string.IsNullOrEmpty(context.Username))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0337));
}
if (string.IsNullOrEmpty(context.Password))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0338));
}
}
if (context.GrantType is GrantTypes.RefreshToken &&
string.IsNullOrEmpty(context.RefreshToken))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0311));
}
@ -1045,7 +1058,7 @@ public static partial class OpenIddictClientHandlers
// In any case, the client identifier of the application MUST be included in the audiences.
// See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information.
var audiences = context.FrontchannelIdentityTokenPrincipal.GetClaims(Claims.Audience);
if (!audiences.Contains(context.Registration.ClientId!))
if (!string.IsNullOrEmpty(context.Registration.ClientId) && !audiences.Contains(context.Registration.ClientId))
{
context.Reject(
error: Errors.InvalidRequest,
@ -1087,7 +1100,7 @@ public static partial class OpenIddictClientHandlers
// Note: the "azp" claim is optional, but if it's present, it MUST match the client identifier of the application.
// See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information.
var presenter = context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.AuthorizedParty);
if (!string.IsNullOrEmpty(presenter) &&
if (!string.IsNullOrEmpty(presenter) && !string.IsNullOrEmpty(context.Registration.ClientId) &&
!string.Equals(presenter, context.Registration.ClientId, StringComparison.Ordinal))
{
context.Reject(
@ -1504,8 +1517,8 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> true,
// For refresh token requests, always send a token request.
GrantTypes.RefreshToken => true,
// For resource owner password credentials and refresh token requests, always send a token request.
GrantTypes.Password or GrantTypes.RefreshToken => true,
_ => false
};
@ -1570,6 +1583,16 @@ public static partial class OpenIddictClientHandlers
context.TokenRequest.RedirectUri = context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri);
}
// If the token request uses a resource owner password credentials grant, attach the credentials to the request.
else if (context.TokenRequest.GrantType is GrantTypes.Password)
{
Debug.Assert(!string.IsNullOrEmpty(context.Username), SR.GetResourceString(SR.ID4014));
Debug.Assert(!string.IsNullOrEmpty(context.Password), SR.GetResourceString(SR.ID4015));
context.TokenRequest.Username = context.Username;
context.TokenRequest.Password = context.Password;
}
// If the token request uses a refresh token grant, attach the refresh token to the request.
else if (context.TokenRequest.GrantType is GrantTypes.RefreshToken)
{
@ -1932,8 +1955,9 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> (true, true, false),
// An access token is always returned as part of refresh token responses.
GrantTypes.RefreshToken => (true, true, false),
// An access token is always returned as part of resource
// owner password credentials and refresh token responses.
GrantTypes.Password or GrantTypes.RefreshToken => (true, true, false),
_ => (false, false, false)
};
@ -1949,6 +1973,14 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code) &&
context.StateTokenPrincipal!.HasScope(Scopes.OpenId) => (true, true, true),
// The resource owner password credentials grant doesn't have an equivalent in
// OpenID Connect so an identity token is typically never returned when using it.
// However, certain server implementations - like OpenIddict - allow returning it
// as a non-standard artifact. As such, the identity token is not considered required
// but will always be validated using the same routine (except nonce validation)
// if it is present in the token response.
GrantTypes.Password => (true, false, true),
// An identity token may or may not be returned as part of refresh token responses
// depending on the policy adopted by the remote authorization server. As such,
// the identity token is not considered required but will always be validated using
@ -1973,10 +2005,11 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> (true, false, false),
// A refresh token may or may not be returned as part of refresh token responses
// depending on the policy adopted by the remote authorization server. As such,
// a refresh token is never considered required for refresh token responses.
GrantTypes.RefreshToken => (true, false, false),
// A refresh token may or may not be returned as part of resource owner password
// credentials and refresh token responses depending on the policy adopted by the
// remote authorization server. As such, a refresh token is never considered
// required for refresh token responses.
GrantTypes.Password or GrantTypes.RefreshToken => (true, false, false),
_ => (false, false, false)
};
@ -2310,7 +2343,7 @@ public static partial class OpenIddictClientHandlers
// In any case, the client identifier of the application MUST be included in the audiences.
// See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information.
var audiences = context.BackchannelIdentityTokenPrincipal.GetClaims(Claims.Audience);
if (!audiences.Contains(context.Registration.ClientId!))
if (!string.IsNullOrEmpty(context.Registration.ClientId) && !audiences.Contains(context.Registration.ClientId))
{
context.Reject(
error: Errors.InvalidRequest,
@ -2352,7 +2385,7 @@ public static partial class OpenIddictClientHandlers
// Note: the "azp" claim is optional, but if it's present, it MUST match the client identifier of the application.
// See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information.
var presenter = context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.AuthorizedParty);
if (!string.IsNullOrEmpty(presenter) &&
if (!string.IsNullOrEmpty(presenter) && !string.IsNullOrEmpty(context.Registration.ClientId) &&
!string.Equals(presenter, context.Registration.ClientId, StringComparison.Ordinal))
{
context.Reject(
@ -2732,7 +2765,8 @@ public static partial class OpenIddictClientHandlers
// endpoint when a frontchannel or backchannel access token is available.
//
// Note: the userinfo endpoint is an optional endpoint and may not be supported.
GrantTypes.AuthorizationCode or GrantTypes.Implicit or GrantTypes.RefreshToken
GrantTypes.AuthorizationCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken
when context.UserinfoEndpoint is not null &&
(!string.IsNullOrEmpty(context.BackchannelAccessToken) ||
!string.IsNullOrEmpty(context.FrontchannelAccessToken)) => true,
@ -3167,6 +3201,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireStateTokenPrincipal>()
.UseScopedHandler<RedeemStateTokenEntry>()
.SetOrder(ValidateUserinfoTokenSubject.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@ -3423,7 +3458,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachResponseType>()
.SetOrder(EvaluateGeneratedChallengeTokens.Descriptor.Order + 1_000)
.Build();
@ -3614,7 +3649,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachResponseMode>()
.SetOrder(AttachResponseType.Descriptor.Order + 1_000)
.Build();
@ -3719,7 +3754,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachClientId>()
.SetOrder(AttachResponseMode.Descriptor.Order + 1_000)
.Build();
@ -3748,7 +3783,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachRedirectUri>()
.SetOrder(AttachClientId.Descriptor.Order + 1_000)
.Build();
@ -3781,7 +3816,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachScopes>()
.SetOrder(AttachRedirectUri.Descriptor.Order + 1_000)
.Build();
@ -3826,7 +3861,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachRequestForgeryProtection>()
.SetOrder(AttachScopes.Descriptor.Order + 1_000)
.Build();
@ -3864,7 +3899,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachNonce>()
.SetOrder(AttachRequestForgeryProtection.Descriptor.Order + 1_000)
.Build();
@ -3907,7 +3942,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<AttachCodeChallengeParameters>()
.SetOrder(AttachNonce.Descriptor.Order + 1_000)
.Build();
@ -4179,7 +4214,7 @@ public static partial class OpenIddictClientHandlers
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireAuthorizationCodeOrImplicitGrantType>()
.AddFilter<RequireInteractiveGrantType>()
.UseSingletonHandler<ValidateRedirectUriParameter>()
.SetOrder(GenerateStateToken.Descriptor.Order + 1_000)
.Build();

166
src/OpenIddict.Client/OpenIddictClientService.cs

@ -332,23 +332,29 @@ public class OpenIddictClientService
}
/// <summary>
/// Refreshes the user tokens using the specified refresh token.
/// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="token">The refresh token to use.</param>
/// <param name="username">The username to use.</param>
/// <param name="password">The password to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> RefreshTokensAsync(
OpenIddictClientRegistration registration, string token, CancellationToken cancellationToken = default)
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(token))
if (string.IsNullOrEmpty(username))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0156), nameof(token));
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) ??
@ -378,10 +384,11 @@ public class OpenIddictClientService
var context = new ProcessAuthenticationContext(transaction)
{
Configuration = configuration,
GrantType = GrantTypes.RefreshToken,
GrantType = GrantTypes.Password,
Issuer = registration.Issuer,
RefreshToken = token,
Registration = registration
Password = password,
Registration = registration,
Username = username
};
await dispatcher.DispatchAsync(context);
@ -414,44 +421,90 @@ public class OpenIddictClientService
scope.Dispose();
}
}
}
/// <summary>
/// Authenticates using the refresh token grant and resolves the corresponding tokens.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="token">The refresh token to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
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));
}
static ClaimsPrincipal CreatePrincipal(params ClaimsPrincipal?[] principals)
var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } ||
!configuration.TokenEndpoint.IsWellFormedOriginalString())
{
// Note: the OpenIddict client handler can be used as a pure OAuth 2.0-only stack for
// delegation scenarios where the identity of the user is not needed. In this case,
// since no principal can be resolved from a token or a userinfo response to construct
// a user identity, a fake one containing an "unauthenticated" identity (i.e with its
// AuthenticationType property deliberately left to null) is used to allow ASP.NET Core
// to return a "successful" authentication result for these delegation-only scenarios.
if (!principals.Any(principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true }))
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
return new ClaimsPrincipal(new ClaimsIdentity());
}
Configuration = configuration,
GrantType = GrantTypes.RefreshToken,
Issuer = registration.Issuer,
RefreshToken = token,
Registration = registration
};
// Create a new composite identity containing the claims of all the principals.
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
await dispatcher.DispatchAsync(context);
foreach (var principal in principals)
if (context.IsRejected)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is null)
{
continue;
}
throw new OpenIddictExceptions.GenericException(
SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
foreach (var claim in principal.Claims)
{
// If a claim with the same type and the same value already exist, skip it.
if (identity.HasClaim(claim.Type, claim.Value))
{
continue;
}
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
identity.AddClaim(claim);
}
// Create a composite principal containing claims resolved from the
// backchannel identity token and the userinfo token, if available.
return (context.TokenResponse, CreatePrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
return new ClaimsPrincipal(identity);
else
{
scope.Dispose();
}
}
}
@ -789,4 +842,43 @@ public class OpenIddictClientService
}
}
}
private static ClaimsPrincipal CreatePrincipal(params ClaimsPrincipal?[] principals)
{
// Note: the OpenIddict client handler can be used as a pure OAuth 2.0-only stack for
// delegation scenarios where the identity of the user is not needed. In this case,
// since no principal can be resolved from a token or a userinfo response to construct
// a user identity, a fake one containing an "unauthenticated" identity (i.e with its
// AuthenticationType property deliberately left to null) is used to allow ASP.NET Core
// to return a "successful" authentication result for these delegation-only scenarios.
if (!principals.Any(principal => principal?.Identity is ClaimsIdentity { IsAuthenticated: true }))
{
return new ClaimsPrincipal(new ClaimsIdentity());
}
// Create a new composite identity containing the claims of all the principals.
var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType);
foreach (var principal in principals)
{
// Note: the principal may be null if no value was extracted from the corresponding token.
if (principal is null)
{
continue;
}
foreach (var claim in principal.Claims)
{
// If a claim with the same type and the same value already exist, skip it.
if (identity.HasClaim(claim.Type, claim.Value))
{
continue;
}
identity.AddClaim(claim);
}
}
return new ClaimsPrincipal(identity);
}
}

Loading…
Cancel
Save