Browse Source

Implement grant_type=refresh_token support in the OpenIddict client

pull/1408/head
Kévin Chalet 4 years ago
parent
commit
5ec1ce631d
  1. 11
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 10
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  3. 1
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  4. 16
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  5. 364
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  6. 123
      src/OpenIddict.Client/OpenIddictClientService.cs

11
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1129,7 +1129,7 @@ Make sure that neither DefaultAuthenticateScheme, DefaultSignInScheme, DefaultSi
</data> </data>
<data name="ID0290" xml:space="preserve"> <data name="ID0290" xml:space="preserve">
<value>An identity cannot be extracted from this request. <value>An identity cannot be extracted from this request.
This generally indicates that the OpenIddict client stack was asked to validate a token for an endpoint it doesn't manage. This generally indicates that the OpenIddict client stack was asked to validate a token for an invalid endpoint.
To validate tokens received by custom API endpoints, the OpenIddict validation handler (e.g OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme or OpenIddictValidationOwinDefaults.AuthenticationType) must be used instead.</value> To validate tokens received by custom API endpoints, the OpenIddict validation handler (e.g OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme or OpenIddictValidationOwinDefaults.AuthenticationType) must be used instead.</value>
</data> </data>
<data name="ID0291" xml:space="preserve"> <data name="ID0291" xml:space="preserve">
@ -1188,6 +1188,15 @@ To apply redirection responses, create a class implementing 'IOpenIddictClientHa
<data name="ID0308" xml:space="preserve"> <data name="ID0308" xml:space="preserve">
<value>The specified list of valid token types is not valid.</value> <value>The specified list of valid token types is not valid.</value>
</data> </data>
<data name="ID0309" xml:space="preserve">
<value>A grant type must be specified when triggering authentication demands from endpoints that are not managed by the OpenIddict client stack.</value>
</data>
<data name="ID0310" xml:space="preserve">
<value>The specified grant type ({0}) is not currently supported for authentication demands.</value>
</data>
<data name="ID0311" xml:space="preserve">
<value>A refresh token must be specified when using the refresh token grant.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>

10
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -284,6 +284,16 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value; set => Transaction.Request = value;
} }
/// <summary>
/// Gets or sets the grant type used for the authentication demand, if applicable.
/// </summary>
public string? GrantType { get; set; }
/// <summary>
/// Gets or sets the response type used for the authentication demand, if applicable.
/// </summary>
public string? ResponseType { get; set; }
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether an authorization /// Gets or sets a boolean indicating whether an authorization
/// code should be extracted from the current context. /// code should be extracted from the current context.

1
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -36,7 +36,6 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<OpenIddictClientService>(); builder.Services.TryAddSingleton<OpenIddictClientService>();
// Register the built-in filters used by the default OpenIddict client event handlers. // Register the built-in filters used by the default OpenIddict client event handlers.
builder.Services.TryAddSingleton<RequireAuthorizationCodeExtracted>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeOrImplicitGrantType>(); builder.Services.TryAddSingleton<RequireAuthorizationCodeOrImplicitGrantType>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>(); builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireBackchannelAccessTokenValidated>(); builder.Services.TryAddSingleton<RequireBackchannelAccessTokenValidated>();

