diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
index ec85e49c..b90d8b63 100644
--- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
+++ b/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;
diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx
index cb4780ed..f92f07c4 100644
--- a/src/OpenIddict.Abstractions/OpenIddictResources.resx
+++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx
@@ -1301,6 +1301,18 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
The '{0}' node cannot be extracted from the response.
+
+ The username cannot be null or empty.
+
+
+ The password cannot be null or empty.
+
+
+ A username must be specified when using the resource owner password credentials grant.
+
+
+ A password must be specified when using the resource owner password credentials grant.
+
The security token is missing.
@@ -1757,6 +1769,12 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
The issuer should be a valid absolute URL at this point.
+
+ The username shouldn't be null or empty at this point.
+
+
+ The password shouldn't be null or empty at this point.
+
An error occurred while validating the token '{Token}'.
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
index 8062e730..8ffc9fb1 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
@@ -133,6 +133,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
.UseSingletonHandler()
// 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
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
+ .AddFilter()
.UseSingletonHandler()
.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
};
diff --git a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs
index ae47c5b8..49570464 100644
--- a/src/OpenIddict.Client/OpenIddictClientConfiguration.cs
+++ b/src/OpenIddict.Client/OpenIddictClientConfiguration.cs
@@ -41,6 +41,16 @@ public class OpenIddictClientConfiguration : IPostConfigureOptions
public string? RefreshToken { get; set; }
+ ///
+ /// Gets or sets the username to send to the server, if applicable.
+ ///
+ public string? Username { get; set; }
+
+ ///
+ /// Gets or sets the password to send to the server, if applicable.
+ ///
+ public string? Password { get; set; }
+
///
/// Gets or sets the frontchannel state token to validate, if applicable.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs
index 9531ea5a..5425427a 100644
--- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs
+++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs
@@ -36,7 +36,6 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton();
// Register the built-in filters used by the default OpenIddict client event handlers.
- builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
@@ -45,6 +44,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
+ builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
builder.Services.TryAddSingleton();
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
index 9c3deb03..870ebbb4 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
@@ -11,23 +11,6 @@ namespace OpenIddict.Client;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientHandlerFilters
{
- ///
- /// Represents a filter that excludes the associated handlers if the challenge
- /// doesn't correspond to an authorization code or implicit grant operation.
- ///
- public class RequireAuthorizationCodeOrImplicitGrantType : IOpenIddictClientHandlerFilter
- {
- public ValueTask IsActiveAsync(ProcessChallengeContext context)
- {
- if (context is null)
- {
- throw new ArgumentNullException(nameof(context));
- }
-
- return new(context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit);
- }
- }
-
///
/// Represents a filter that excludes the associated handlers if no authorization code is validated.
///
@@ -156,6 +139,23 @@ public static class OpenIddictClientHandlerFilters
}
}
+ ///
+ /// Represents a filter that excludes the associated handlers if the challenge
+ /// doesn't correspond to an authorization code or implicit grant operation.
+ ///
+ public class RequireInteractiveGrantType : IOpenIddictClientHandlerFilter
+ {
+ public ValueTask IsActiveAsync(ProcessChallengeContext context)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ return new(context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit);
+ }
+ }
+
///
/// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token.
///
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
index 43c0f48b..cc556195 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
@@ -59,7 +59,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseScopedHandler()
.SetOrder(int.MaxValue - 100_000)
.Build();
@@ -104,7 +104,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseScopedHandler()
.SetOrder(PrepareAuthorizationRequest.Descriptor.Order + 1_000)
.Build();
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index ae9b6a0f..fab977d0 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/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()
.AddFilter()
+ .AddFilter()
.UseScopedHandler()
.SetOrder(ValidateUserinfoTokenSubject.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
@@ -3423,7 +3458,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(EvaluateGeneratedChallengeTokens.Descriptor.Order + 1_000)
.Build();
@@ -3614,7 +3649,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(AttachResponseType.Descriptor.Order + 1_000)
.Build();
@@ -3719,7 +3754,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(AttachResponseMode.Descriptor.Order + 1_000)
.Build();
@@ -3748,7 +3783,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(AttachClientId.Descriptor.Order + 1_000)
.Build();
@@ -3781,7 +3816,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(AttachRedirectUri.Descriptor.Order + 1_000)
.Build();
@@ -3826,7 +3861,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(AttachScopes.Descriptor.Order + 1_000)
.Build();
@@ -3864,7 +3899,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(AttachRequestForgeryProtection.Descriptor.Order + 1_000)
.Build();
@@ -3907,7 +3942,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(AttachNonce.Descriptor.Order + 1_000)
.Build();
@@ -4179,7 +4214,7 @@ public static partial class OpenIddictClientHandlers
///
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder()
- .AddFilter()
+ .AddFilter()
.UseSingletonHandler()
.SetOrder(GenerateStateToken.Descriptor.Order + 1_000)
.Build();
diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs
index fd3c0335..a9402ca7 100644
--- a/src/OpenIddict.Client/OpenIddictClientService.cs
+++ b/src/OpenIddict.Client/OpenIddictClientService.cs
@@ -332,23 +332,29 @@ public class OpenIddictClientService
}
///
- /// Refreshes the user tokens using the specified refresh token.
+ /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
///
/// The client registration.
- /// The refresh token to use.
+ /// 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)> 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();
}
}
+ }
+
+ ///
+ /// 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));
+ }
- 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();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ 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);
+ }
}