16
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -11,22 +11,6 @@ namespace OpenIddict.Client;
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientHandlerFilters public static class OpenIddictClientHandlerFilters
{ {
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization code is extracted.
/// </summary>
public class RequireAuthorizationCodeExtracted : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.ExtractAuthorizationCode);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers if the challenge /// Represents a filter that excludes the associated handlers if the challenge
/// doesn't correspond to an authorization code or implicit grant operation. /// doesn't correspond to an authorization code or implicit grant operation.

364
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -33,7 +33,8 @@ public static partial class OpenIddictClientHandlers
ResolveClientRegistrationFromStateToken.Descriptor, ResolveClientRegistrationFromStateToken.Descriptor,
ValidateIssuerParameter.Descriptor, ValidateIssuerParameter.Descriptor,
ValidateFrontchannelErrorParameters.Descriptor, ValidateFrontchannelErrorParameters.Descriptor,
ValidateGrantType.Descriptor, ResolveGrantTypeFromStateToken.Descriptor,
ResolveResponseTypeFromStateToken.Descriptor,
EvaluateValidatedFrontchannelTokens.Descriptor, EvaluateValidatedFrontchannelTokens.Descriptor,
ResolveValidatedFrontchannelTokens.Descriptor, ResolveValidatedFrontchannelTokens.Descriptor,
@ -130,11 +131,33 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// Authentication demands can be triggered from the redirection endpoint
// to handle authorization callbacks but also from unknown endpoints
// when using the refresh token grant, to perform a token refresh dance.
switch (context.EndpointType) switch (context.EndpointType)
{ {
case OpenIddictClientEndpointType.Redirection: case OpenIddictClientEndpointType.Redirection:
break; break;
case OpenIddictClientEndpointType.Unknown:
if (string.IsNullOrEmpty(context.GrantType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
}
if (context.GrantType is not GrantTypes.RefreshToken)
{
throw new InvalidOperationException(SR.FormatID0310(context.GrantType));
}
if (string.IsNullOrEmpty(context.RefreshToken))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0311));
}
break;
default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0290)); default: throw new InvalidOperationException(SR.GetResourceString(SR.ID0290));
} }
@ -483,6 +506,7 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.UseSingletonHandler<ValidateFrontchannelErrorParameters>() .UseSingletonHandler<ValidateFrontchannelErrorParameters>()
.SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000) .SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000)
.Build(); .Build();
@ -515,10 +539,10 @@ public static partial class OpenIddictClientHandlers
} }
/// <summary> /// <summary>
/// Contains the logic responsible of ensuring the authentication demand is valid /// Contains the logic responsible of resolving the grant type
/// based on the grant type initially negotiated and stored in the state token. /// initially negotiated and stored in the state token, if applicable.
/// </summary> /// </summary>
public class ValidateGrantType : IOpenIddictClientHandler<ProcessAuthenticationContext> public class ResolveGrantTypeFromStateToken : IOpenIddictClientHandler<ProcessAuthenticationContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -526,7 +550,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>() .AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ValidateGrantType>() .UseSingletonHandler<ResolveGrantTypeFromStateToken>()
.SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000) .SetOrder(ValidateIssuerParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -544,8 +568,8 @@ public static partial class OpenIddictClientHandlers
// Resolve the negotiated grant type from the state token. // Resolve the negotiated grant type from the state token.
var type = context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType); var type = context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType);
// Note: OpenIddict currently only supports the implicit and authorization code // Note: OpenIddict currently only supports the implicit, authorization code and refresh
// grants but additional grants (like CIBA) may be supported in future versions. // token grants but additional grants (like CIBA) may be supported in future versions.
switch (context.EndpointType) switch (context.EndpointType)
{ {
// Authentication demands triggered from the redirection endpoint are only valid for // Authentication demands triggered from the redirection endpoint are only valid for
@ -561,14 +585,17 @@ public static partial class OpenIddictClientHandlers
return default; return default;
} }
context.GrantType = type;
return default; return default;
} }
} }
/// <summary> /// <summary>
/// Contains the logic responsible of determining the set of frontchannel tokens to validate. /// Contains the logic responsible of resolving the response type
/// initially negotiated and stored in the state token, if applicable.
/// </summary> /// </summary>
public class EvaluateValidatedFrontchannelTokens : IOpenIddictClientHandler<ProcessAuthenticationContext> public class ResolveResponseTypeFromStateToken : IOpenIddictClientHandler<ProcessAuthenticationContext>
{ {
/// <summary> /// <summary>
/// Gets the default descriptor definition assigned to this handler. /// Gets the default descriptor definition assigned to this handler.
@ -576,8 +603,8 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>() .AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<EvaluateValidatedFrontchannelTokens>() .UseSingletonHandler<ResolveResponseTypeFromStateToken>()
.SetOrder(ValidateGrantType.Descriptor.Order + 1_000) .SetOrder(ResolveGrantTypeFromStateToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -591,16 +618,39 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Resolve the grant grant and the response type stored in the state token and extract its individual elements. // Resolve the negotiated response type from the state token.
var types = ( context.ResponseType = context.StateTokenPrincipal.GetClaim(Claims.Private.ResponseType);
GrantType: context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType),
ResponseTypes: context.StateTokenPrincipal.GetClaim(Claims.Private.ResponseType) return default;
!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) }
.ToImmutableHashSet()); }
/// <summary>
/// Contains the logic responsible of determining the set of frontchannel tokens to validate.
/// </summary>
public class EvaluateValidatedFrontchannelTokens : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<EvaluateValidatedFrontchannelTokens>()
.SetOrder(ResolveResponseTypeFromStateToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
(context.ExtractAuthorizationCode, (context.ExtractAuthorizationCode,
context.RequireAuthorizationCode, context.RequireAuthorizationCode,
context.ValidateAuthorizationCode) = types switch context.ValidateAuthorizationCode) = context.GrantType switch
{ {
// An authorization code is returned for the authorization code and implicit grants when // An authorization code is returned for the authorization code and implicit grants when
// the response type contains the "code" value, which includes the authorization code // the response type contains the "code" value, which includes the authorization code
@ -610,15 +660,15 @@ public static partial class OpenIddictClientHandlers
// Note: since authorization codes are supposed to be opaque to the clients, they are never // Note: since authorization codes are supposed to be opaque to the clients, they are never
// validated by default. Clients that need to deal with non-standard implementations // validated by default. Clients that need to deal with non-standard implementations
// can use custom handlers to validate access tokens that use a readable format (e.g JWT). // can use custom handlers to validate access tokens that use a readable format (e.g JWT).
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet<string> set) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
when set.Contains(ResponseTypes.Code) => (true, true, false), => (true, true, false),
_ => (false, false, false) _ => (false, false, false)
}; };
(context.ExtractFrontchannelAccessToken, (context.ExtractFrontchannelAccessToken,
context.RequireFrontchannelAccessToken, context.RequireFrontchannelAccessToken,
context.ValidateFrontchannelAccessToken) = types switch context.ValidateFrontchannelAccessToken) = context.GrantType switch
{ {
// An access token is returned for the authorization code and implicit grants when // An access token is returned for the authorization code and implicit grants when
// the response type contains the "token" value, which includes some variations of // the response type contains the "token" value, which includes some variations of
@ -628,15 +678,15 @@ public static partial class OpenIddictClientHandlers
// Note: since access tokens are supposed to be opaque to the clients, they are never // Note: since access tokens are supposed to be opaque to the clients, they are never
// validated by default. Clients that need to deal with non-standard implementations // validated by default. Clients that need to deal with non-standard implementations
// can use custom handlers to validate access tokens that use a readable format (e.g JWT). // can use custom handlers to validate access tokens that use a readable format (e.g JWT).
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet<string> set) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Token)
when set.Contains(ResponseTypes.Token) => (true, true, false), => (true, true, false),
_ => (false, false, false) _ => (false, false, false)
}; };
(context.ExtractFrontchannelIdentityToken, (context.ExtractFrontchannelIdentityToken,
context.RequireFrontchannelIdentityToken, context.RequireFrontchannelIdentityToken,
context.ValidateFrontchannelIdentityToken) = types switch context.ValidateFrontchannelIdentityToken) = context.GrantType switch
{ {
// An identity token is returned for the authorization code and implicit grants when // An identity token is returned for the authorization code and implicit grants when
// the response type contains the "id_token" value, which includes some variations // the response type contains the "id_token" value, which includes some variations
@ -645,13 +695,15 @@ public static partial class OpenIddictClientHandlers
// //
// Note: the granted scopes list (returned as a "scope" parameter in authorization // Note: the granted scopes list (returned as a "scope" parameter in authorization
// responses) is not used in this case as it's not protected against tampering. // responses) is not used in this case as it's not protected against tampering.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet<string> set) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.IdToken)
when set.Contains(ResponseTypes.IdToken) => (true, true, true), => (true, true, true),
_ => (false, false, false) _ => (false, false, false)
}; };
return default; return default;
bool HasResponseType(string value) => context.ResponseType!.Split(Separators.Space).Contains(value);
} }
} }
@ -976,7 +1028,7 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.FrontchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Note: while an identity token typically contains a single audience represented // Note: while an identity token typically contains a single audience represented
// as a JSON string, multiple values can be returned represented a a JSON array. // as a JSON string, multiple values can be returned represented as a JSON array.
// //
// In any case, the client identifier of the application MUST be included in the audiences. // 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. // See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information.
@ -1084,7 +1136,7 @@ public static partial class OpenIddictClientHandlers
return default; return default;
// If the two nonces don't match, return an error. // If the two nonces don't match, return an error.
case { FrontchannelIdentityTokenNonce: { } left, StateTokenNonce: { } right } when case { FrontchannelIdentityTokenNonce: string left, StateTokenNonce: string right } when
#if SUPPORTS_TIME_CONSTANT_COMPARISONS #if SUPPORTS_TIME_CONSTANT_COMPARISONS
!CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)): !CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)):
#else #else
@ -1388,7 +1440,6 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<EvaluateValidatedBackchannelTokens>() .UseSingletonHandler<EvaluateValidatedBackchannelTokens>()
.SetOrder(ValidateAuthorizationCode.Descriptor.Order + 1_000) .SetOrder(ValidateAuthorizationCode.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
@ -1402,18 +1453,9 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Resolve the grant grant and the response type stored in the state token and extract its individual elements.
var types = (
GrantType: context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType),
ResponseTypes: context.StateTokenPrincipal.GetClaim(Claims.Private.ResponseType)
!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)
.ToImmutableHashSet());
(context.ExtractBackchannelAccessToken, (context.ExtractBackchannelAccessToken,
context.RequireBackchannelAccessToken, context.RequireBackchannelAccessToken,
context.ValidateBackchannelAccessToken) = types switch context.ValidateBackchannelAccessToken) = context.GrantType switch
{ {
// An access token is always returned as part of token responses, independently of // An access token is always returned as part of token responses, independently of
// the negotiated response types or whether the server supports OpenID Connect or not. // the negotiated response types or whether the server supports OpenID Connect or not.
@ -1422,46 +1464,61 @@ public static partial class OpenIddictClientHandlers
// Note: since access tokens are supposed to be opaque to the clients, they are never // Note: since access tokens are supposed to be opaque to the clients, they are never
// validated by default. Clients that need to deal with non-standard implementations // validated by default. Clients that need to deal with non-standard implementations
// can use custom handlers to validate access tokens that use a readable format (e.g JWT). // can use custom handlers to validate access tokens that use a readable format (e.g JWT).
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet<string> set) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
when set.Contains(ResponseTypes.Code) => (true, true, false), => (true, true, false),
// An access token is always returned as part of refresh token responses.
GrantTypes.RefreshToken => (true, true, false),
_ => (false, false, false) _ => (false, false, false)
}; };
(context.ExtractBackchannelIdentityToken, (context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken, context.RequireBackchannelIdentityToken,
context.ValidateBackchannelIdentityToken) = types switch context.ValidateBackchannelIdentityToken) = context.GrantType switch
{ {
// An identity token is always returned as part of token responses for the code and // An identity token is always returned as part of token responses for the code and
// hybrid flows when the authorization server supports OpenID Connect. As such, // hybrid flows when the authorization server supports OpenID Connect. As such,
// a backchannel identity token is only considered required if the negotiated scopes // a backchannel identity token is only considered required if the negotiated scopes
// include "openid", which indicates the initial request was an OpenID Connect request. // include "openid", which indicates the initial request was an OpenID Connect request.
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet<string> set) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code) &&
when set.Contains(ResponseTypes.Code) && context.StateTokenPrincipal!.HasScope(Scopes.OpenId) => (true, true, true),
context.StateTokenPrincipal.HasScope(Scopes.OpenId) => (true, true, 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
// the same routine (except nonce validation) if it is present in the token response.
GrantTypes.RefreshToken => (true, false, true),
_ => (false, false, false) _ => (false, false, false)
}; };
(context.ExtractRefreshToken, (context.ExtractRefreshToken,
context.RequireRefreshToken, context.RequireRefreshToken,
context.ValidateRefreshToken) = types switch context.ValidateRefreshToken) = context.GrantType switch
{ {
// A refresh token may be returned as part of token responses, depending on the // A refresh token may be returned as part of token responses, depending on the
// policy enforced by the remote authorization server (e.g the "offline_access" // policy enforced by the remote authorization server (e.g the "offline_access"
// scope may be used). Since the requirements will differ between authorization // scope may be used). Since the requirements will differ between authorization
// servers, a refresh token is never considered required by default. // servers, a refresh token is never considered required by default.
// //
// Note: since refresh tokens are supposed to be opaque to the clients, they are never // Note: since refresh tokens are supposed to be opaque to the clients, they are never
// validated by default. Clients that need to deal with non-standard implementations // validated by default. Clients that need to deal with non-standard implementations
// can use custom handlers to validate access tokens that use a readable format (e.g JWT). // can use custom handlers to validate access tokens that use a readable format (e.g JWT).
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet<string> set) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
when set.Contains(ResponseTypes.Code) => (true, false, false), => (true, false, false),
_ => (false, 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),
_ => (false, false, false)
};
return default; return default;
bool HasResponseType(string value) => context.ResponseType!.Split(Separators.Space).Contains(value);
} }
} }
@ -1475,8 +1532,6 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireAuthorizationCodeExtracted>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<AttachTokenRequestParameters>() .UseSingletonHandler<AttachTokenRequestParameters>()
.SetOrder(EvaluateValidatedBackchannelTokens.Descriptor.Order + 1_000) .SetOrder(EvaluateValidatedBackchannelTokens.Descriptor.Order + 1_000)
.Build(); .Build();
@ -1489,18 +1544,21 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); if (!context.ExtractBackchannelAccessToken &&
!context.ExtractBackchannelIdentityToken &&
// Attach a new request instance if none was created already. !context.ExtractRefreshToken)
{
return default;
}
// Attach a new request instance if necessary.
context.TokenRequest ??= new OpenIddictRequest(); context.TokenRequest ??= new OpenIddictRequest();
// Attach the grant type selected during the challenge phase. // Attach the selected grant type.
context.TokenRequest.GrantType = context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType) switch context.TokenRequest.GrantType = context.GrantType switch
{ {
null => throw new InvalidOperationException(SR.GetResourceString(SR.ID0294)), null => throw new InvalidOperationException(SR.GetResourceString(SR.ID0294)),
GrantTypes.AuthorizationCode => GrantTypes.AuthorizationCode,
// Note: in OpenID Connect, the hybrid flow doesn't have a dedicated grant_type and is // Note: in OpenID Connect, the hybrid flow doesn't have a dedicated grant_type and is
// typically treated as a combination of both the implicit and authorization code grants. // typically treated as a combination of both the implicit and authorization code grants.
// If the implicit flow was selected during the challenge phase and an authorization code // If the implicit flow was selected during the challenge phase and an authorization code
@ -1508,7 +1566,7 @@ public static partial class OpenIddictClientHandlers
// use grant_type=authorization_code when communicating with the remote token endpoint. // use grant_type=authorization_code when communicating with the remote token endpoint.
GrantTypes.Implicit => GrantTypes.AuthorizationCode, GrantTypes.Implicit => GrantTypes.AuthorizationCode,
// If the grant_type is not natively supported or recognized, try to send it as-is. // For other values, don't do any mapping.
string type => type string type => type
}; };
@ -1521,11 +1579,22 @@ public static partial class OpenIddictClientHandlers
// the redirect_uri from the state token principal and attach them to the request, if available. // the redirect_uri from the state token principal and attach them to the request, if available.
if (context.TokenRequest.GrantType is GrantTypes.AuthorizationCode) if (context.TokenRequest.GrantType is GrantTypes.AuthorizationCode)
{ {
Debug.Assert(!string.IsNullOrEmpty(context.AuthorizationCode), SR.GetResourceString(SR.ID4010));
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
context.TokenRequest.Code = context.AuthorizationCode; context.TokenRequest.Code = context.AuthorizationCode;
context.TokenRequest.CodeVerifier = context.StateTokenPrincipal.GetClaim(Claims.Private.CodeVerifier); context.TokenRequest.CodeVerifier = context.StateTokenPrincipal.GetClaim(Claims.Private.CodeVerifier);
context.TokenRequest.RedirectUri = context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri); context.TokenRequest.RedirectUri = context.StateTokenPrincipal.GetClaim(Claims.Private.RedirectUri);
} }
// If the token request uses a refresh token grant, attach the refresh token to the request.
else if (context.TokenRequest.GrantType is GrantTypes.RefreshToken)
{
Debug.Assert(!string.IsNullOrEmpty(context.RefreshToken), SR.GetResourceString(SR.ID4010));
context.TokenRequest.RefreshToken = context.RefreshToken;
}
return default; return default;
} }
} }
@ -1920,7 +1989,7 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.BackchannelIdentityTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Note: while an identity token typically contains a single audience represented // Note: while an identity token typically contains a single audience represented
// as a JSON string, multiple values can be returned represented a a JSON array. // as a JSON string, multiple values can be returned represented as a JSON array.
// //
// In any case, the client identifier of the application MUST be included in the audiences. // 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. // See https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information.
@ -1993,6 +2062,7 @@ public static partial class OpenIddictClientHandlers
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireBackchannelIdentityTokenPrincipal>() .AddFilter<RequireBackchannelIdentityTokenPrincipal>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<ValidateBackchannelIdentityTokenNonce>() .UseSingletonHandler<ValidateBackchannelIdentityTokenNonce>()
.SetOrder(ValidateBackchannelIdentityTokenPresenter.Descriptor.Order + 1_000) .SetOrder(ValidateBackchannelIdentityTokenPresenter.Descriptor.Order + 1_000)
.Build(); .Build();
@ -2027,7 +2097,7 @@ public static partial class OpenIddictClientHandlers
return default; return default;
// If the two nonces don't match, return an error. // If the two nonces don't match, return an error.
case { BackchannelIdentityTokenNonce: { } left, StateTokenNonce: { } right } when case { BackchannelIdentityTokenNonce: string left, StateTokenNonce: string right } when
#if SUPPORTS_TIME_CONSTANT_COMPARISONS #if SUPPORTS_TIME_CONSTANT_COMPARISONS
!CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)): !CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(left), Encoding.ASCII.GetBytes(right)):
#else #else
@ -2304,49 +2374,51 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.UseSingletonHandler<EvaluateValidatedUserinfoToken>() .UseSingletonHandler<EvaluateValidatedUserinfoToken>()
.SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000) .SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context) public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{ {
if (context is null) if (context is null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Resolve the grant grant and the response type stored in the state token and extract its individual elements. // Ensure the issuer resolved from the configuration matches the expected value.
var types = ( if (configuration.Issuer != context.Issuer)
GrantType: context.StateTokenPrincipal.GetClaim(Claims.Private.GrantType), {
ResponseTypes: context.StateTokenPrincipal.GetClaim(Claims.Private.ResponseType) throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) }
.ToImmutableHashSet());
(context.ExtractUserinfoToken, (context.ExtractUserinfoToken,
context.RequireUserinfoToken, context.RequireUserinfoToken,
context.ValidateUserinfoToken) = types switch context.ValidateUserinfoToken) = context.GrantType switch
{ {
// Information about the authenticated user can be retrieved from the userinfo // Information about the authenticated user can be retrieved from the userinfo endpoint
// endpoint when a backchannel access token is available. In this case, user data // when a frontchannel or backchannel access token is available. In this case, user data
// will be returned either as a JSON object or as a signed and/or encrypted // will be returned either as a JSON object or as a signed and/or encrypted
// JSON Web Token if the client registration indicates the client supports it. // JSON Web Token if the client registration indicates the client supports it.
// //
// By default, OpenIddict doesn't require that userinfo-as-JWT responses be used // By default, OpenIddict doesn't require that userinfo be used but userinfo tokens
// but userinfo tokens will be extracted and validated if they are available. // or responses will be extracted and validated if the userinfo endpoint and either
(GrantTypes.AuthorizationCode or GrantTypes.Implicit, ImmutableHashSet<string> set) // a frontchannel or backchannel access token was extracted and is available.
when context.StateTokenPrincipal.HasScope(Scopes.OpenId) && GrantTypes.AuthorizationCode or GrantTypes.Implicit or GrantTypes.RefreshToken
(set.Contains(ResponseTypes.Code) || set.Contains(ResponseTypes.Token)) when configuration.UserinfoEndpoint is not null && context switch
=> (true, false, true), {
{ ExtractBackchannelAccessToken: true, BackchannelAccessToken.Length: > 0 } => true,
{ ExtractFrontchannelAccessToken: true, FrontchannelAccessToken.Length: > 0 } => true,
_ => false
} => (true, false, true),
_ => (false, false, false) _ => (false, false, false)
}; };
return default;
} }
} }
@ -2360,54 +2432,36 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireUserinfoTokenExtracted>()
.UseSingletonHandler<AttachUserinfoRequestParameters>() .UseSingletonHandler<AttachUserinfoRequestParameters>()
.SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000) .SetOrder(EvaluateValidatedUserinfoToken.Descriptor.Order + 1_000)
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context) public ValueTask HandleAsync(ProcessAuthenticationContext context)
{ {
if (context is null) if (context is null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ?? // Attach a new request instance if necessary.
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); context.UserinfoRequest ??= new OpenIddictRequest();
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
var token = context switch // Attach the access token required to access the user information.
context.UserinfoRequest.AccessToken = context switch
{ {
// Note: the backchannel access token (retrieved from the token endpoint) is always preferred to // Note: the backchannel access token (retrieved from the token endpoint) is always preferred to
// the frontchannel access token if available, as it may grant a greater access to user's resources. // the frontchannel access token if available, as it may grant a greater access to user's resources.
{ ExtractBackchannelAccessToken: true, BackchannelAccessToken: { Length: > 0 } value } { ExtractBackchannelAccessToken: true, BackchannelAccessToken: { Length: > 0 } token } => token,
// If the userinfo endpoint is not available, skip the request.
when configuration.UserinfoEndpoint is not null => value,
// If the backchannel access token is not available, try to use the frontchannel access token. // If the backchannel access token is not available, try to use the frontchannel access token.
{ ExtractFrontchannelAccessToken: true, FrontchannelAccessToken: { Length: > 0 } value } { ExtractFrontchannelAccessToken: true, FrontchannelAccessToken: { Length: > 0 } token } => token,
// If the userinfo endpoint is not available, skip the request.
when configuration.UserinfoEndpoint is not null => value,
// Otherwise, skip the userinfo request. _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0162))
_ => null
}; };
if (string.IsNullOrEmpty(token)) return default;
{
return;
}
// Attach a new request instance if none was created already.
context.UserinfoRequest ??= new OpenIddictRequest();
// Attach the access token.
context.UserinfoRequest.AccessToken = token;
} }
} }
@ -2603,7 +2657,6 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireUserinfoTokenPrincipal>() .AddFilter<RequireUserinfoTokenPrincipal>()
.UseSingletonHandler<ValidateUserinfoTokenWellknownClaims>() .UseSingletonHandler<ValidateUserinfoTokenWellknownClaims>()
.SetOrder(ValidateUserinfoToken.Descriptor.Order + 1_000) .SetOrder(ValidateUserinfoToken.Descriptor.Order + 1_000)
@ -2611,21 +2664,29 @@ public static partial class OpenIddictClientHandlers
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context) public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{ {
if (context is null) if (context is null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
// The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints // The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints
// but must also support non-standard implementations, that are common with OAuth 2.0-only servers. // but must also support non-standard implementations, that are common with OAuth 2.0-only servers.
// //
// As such, protocol requirements are only enforced if an OpenID Connect request was initially sent. // As such, protocol requirements are only enforced if the server supports OpenID Connect.
if (context.StateTokenPrincipal.HasScope(Scopes.OpenId)) if (configuration.ScopesSupported.Contains(Scopes.OpenId))
{ {
foreach (var group in context.UserinfoTokenPrincipal.Claims foreach (var group in context.UserinfoTokenPrincipal.Claims
.GroupBy(claim => claim.Type) .GroupBy(claim => claim.Type)
@ -2641,12 +2702,10 @@ public static partial class OpenIddictClientHandlers
description: SR.FormatID2131(group.Key), description: SR.FormatID2131(group.Key),
uri: SR.FormatID8000(SR.ID2131)); uri: SR.FormatID8000(SR.ID2131));
return default; return;
} }
} }
return default;
static bool ValidateClaimGroup(KeyValuePair<string, List<Claim>> claims) => claims switch static bool ValidateClaimGroup(KeyValuePair<string, List<Claim>> claims) => claims switch
{ {
// The following JWT claims MUST be represented as unique strings. // The following JWT claims MUST be represented as unique strings.
@ -2671,7 +2730,6 @@ public static partial class OpenIddictClientHandlers
/// </summary> /// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; } public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireStateTokenPrincipal>()
.AddFilter<RequireUserinfoTokenPrincipal>() .AddFilter<RequireUserinfoTokenPrincipal>()
.UseSingletonHandler<ValidateUserinfoTokenSubject>() .UseSingletonHandler<ValidateUserinfoTokenSubject>()
.SetOrder(ValidateUserinfoTokenWellknownClaims.Descriptor.Order + 1_000) .SetOrder(ValidateUserinfoTokenWellknownClaims.Descriptor.Order + 1_000)
@ -2679,21 +2737,29 @@ public static partial class OpenIddictClientHandlers
.Build(); .Build();
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context) public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{ {
if (context is null) if (context is null)
{ {
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
var configuration = await context.Registration.ConfigurationManager.GetConfigurationAsync(default) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
// Ensure the issuer resolved from the configuration matches the expected value.
if (configuration.Issuer != context.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0307));
}
// The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints // The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints
// but must also support non-standard implementations, that are common with OAuth 2.0-only servers. // but must also support non-standard implementations, that are common with OAuth 2.0-only servers.
// //
// As such, protocol requirements are only enforced if an OpenID Connect request was initially sent. // As such, protocol requirements are only enforced if the server supports OpenID Connect.
if (context.StateTokenPrincipal.HasScope(Scopes.OpenId)) if (configuration.ScopesSupported.Contains(Scopes.OpenId))
{ {
// Standard OpenID Connect userinfo responses/tokens MUST contain a "sub" claim. For more // Standard OpenID Connect userinfo responses/tokens MUST contain a "sub" claim. For more
// information, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. // information, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse.
@ -2704,7 +2770,7 @@ public static partial class OpenIddictClientHandlers
description: SR.FormatID2132(Claims.Subject), description: SR.FormatID2132(Claims.Subject),
uri: SR.FormatID8000(SR.ID2132)); uri: SR.FormatID8000(SR.ID2132));
return default; return;
} }
// The "sub" claim returned as part of the userinfo response/token MUST exactly match the value // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value
@ -2719,7 +2785,7 @@ public static partial class OpenIddictClientHandlers
description: SR.FormatID2133(Claims.Subject), description: SR.FormatID2133(Claims.Subject),
uri: SR.FormatID8000(SR.ID2133)); uri: SR.FormatID8000(SR.ID2133));
return default; return;
} }
// The "sub" claim returned as part of the userinfo response/token MUST exactly match the value // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value
@ -2734,11 +2800,9 @@ public static partial class OpenIddictClientHandlers
description: SR.FormatID2133(Claims.Subject), description: SR.FormatID2133(Claims.Subject),
uri: SR.FormatID8000(SR.ID2133)); uri: SR.FormatID8000(SR.ID2133));
return default; return;
} }
} }
return default;
} }
} }
@ -2996,11 +3060,9 @@ public static partial class OpenIddictClientHandlers
context.ResponseType = ( context.ResponseType = (
context.GrantType, context.GrantType,
context.Registration.ResponseTypes.Select(types => context.Registration.ResponseTypes.Select(types =>
types.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) types.Split(Separators.Space).ToImmutableHashSet(StringComparer.Ordinal)).ToList(),
.ToImmutableHashSet(StringComparer.Ordinal)).ToList(),
configuration.ResponseTypesSupported.Select(types => configuration.ResponseTypesSupported.Select(types =>
types.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) types.Split(Separators.Space).ToImmutableHashSet(StringComparer.Ordinal)).ToList()) switch
.ToImmutableHashSet(StringComparer.Ordinal)).ToList()) switch
{ {
// Note: the OAuth 2.0 provider metadata and OpenID Connect discovery specification define // Note: the OAuth 2.0 provider metadata and OpenID Connect discovery specification define
// the supported response types as a required property. Nevertheless, to ensure OpenIddict // the supported response types as a required property. Nevertheless, to ensure OpenIddict
@ -3140,8 +3202,8 @@ public static partial class OpenIddictClientHandlers
client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token })) client.Any(static set => set.SetEquals(new[] { ResponseTypes.IdToken, ResponseTypes.Token }))
=> ResponseTypes.IdToken + ' ' + ResponseTypes.Token, => ResponseTypes.IdToken + ' ' + ResponseTypes.Token,
// Note: response_type=token is not considered considered secure enough as it allows // Note: response_type=token is not considered secure enough as it allows malicious
// malicious actors to inject access tokens that were issued to a different client. // actors to inject access tokens that were initially issued to a different client.
// As such, while OpenIddict-based servers allow using response_type=token for backward // As such, while OpenIddict-based servers allow using response_type=token for backward
// compatibility with legacy clients, OpenIddict-based clients are deliberately not // compatibility with legacy clients, OpenIddict-based clients are deliberately not
// allowed to negotiate the unsafe and OAuth 2.0-only response_type=token flow. // allowed to negotiate the unsafe and OAuth 2.0-only response_type=token flow.
@ -3210,8 +3272,7 @@ public static partial class OpenIddictClientHandlers
// can never be used with a response type containing id_token or token, as required by the OAuth 2.0 // can never be used with a response type containing id_token or token, as required by the OAuth 2.0
// multiple response types specification. To prevent invalid combinations from being sent to the // multiple response types specification. To prevent invalid combinations from being sent to the
// remote server, the response types are taken into account when selecting the best response mode. // remote server, the response types are taken into account when selecting the best response mode.
var types = context.ResponseType!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries) var types = context.ResponseType!.Split(Separators.Space).ToImmutableHashSet(StringComparer.Ordinal);
.ToImmutableHashSet(StringComparer.Ordinal);
context.ResponseMode = (context.Registration.ResponseModes, configuration.ResponseModesSupported) switch context.ResponseMode = (context.Registration.ResponseModes, configuration.ResponseModesSupported) switch
{ {
@ -3493,17 +3554,10 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// Some specific response_type/response_mode combinations are not allowed (e.g response_mode=query
// can never be used with a response type containing id_token or token, as required by the OAuth 2.0
// multiple response types specification. To prevent invalid combinations from being sent to the
// remote server, the response types are taken into account when selecting the best response mode.
var types = context.ResponseType!.Split(Separators.Space, StringSplitOptions.RemoveEmptyEntries)
.ToImmutableHashSet(StringComparer.Ordinal);
// Don't attach a code challenge method if no authorization code is requested as some implementations // Don't attach a code challenge method if no authorization code is requested as some implementations
// (like OpenIddict server) are known to eagerly block authorization requests that specify an invalid // (like OpenIddict server) are known to eagerly block authorization requests that specify an invalid
// code_challenge/code_challenge_method/response_type combination (e.g response_type=id_token). // code_challenge/code_challenge_method/response_type combination (e.g response_type=id_token).
if (!types.Contains(ResponseTypes.Code)) if (!context.ResponseType!.Split(Separators.Space).Contains(ResponseTypes.Code))
{ {
return; return;
} }

123
src/OpenIddict.Client/OpenIddictClientService.cs

@ -311,6 +311,129 @@ public class OpenIddictClientService
} }
} }
/// <summary>
/// Refreshes the user tokens using the specified refresh token.
/// </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)> RefreshTokensAsync(
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
GrantType = GrantTypes.RefreshToken,
Issuer = registration.Issuer,
RefreshToken = token,
Registration = registration
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new OpenIddictExceptions.GenericException(
SR.FormatID0163(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, CreatePrincipal(
context.BackchannelIdentityTokenPrincipal,
context.UserinfoTokenPrincipal));
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
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);
}
}
/// <summary> /// <summary>
/// Sends the token request and retrieves the corresponding response. /// Sends the token request and retrieves the corresponding response.
/// </summary> /// </summary>

Loading…
Cancel
Save