Browse Source

Implement OAuth 2.0 Token Exchange support

pull/2338/head
Kévin Chalet 7 months ago
parent
commit
8c94cb7c13
  1. 2
      .editorconfig
  2. 3
      README.md
  3. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
  4. 86
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs
  5. 3
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  6. 1
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  7. 162
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  8. 3
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  9. 22
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  10. 72
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  11. 56
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  12. 45
      src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs
  13. 9
      src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
  14. 4
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
  15. 35
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  16. 11
      src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs
  17. 16
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  18. 9
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  19. 101
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  20. 1
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  21. 17
      src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs
  22. 3
      src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs
  23. 346
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  24. 216
      src/OpenIddict.Client/OpenIddictClientModels.cs
  25. 2
      src/OpenIddict.Client/OpenIddictClientOptions.cs
  26. 205
      src/OpenIddict.Client/OpenIddictClientService.cs
  27. 8
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs
  28. 52
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  29. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs
  30. 12
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  31. 19
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  32. 44
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  33. 2
      src/OpenIddict.Server/OpenIddictServerEvents.Device.cs
  34. 50
      src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs
  35. 4
      src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs
  36. 4
      src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs
  37. 4
      src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs
  38. 138
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  39. 5
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  40. 51
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  41. 4
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  42. 485
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  43. 58
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  44. 30
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  45. 30
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  46. 761
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  47. 54
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  48. 6
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs
  49. 7
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs
  50. 6
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  51. 2
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs
  52. 8
      src/OpenIddict.Validation/OpenIddictValidationService.cs
  53. 4099
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs
  54. 3
      test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
  55. 48
      test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

2
.editorconfig

@ -10,7 +10,7 @@ tab_width = 4
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
csharp_new_line_before_catch = true

3
README.md

@ -16,7 +16,8 @@ OpenIddict aims at providing a **versatile solution** to implement **OpenID Conn
> to integrate with OpenIddict-based identity providers or any other OAuth 2.0/OpenID Connect-compliant implementation.
OpenIddict fully supports the **[code/implicit/hybrid flows](http://openid.net/specs/openid-connect-core-1_0.html)**,
the **[client credentials/resource owner password grants](https://tools.ietf.org/html/rfc6749)** and the [device authorization flow](https://tools.ietf.org/html/rfc8628).
the **[client credentials/resource owner password grants](https://datatracker.ietf.org/doc/html/rfc6749)**,
the [device authorization flow](https://datatracker.ietf.org/doc/html/rfc8628) and the **[token exchange grant](https://datatracker.ietf.org/doc/html/rfc8693)**.
OpenIddict natively supports **[Entity Framework Core](https://www.nuget.org/packages/OpenIddict.EntityFrameworkCore)**,
**[Entity Framework 6](https://www.nuget.org/packages/OpenIddict.EntityFramework)** and **[MongoDB](https://www.nuget.org/packages/OpenIddict.MongoDb)**

2
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs

@ -66,7 +66,7 @@ public class AuthorizationController : Controller
var result = await context.Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ApplicationCookie);
if (result is not { Identity: ClaimsIdentity } ||
((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
(request.MaxAge is not null && result.Properties?.IssuedUtc is not null &&
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
TempData["IgnoreAuthenticationChallenge"] is null or false))
{

86
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthorizationController.cs

@ -5,6 +5,7 @@
*/
using System.Security.Claims;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@ -77,7 +78,7 @@ public class AuthorizationController : Controller
var result = await HttpContext.AuthenticateAsync();
if (result is not { Succeeded: true } ||
((request.HasPromptValue(PromptValues.Login) || request.MaxAge is 0 ||
(request.MaxAge != null && result.Properties?.IssuedUtc != null &&
(request.MaxAge is not null && result.Properties?.IssuedUtc is not null &&
TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) &&
TempData["IgnoreAuthenticationChallenge"] is null or false))
{
@ -450,7 +451,7 @@ public class AuthorizationController : Controller
}
#endregion
#region Password, authorization code, device and refresh token flows
#region Password, authorization code, device, refresh token and token exchange flows
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
@ -560,6 +561,87 @@ public class AuthorizationController : Controller
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsTokenExchangeGrantType())
{
// Retrieve the claims principal stored in the subject token.
//
// Note: the principal may not represent a user (e.g if the token was issued during a client credentials token
// request and represents a client application): developers are strongly encouraged to ensure that the user
// and client identifiers are randomly generated so that a malicious client cannot impersonate a legit user.
//
// See https://datatracker.ietf.org/doc/html/rfc9068#SecurityConsiderations for more information.
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
// If available, retrieve the claims principal stored in the actor token.
var actor = result.Properties?.GetParameter<ClaimsPrincipal>(OpenIddictServerAspNetCoreConstants.Properties.ActorTokenPrincipal);
// Retrieve the user profile corresponding to the subject token.
var user = await _userManager.FindByIdAsync(result.Principal!.GetClaim(Claims.Subject)!);
if (user is null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
// Note: whether the identity represents a delegated or impersonated access (or any other
// model) is entirely up to the implementer: to support all scenarios, OpenIddict doesn't
// enforce any specific constraint on the identity used for the sign-in operation and only
// requires that the standard "act" and "may_act" claims be valid JSON objects if present.
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
// Add the claims that will be persisted in the issued token.
identity.SetClaim(Claims.Subject, await _userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await _userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await _userManager.GetUserNameAsync(user))
.SetClaim(Claims.PreferredUsername, await _userManager.GetUserNameAsync(user))
.SetClaims(Claims.Role, [.. await _userManager.GetRolesAsync(user)]);
// Note: IdentityModel doesn't support serializing ClaimsIdentity.Actor to the
// standard "act" claim yet, which requires adding the "act" claim manually.
//
// For more information, see
// https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3219.
if (!string.IsNullOrEmpty(actor?.GetClaim(Claims.Subject)) &&
!string.Equals(identity.GetClaim(Claims.Subject), actor.GetClaim(Claims.Subject), StringComparison.Ordinal))
{
identity.SetClaim(Claims.Actor, new JsonObject
{
[Claims.Subject] = actor.GetClaim(Claims.Subject)
});
}
// Note: in this sample, the granted scopes match the requested scope
// but you may want to allow the user to uncheck specific scopes.
// For that, simply restrict the list of scopes before calling SetScopes.
identity.SetScopes(request.GetScopes());
identity.SetResources(await _scopeManager.ListResourcesAsync(identity.GetScopes()).ToListAsync());
identity.SetDestinations(GetDestinations);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
#endregion

3
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs

@ -117,7 +117,8 @@ public class Startup
.AllowImplicitFlow()
.AllowNoneFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
.AllowRefreshTokenFlow()
.AllowTokenExchangeFlow();
// Register the public scopes that will be exposed by the configuration endpoint.
options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "demo_api");

1
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -69,6 +69,7 @@ public class Worker : IHostedService
Permissions.GrantTypes.Implicit,
Permissions.GrantTypes.Password,
Permissions.GrantTypes.RefreshToken,
Permissions.GrantTypes.TokenExchange,
Permissions.ResponseTypes.Code,
Permissions.ResponseTypes.CodeIdToken,
Permissions.ResponseTypes.CodeIdTokenToken,

162
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -300,6 +300,90 @@ public class InteractiveService : BackgroundService
AnsiConsole.MarkupLine("[green]Client credentials authentication successful.[/]");
}
else if (type is GrantTypes.TokenExchange)
{
var (identifier, subject, actor) = (
await GetRequestedTokenTypeAsync(stoppingToken),
await GetSubjectTokenAsync(stoppingToken),
await GetActorTokenAsync(stoppingToken));
AnsiConsole.MarkupLine("[cyan]Sending the token request.[/]");
// Ask OpenIddict to send the specified subject token (and actor token, if available).
var response = await _service.AuthenticateWithTokenExchangeAsync(new()
{
ActorToken = actor.Token,
ActorTokenType = actor.TokenType,
CancellationToken = stoppingToken,
ProviderName = provider,
RequestedTokenType = identifier,
SubjectToken = subject.Token,
SubjectTokenType = subject.TokenType
});
AnsiConsole.MarkupLine("[green]Token exchange authentication successful:[/]");
AnsiConsole.Write(CreateClaimTable(response.Principal));
// If introspection is supported by the server, ask the user if the issued token should be introspected.
if (configuration.IntrospectionEndpoint is not null &&
response.IssuedTokenType is TokenTypeIdentifiers.AccessToken &&
await IntrospectAccessTokenAsync(stoppingToken))
{
AnsiConsole.MarkupLine("[steelblue]Claims extracted from the token introspection response:[/]");
AnsiConsole.Write(CreateClaimTable((await _service.IntrospectTokenAsync(new()
{
CancellationToken = stoppingToken,
ProviderName = provider,
Token = response.IssuedToken,
TokenTypeHint = TokenTypeHints.AccessToken
})).Principal));
}
// If revocation is supported by the server, ask the user if the issued token should be revoked.
if (configuration.RevocationEndpoint is not null &&
response.IssuedTokenType is TokenTypeIdentifiers.AccessToken &&
await RevokeAccessTokenAsync(stoppingToken))
{
await _service.RevokeTokenAsync(new()
{
CancellationToken = stoppingToken,
ProviderName = provider,
Token = response.IssuedToken,
TokenTypeHint = response.IssuedTokenType is TokenTypeIdentifiers.AccessToken ?
TokenTypeHints.AccessToken : TokenTypeHints.RefreshToken
});
AnsiConsole.MarkupLine("[steelblue]Access token revoked.[/]");
}
// If a refresh token was returned by the authorization server, ask the user
// if the access token should be refreshed using the refresh_token grant.
if (response.IssuedTokenType is TokenTypeIdentifiers.RefreshToken &&
await RefreshTokenAsync(stoppingToken))
{
AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity (issued refresh token):[/]");
AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new()
{
CancellationToken = stoppingToken,
ProviderName = provider,
RefreshToken = response.IssuedToken
})).Principal));
}
// If a refresh token was returned by the authorization server, ask the user
// if the access token should be refreshed using the refresh_token grant.
if (!string.IsNullOrEmpty(response.RefreshToken) && await RefreshTokenAsync(stoppingToken))
{
AnsiConsole.MarkupLine("[steelblue]Claims extracted from the refreshed identity (classic refresh token):[/]");
AnsiConsole.Write(CreateClaimTable((await _service.AuthenticateWithRefreshTokenAsync(new()
{
CancellationToken = stoppingToken,
ProviderName = provider,
RefreshToken = response.RefreshToken
})).Principal));
}
}
}
}
@ -462,6 +546,12 @@ public class InteractiveService : BackgroundService
choices.Add((GrantTypes.Password, "Resource owner password credentials grant"));
}
if (configuration.GrantTypesSupported.Contains(GrantTypes.TokenExchange) &&
configuration.TokenEndpoint is not null)
{
choices.Add((GrantTypes.TokenExchange, "Token exchange"));
}
if (configuration.GrantTypesSupported.Contains(GrantTypes.ClientCredentials) &&
configuration.TokenEndpoint is not null)
{
@ -537,6 +627,78 @@ public class InteractiveService : BackgroundService
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken);
}
Task<string?> GetRequestedTokenTypeAsync(CancellationToken cancellationToken)
{
static string? Prompt() => AnsiConsole.Prompt(new SelectionPrompt<(string? TokenType, string DisplayName)>()
.Title("Select the type of the requested token (optional):")
.AddChoices<(string? TokenType, string DisplayName)>(
[
(null, "No value"),
(TokenTypeIdentifiers.AccessToken, "Access token"),
(TokenTypeIdentifiers.IdentityToken, "Identity token"),
(TokenTypeIdentifiers.RefreshToken, "Refresh token")
])
.UseConverter(choice => choice.DisplayName)).TokenType;
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken);
}
Task<(string TokenType, string Token)> GetSubjectTokenAsync(CancellationToken cancellationToken)
{
static (string TokenType, string Token) Prompt()
{
var type = AnsiConsole.Prompt(new SelectionPrompt<(string TokenType, string DisplayName)>()
.Title("Select the type of the subject type:")
.AddChoices<(string TokenType, string DisplayName)>(
[
(TokenTypeIdentifiers.AccessToken, "Access token"),
(TokenTypeIdentifiers.IdentityToken, "Identity token"),
(TokenTypeIdentifiers.RefreshToken, "Refresh token")
])
.UseConverter(choice => choice.DisplayName)).TokenType;
var token = AnsiConsole.Prompt(new TextPrompt<string>("Please enter the subject token:")
{
AllowEmpty = false,
IsSecret = false
});
return (type, token);
}
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken);
}
Task<(string? TokenType, string? Token)> GetActorTokenAsync(CancellationToken cancellationToken)
{
static (string? TokenType, string? Token) Prompt()
{
var type = AnsiConsole.Prompt(new SelectionPrompt<(string? TokenType, string DisplayName)>()
.Title("Select the type of the actor type (optional):")
.AddChoices<(string? TokenType, string DisplayName)>(
[
(null, "No actor token"),
(TokenTypeIdentifiers.AccessToken, "Access token"),
(TokenTypeIdentifiers.IdentityToken, "Identity token"),
(TokenTypeIdentifiers.RefreshToken, "Refresh token")
])
.UseConverter(choice => choice.DisplayName)).TokenType;
if (string.IsNullOrEmpty(type))
{
return (null, null);
}
return (type, AnsiConsole.Prompt(new TextPrompt<string>("Please enter the actor token:")
{
AllowEmpty = true,
IsSecret = false
}));
}
return WaitAsync(Task.Run(Prompt, cancellationToken), cancellationToken);
}
static Task<bool> IntrospectAccessTokenAsync(CancellationToken cancellationToken)
{
static bool Prompt() => AnsiConsole.Prompt(new ConfirmationPrompt(

3
sandbox/OpenIddict.Sandbox.Console.Client/Program.cs

@ -40,7 +40,8 @@ builder.Services.AddOpenIddict()
.AllowImplicitFlow()
.AllowNoneFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
.AllowRefreshTokenFlow()
.AllowTokenExchangeFlow();
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.

22
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -64,12 +64,14 @@ public static class OpenIddictConstants
{
public const string AccessTokenHash = "at_hash";
public const string Active = "active";
public const string Actor = "actor";
public const string Address = "address";
public const string Audience = "aud";
public const string AuthenticationContextReference = "acr";
public const string AuthenticationMethodReference = "amr";
public const string AuthenticationTime = "auth_time";
public const string AuthorizationServer = "as";
public const string AuthorizedActor = "may_act";
public const string AuthorizedParty = "azp";
public const string Birthdate = "birthdate";
public const string ClientId = "client_id";
@ -137,6 +139,7 @@ public static class OpenIddictConstants
public const string HostProperties = "oi_hst_props";
public const string IdentityTokenLifetime = "oi_idt_lft";
public const string InstanceId = "oi_instc_id";
public const string IssuedTokenLifetime = "oi_isst_lft";
public const string Issuer = "oi_iss";
public const string Nonce = "oi_nce";
public const string PostLogoutRedirectUri = "oi_pstlgt_reduri";
@ -209,6 +212,7 @@ public static class OpenIddictConstants
{
public const string AccessToken = "access_token";
public const string IdentityToken = "id_token";
public const string IssuedToken = "issued_token";
}
public static class Errors
@ -227,6 +231,7 @@ public static class OpenIddictConstants
public const string InvalidRequestObject = "invalid_request_object";
public const string InvalidRequestUri = "invalid_request_uri";
public const string InvalidScope = "invalid_scope";
public const string InvalidTarget = "invalid_target";
public const string InvalidToken = "invalid_token";
public const string LoginRequired = "login_required";
public const string MissingToken = "missing_token";
@ -250,6 +255,7 @@ public static class OpenIddictConstants
public const string Implicit = "implicit";
public const string Password = "password";
public const string RefreshToken = "refresh_token";
public const string TokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange";
}
public static class JsonWebTokenTypes
@ -331,8 +337,10 @@ public static class OpenIddictConstants
public static class Parameters
{
public const string AccessToken = "access_token";
public const string Active = "active";
public const string AcrValues = "acr_values";
public const string Active = "active";
public const string ActorToken = "actor_token";
public const string ActorTokenType = "actor_token_type";
public const string Assertion = "assertion";
public const string Audience = "audience";
public const string Claims = "claims";
@ -357,6 +365,7 @@ public static class OpenIddictConstants
public const string IdTokenHint = "id_token_hint";
public const string Interval = "interval";
public const string Iss = "iss";
public const string IssuedTokenType = "issued_token_type";
public const string LoginHint = "login_hint";
public const string Keys = "keys";
public const string MaxAge = "max_age";
@ -369,12 +378,15 @@ public static class OpenIddictConstants
public const string RefreshToken = "refresh_token";
public const string Registration = "registration";
public const string Request = "request";
public const string RequestedTokenType = "requested_token_type";
public const string RequestUri = "request_uri";
public const string Resource = "resource";
public const string ResponseMode = "response_mode";
public const string ResponseType = "response_type";
public const string Scope = "scope";
public const string State = "state";
public const string SubjectToken = "subject_token";
public const string SubjectTokenType = "subject_token_type";
public const string Token = "token";
public const string TokenType = "token_type";
public const string TokenTypeHint = "token_type_hint";
@ -406,6 +418,7 @@ public static class OpenIddictConstants
public const string Implicit = "gt:implicit";
public const string Password = "gt:password";
public const string RefreshToken = "gt:refresh_token";
public const string TokenExchange = "gt:urn:ietf:params:oauth:grant-type:token-exchange";
}
public static class Prefixes
@ -543,6 +556,7 @@ public static class OpenIddictConstants
public const string AuthorizationCode = "tkn_lft:auc";
public const string DeviceCode = "tkn_lft:dvc";
public const string IdentityToken = "tkn_lft:idt";
public const string IssuedToken = "tkn_lft:isst";
public const string RefreshToken = "tkn_lft:reft";
public const string RequestToken = "tkn_lft:reqt";
public const string UserCode = "tkn_lft:usrc";
@ -599,6 +613,12 @@ public static class OpenIddictConstants
public const string IdentityToken = "urn:ietf:params:oauth:token-type:id_token";
public const string RefreshToken = "urn:ietf:params:oauth:token-type:refresh_token";
public static class Prefixes
{
public const string Ietf = "urn:ietf:params:oauth:token-type:";
public const string OpenIddict = "urn:openiddict:params:oauth:token-type:";
}
public static class Private
{
public const string AuthorizationCode = "urn:openiddict:params:oauth:token-type:authorization_code";

72
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1745,6 +1745,54 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID0479" xml:space="preserve">
<value>The specified Stripe Connect account type is not valid. Supported values are 'Standard' and 'Express'.</value>
</data>
<data name="ID0480" xml:space="preserve">
<value>A subject token must be specified when using the token exchange grant.</value>
</data>
<data name="ID0481" xml:space="preserve">
<value>A subject token type must be specified when using the token exchange grant.</value>
</data>
<data name="ID0482" xml:space="preserve">
<value>An actor token type must be specified when using an actor token with the token exchange grant.</value>
</data>
<data name="ID0483" xml:space="preserve">
<value>An actor token must be specified when using an actor token type with the token exchange grant.</value>
</data>
<data name="ID0484" xml:space="preserve">
<value>An access token cannot be included in the response when using the token exchange grant.</value>
</data>
<data name="ID0485" xml:space="preserve">
<value>An error occurred while performing a token exchange.
Error: {0}
Error description: {1}
Error URI: {2}</value>
</data>
<data name="ID0486" xml:space="preserve">
<value>The list of allowed subject token types cannot be empty when enabling the token exchange grant.</value>
</data>
<data name="ID0487" xml:space="preserve">
<value>An incompatible token type was configured as an allowed subject token type.</value>
</data>
<data name="ID0488" xml:space="preserve">
<value>An incompatible token type was configured as an allowed actor token type.</value>
</data>
<data name="ID0489" xml:space="preserve">
<value>An incompatible token type was configured as an allowed requested token type.</value>
</data>
<data name="ID0490" xml:space="preserve">
<value>A default requested token type must be set in the server options.</value>
</data>
<data name="ID0491" xml:space="preserve">
<value>An incompatible token type was configured as the default requested token type.</value>
</data>
<data name="ID0492" xml:space="preserve">
<value>The default requested token type must be present in the list of allowed requested token types.</value>
</data>
<data name="ID0493" xml:space="preserve">
<value>The type of the subject token cannot be resolved from the authentication context.</value>
</data>
<data name="ID0494" xml:space="preserve">
<value>The type of the actor token cannot be resolved from the authentication context.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>
@ -2300,6 +2348,18 @@ Alternatively, any value respecting the '[region]-[subregion]-[identifier]' patt
<data name="ID2185" xml:space="preserve">
<value>The specified token doesn't contain any valid presenter, which may indicate that it was issued to a different client.</value>
</data>
<data name="ID2186" xml:space="preserve">
<value>The specified subject token cannot be used without sending a client identifier.</value>
</data>
<data name="ID2187" xml:space="preserve">
<value>The specified subject token cannot be used by this client application.</value>
</data>
<data name="ID2188" xml:space="preserve">
<value>The specified actor token cannot be used without sending a client identifier.</value>
</data>
<data name="ID2189" xml:space="preserve">
<value>The specified actor token cannot be used by this client application.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -3065,6 +3125,18 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6267" xml:space="preserve">
<value>The token was rejected because it had no valid audience.</value>
</data>
<data name="ID6268" xml:space="preserve">
<value>The token request was rejected because the client identifier of the application was not available and could not be compared to the audiences and presenters lists stored in the subject token.</value>
</data>
<data name="ID6269" xml:space="preserve">
<value>The token request was rejected because the subject token was issued to a different client or for another resource server.</value>
</data>
<data name="ID6270" xml:space="preserve">
<value>The token request was rejected because the client identifier of the application was not available and could not be compared to the audiences and presenters lists stored in the actor token.</value>
</data>
<data name="ID6271" xml:space="preserve">
<value>The token request was rejected because the actor token was issued to a different client or for another resource server.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

56
src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs

@ -265,7 +265,7 @@ public static class OpenIddictExtensions
}
// Return true if the response_type parameter contains "id_token" or "token".
return (flags & /* id_token: */ 0x01) == 0x01 || (flags & /* token: */ 0x02) == 0x02;
return (flags & /* id_token: */ 0x01) is 0x01 || (flags & /* token: */ 0x02) is 0x02;
}
/// <summary>
@ -324,13 +324,13 @@ public static class OpenIddictExtensions
}
// Return false if the response_type parameter doesn't contain "code".
if ((flags & /* code: */ 0x01) != 0x01)
if ((flags & /* code: */ 0x01) is not 0x01)
{
return false;
}
// Return true if the response_type parameter contains "id_token" or "token".
return (flags & /* id_token: */ 0x02) == 0x02 || (flags & /* token: */ 0x04) == 0x04;
return (flags & /* id_token: */ 0x02) is 0x02 || (flags & /* token: */ 0x04) is 0x04;
}
/// <summary>
@ -497,6 +497,22 @@ public static class OpenIddictExtensions
return string.Equals(request.GrantType, GrantTypes.RefreshToken, StringComparison.Ordinal);
}
/// <summary>
/// Determines whether the "grant_type" parameter corresponds to the token exchange grant.
/// See https://datatracker.ietf.org/doc/html/rfc8693#section-2.1 for more information.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
/// <returns><see langword="true"/> if the request is a token exchange grant request, <see langword="false"/> otherwise.</returns>
public static bool IsTokenExchangeGrantType(this OpenIddictRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
return string.Equals(request.GrantType, GrantTypes.TokenExchange, StringComparison.Ordinal);
}
/// <summary>
/// Gets the destinations associated with a claim.
/// </summary>
@ -2955,6 +2971,22 @@ public static class OpenIddictExtensions
public static TimeSpan? GetIdentityTokenLifetime(this ClaimsPrincipal principal)
=> GetLifetime(principal, Claims.Private.IdentityTokenLifetime);
/// <summary>
/// Gets the issued token lifetime associated with the claims identity.
/// </summary>
/// <param name="identity">The claims identity.</param>
/// <returns>The issued token lifetime or <see langword="null"/> if the claim cannot be found.</returns>
public static TimeSpan? GetIssuedTokenLifetime(this ClaimsIdentity identity)
=> GetLifetime(identity, Claims.Private.IssuedTokenLifetime);
/// <summary>
/// Gets the issued token lifetime associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns>The issued token lifetime or <see langword="null"/> if the claim cannot be found.</returns>
public static TimeSpan? GetIssuedTokenLifetime(this ClaimsPrincipal principal)
=> GetLifetime(principal, Claims.Private.IssuedTokenLifetime);
/// <summary>
/// Gets the request token lifetime associated with the claims identity.
/// </summary>
@ -3649,6 +3681,24 @@ public static class OpenIddictExtensions
public static ClaimsPrincipal SetIdentityTokenLifetime(this ClaimsPrincipal principal, TimeSpan? lifetime)
=> principal.SetClaim(Claims.Private.IdentityTokenLifetime, (long?) lifetime?.TotalSeconds);
/// <summary>
/// Sets the issued token lifetime associated with the claims identity.
/// </summary>
/// <param name="identity">The claims identity.</param>
/// <param name="lifetime">The issued token lifetime to store.</param>
/// <returns>The claims identity.</returns>
public static ClaimsIdentity SetIssuedTokenLifetime(this ClaimsIdentity identity, TimeSpan? lifetime)
=> identity.SetClaim(Claims.Private.IssuedTokenLifetime, (long?) lifetime?.TotalSeconds);
/// <summary>
/// Sets the issued token lifetime associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <param name="lifetime">The issued token lifetime to store.</param>
/// <returns>The claims principal.</returns>
public static ClaimsPrincipal SetIssuedTokenLifetime(this ClaimsPrincipal principal, TimeSpan? lifetime)
=> principal.SetClaim(Claims.Private.IssuedTokenLifetime, (long?) lifetime?.TotalSeconds);
/// <summary>
/// Sets the refresh token lifetime associated with the claims identity.
/// </summary>

45
src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs

@ -123,6 +123,24 @@ public class OpenIddictRequest : OpenIddictMessage
set => SetParameter(OpenIddictConstants.Parameters.AcrValues, value);
}
/// <summary>
/// Gets or sets the "actor_token" parameter.
/// </summary>
public string? ActorToken
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.ActorToken);
set => SetParameter(OpenIddictConstants.Parameters.ActorToken, value);
}
/// <summary>
/// Gets or sets the "actor_token_type" parameter.
/// </summary>
public string? ActorTokenType
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.ActorTokenType);
set => SetParameter(OpenIddictConstants.Parameters.ActorTokenType, value);
}
/// <summary>
/// Gets or sets the "assertion" parameter.
/// </summary>
@ -368,6 +386,15 @@ public class OpenIddictRequest : OpenIddictMessage
set => SetParameter(OpenIddictConstants.Parameters.Request, value);
}
/// <summary>
/// Gets or sets the "requested_token_type" parameter.
/// </summary>
public string? RequestedTokenType
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.RequestedTokenType);
set => SetParameter(OpenIddictConstants.Parameters.RequestedTokenType, value);
}
/// <summary>
/// Gets or sets the "request_uri" parameter.
/// </summary>
@ -422,6 +449,24 @@ public class OpenIddictRequest : OpenIddictMessage
set => SetParameter(OpenIddictConstants.Parameters.State, value);
}
/// <summary>
/// Gets or sets the "subject_token" parameter.
/// </summary>
public string? SubjectToken
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.SubjectToken);
set => SetParameter(OpenIddictConstants.Parameters.SubjectToken, value);
}
/// <summary>
/// Gets or sets the "subject_token_type" parameter.
/// </summary>
public string? SubjectTokenType
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.SubjectTokenType);
set => SetParameter(OpenIddictConstants.Parameters.SubjectTokenType, value);
}
/// <summary>
/// Gets or sets the "token" parameter.
/// </summary>

9
src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs

@ -185,6 +185,15 @@ public class OpenIddictResponse : OpenIddictMessage
set => SetParameter(OpenIddictConstants.Parameters.Iss, value);
}
/// <summary>
/// Gets or sets the "issued_token_type" parameter.
/// </summary>
public string? IssuedTokenType
{
get => (string?) GetParameter(OpenIddictConstants.Parameters.IssuedTokenType);
set => SetParameter(OpenIddictConstants.Parameters.IssuedTokenType, value);
}
/// <summary>
/// Gets or sets the "refresh_token" parameter.
/// </summary>

4
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs

@ -24,6 +24,7 @@ public static class OpenIddictClientAspNetCoreConstants
public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_identity_token_principal";
public const string GrantType = ".grant_type";
public const string IdentityTokenHint = ".identity_token_hint";
public const string IssuedTokenPrincipal = ".issued_token_principal";
public const string Issuer = ".issuer";
public const string LoginHint = ".login_hint";
public const string ProviderName = ".provider_name";
@ -45,6 +46,9 @@ public static class OpenIddictClientAspNetCoreConstants
public const string FrontchannelAccessToken = "frontchannel_access_token";
public const string FrontchannelAccessTokenExpirationDate = "frontchannel_access_token_expiration_date";
public const string FrontchannelIdentityToken = "frontchannel_id_token";
public const string IssuedToken = "issued_token";
public const string IssuedTokenExpirationDate = "issued_token_expiration_date";
public const string IssuedTokenType = "issued_token_type";
public const string RefreshToken = "refresh_token";
public const string StateToken = "state_token";
public const string UserInfoToken = "userinfo_token";

35
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs

@ -268,6 +268,36 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
});
}
if (!string.IsNullOrEmpty(context.IssuedToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.IssuedToken,
Value = context.IssuedToken
});
}
if (context.IssuedTokenExpirationDate is not null)
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.IssuedTokenExpirationDate,
Value = context.IssuedTokenExpirationDate.Value.ToString("o", CultureInfo.InvariantCulture)
});
}
if (!string.IsNullOrEmpty(context.IssuedTokenType))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.IssuedTokenType,
Value = context.IssuedTokenType
});
}
if (!string.IsNullOrEmpty(context.RefreshToken))
{
tokens ??= new(capacity: 1);
@ -328,6 +358,11 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
properties.SetParameter(Properties.FrontchannelIdentityTokenPrincipal, context.FrontchannelIdentityTokenPrincipal);
}
if (context.IssuedTokenPrincipal is not null)
{
properties.SetParameter(Properties.IssuedTokenPrincipal, context.IssuedTokenPrincipal);
}
if (context.RefreshTokenPrincipal is not null)
{
properties.SetParameter(Properties.RefreshTokenPrincipal, context.RefreshTokenPrincipal);

11
src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs

@ -22,27 +22,19 @@ public static class OpenIddictClientOwinConstants
public static class Properties
{
public const string AuthorizationCodePrincipal = ".authorization_code_principal";
public const string BackchannelAccessTokenPrincipal = ".backchannel_access_token_principal";
public const string BackchannelIdentityTokenPrincipal = ".backchannel_identity_token_principal";
public const string CodeChallengeMethod = ".code_challenge_method";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
public const string FrontchannelAccessTokenPrincipal = ".frontchannel_access_token_principal";
public const string FrontchannelIdentityTokenPrincipal = ".frontchannel_identity_token_principal";
public const string GrantType = ".grant_type";
public const string IdentityTokenHint = ".identity_token_hint";
public const string Issuer = ".issuer";
public const string LoginHint = ".login_hint";
public const string ProviderName = ".provider_name";
public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string RegistrationId = ".registration_id";
public const string ResponseMode = ".response_mode";
public const string ResponseType = ".response_type";
public const string Scope = ".scope";
public const string StateTokenPrincipal = ".state_token_principal";
public const string UserInfoTokenPrincipal = ".userinfo_token_principal";
}
public static class PropertyTypes
@ -62,6 +54,9 @@ public static class OpenIddictClientOwinConstants
public const string FrontchannelAccessToken = "frontchannel_access_token";
public const string FrontchannelAccessTokenExpirationDate = "frontchannel_access_token_expiration_date";
public const string FrontchannelIdentityToken = "frontchannel_id_token";
public const string IssuedToken = "issued_token";
public const string IssuedTokenExpirationDate = "issued_token_expiration_date";
public const string IssuedTokenType = "issued_token_type";
public const string RefreshToken = "refresh_token";
public const string StateToken = "state_token";
public const string UserInfoToken = "userinfo_token";

16
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs

@ -230,6 +230,22 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
properties.Dictionary[Tokens.FrontchannelIdentityToken] = context.FrontchannelIdentityToken;
}
if (!string.IsNullOrEmpty(context.IssuedToken))
{
properties.Dictionary[Tokens.IssuedToken] = context.IssuedToken;
}
if (context.IssuedTokenExpirationDate is not null)
{
properties.Dictionary[Tokens.IssuedTokenExpirationDate] =
context.IssuedTokenExpirationDate.Value.ToString("o", CultureInfo.InvariantCulture);
}
if (!string.IsNullOrEmpty(context.IssuedTokenType))
{
properties.Dictionary[Tokens.IssuedTokenType] = context.IssuedTokenType;
}
if (!string.IsNullOrEmpty(context.RefreshToken))
{
properties.Dictionary[Tokens.RefreshToken] = context.RefreshToken;

9
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -1121,6 +1121,15 @@ public sealed class OpenIddictClientBuilder
public OpenIddictClientBuilder AllowRefreshTokenFlow()
=> Configure(options => options.GrantTypes.Add(GrantTypes.RefreshToken));
/// <summary>
/// Enables token exchange flow support. For more information about this
/// specific OAuth 2.0 flow, visit https://datatracker.ietf.org/doc/html/rfc8693.
/// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientBuilder AllowTokenExchangeFlow()
=> Configure(options => options.GrantTypes.Add(GrantTypes.TokenExchange));
/// <summary>
/// Sets the relative or absolute URIs associated to the post-logout redirection endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.

101
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -371,6 +371,16 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? RequestForgeryProtection { get; set; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server, if applicable.
/// </summary>
public HashSet<string> Audiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the resources that will be sent to the authorization server, if applicable.
/// </summary>
public HashSet<string> Resources { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the scopes that will be sent to the authorization server, if applicable.
/// </summary>
@ -453,6 +463,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool ExtractFrontchannelIdentityToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an issued
/// token should be extracted from the current context.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ExtractIssuedToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a refresh
/// token should be extracted from the current context.
@ -525,6 +544,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool RequireFrontchannelIdentityToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an issued token
/// must be resolved for the authentication to be considered valid.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RequireIssuedToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a refresh token
/// must be resolved for the authentication to be considered valid.
@ -597,6 +625,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool ValidateFrontchannelIdentityToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the issued token
/// extracted from the current context should be validated.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ValidateIssuedToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the refresh token
/// extracted from the current context should be validated.
@ -669,6 +706,15 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool RejectFrontchannelIdentityToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid issued token
/// will cause the authentication demand to be rejected or will be ignored.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RejectIssuedToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid refresh token
/// will cause the authentication demand to be rejected or will be ignored.
@ -696,6 +742,16 @@ public static partial class OpenIddictClientEvents
/// </remarks>
public bool RejectUserInfoToken { get; set; }
/// <summary>
/// Gets or sets the actor token to send to the server, if applicable.
/// </summary>
public string? ActorToken { get; set; }
/// <summary>
/// Gets or sets the type of the actor token, if applicable.
/// </summary>
public string? ActorTokenType { get; set; }
/// <summary>
/// Gets or sets the authorization code to validate, if applicable.
/// </summary>
@ -736,6 +792,21 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? FrontchannelIdentityToken { get; set; }
/// <summary>
/// Gets or sets the issued token to validate, if applicable.
/// </summary>
public string? IssuedToken { get; set; }
/// <summary>
/// Gets or sets the expiration date of the issued token, if applicable.
/// </summary>
public DateTimeOffset? IssuedTokenExpirationDate { get; set; }
/// <summary>
/// Gets or sets the type of the issued token, if applicable.
/// </summary>
public string? IssuedTokenType { get; set; }
/// <summary>
/// Gets or sets the refresh token to validate, if applicable.
/// </summary>
@ -751,11 +822,26 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Gets or sets the type of the requested token to send to the server, if applicable.
/// </summary>
public string? RequestedTokenType { get; set; }
/// <summary>
/// Gets or sets the frontchannel state token to validate, if applicable.
/// </summary>
public string? StateToken { get; set; }
/// <summary>
/// Gets or sets the subject token to send to the server, if applicable.
/// </summary>
public string? SubjectToken { get; set; }
/// <summary>
/// Gets or sets the type of the subject token, if applicable.
/// </summary>
public string? SubjectTokenType { get; set; }
/// <summary>
/// Gets or sets the userinfo token to validate, if applicable.
/// </summary>
@ -786,6 +872,11 @@ public static partial class OpenIddictClientEvents
/// </summary>
public ClaimsPrincipal? FrontchannelIdentityTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the issued token, if applicable.
/// </summary>
public ClaimsPrincipal? IssuedTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the merged principal containing the claims of the other principals.
/// </summary>
@ -1036,6 +1127,16 @@ public static partial class OpenIddictClientEvents
/// </summary>
public string? LoginHint { get; set; }
/// <summary>
/// Gets the set of audiences that will be requested to the authorization server.
/// </summary>
public HashSet<string> Audiences { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the set of resources that will be requested to the authorization server.
/// </summary>
public HashSet<string> Resources { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the set of scopes that will be requested to the authorization server.
/// </summary>

1
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -52,6 +52,7 @@ public static class OpenIddictClientExtensions
builder.Services.TryAddSingleton<RequireInteractiveGrantType>();
builder.Services.TryAddSingleton<RequireIntrospectionClientAssertionGenerated>();
builder.Services.TryAddSingleton<RequireIntrospectionRequest>();
builder.Services.TryAddSingleton<RequireIssuedTokenValidated>();
builder.Services.TryAddSingleton<RequireLoginStateTokenGenerated>();
builder.Services.TryAddSingleton<RequireLogoutStateTokenGenerated>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();

17
src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs

@ -303,6 +303,23 @@ public static class OpenIddictClientHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no issued token is validated.
/// </summary>
public sealed class RequireIssuedTokenValidated : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.ValidateIssuedToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the selected token format is not JSON Web Token.
/// </summary>

3
src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs

@ -75,7 +75,8 @@ public static partial class OpenIddictClientHandlers
=> ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following parameters MUST be formatted as unique strings:
Parameters.AccessToken or Parameters.IdToken or Parameters.RefreshToken
Parameters.AccessToken or Parameters.IdToken or
Parameters.IssuedTokenType or Parameters.RefreshToken
=> ((JsonElement) value).ValueKind is JsonValueKind.String,
// The following parameters MUST be formatted as numeric dates:

346
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -86,6 +86,7 @@ public static partial class OpenIddictClientHandlers
ValidateBackchannelTokenDigests.Descriptor,
ValidateBackchannelAccessToken.Descriptor,
ValidateIssuedToken.Descriptor,
ValidateRefreshToken.Descriptor,
EvaluateUserInfoRequest.Descriptor,
@ -345,26 +346,26 @@ public static partial class OpenIddictClientHandlers
break;
case OpenIddictClientEndpointType.Unknown when !string.IsNullOrEmpty(context.Nonce):
break;
case OpenIddictClientEndpointType.Unknown:
if (string.IsNullOrEmpty(context.Nonce))
if (string.IsNullOrEmpty(context.GrantType))
{
if (string.IsNullOrEmpty(context.GrantType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
}
if (!context.Options.GrantTypes.Contains(context.GrantType))
{
throw new InvalidOperationException(SR.FormatID0359(context.GrantType));
}
if (!context.Options.GrantTypes.Contains(context.GrantType))
{
throw new InvalidOperationException(SR.FormatID0359(context.GrantType));
}
if (context.GrantType is GrantTypes.DeviceCode && string.IsNullOrEmpty(context.DeviceCode))
{
switch (context.GrantType)
{
case GrantTypes.DeviceCode when string.IsNullOrEmpty(context.DeviceCode):
throw new InvalidOperationException(SR.GetResourceString(SR.ID0396));
}
if (context.GrantType is GrantTypes.Password)
{
case GrantTypes.Password:
if (string.IsNullOrEmpty(context.Username))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0337));
@ -374,21 +375,43 @@ public static partial class OpenIddictClientHandlers
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0338));
}
}
if (context.GrantType is GrantTypes.RefreshToken && string.IsNullOrEmpty(context.RefreshToken))
{
break;
case GrantTypes.RefreshToken when string.IsNullOrEmpty(context.RefreshToken):
throw new InvalidOperationException(SR.GetResourceString(SR.ID0311));
}
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
throw context.Options.Registrations.Count is 0 ?
new InvalidOperationException(SR.GetResourceString(SR.ID0304)) :
new InvalidOperationException(SR.GetResourceString(SR.ID0355));
}
case GrantTypes.TokenExchange:
if (string.IsNullOrEmpty(context.SubjectToken))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0480));
}
if (string.IsNullOrEmpty(context.SubjectTokenType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0481));
}
if (!string.IsNullOrEmpty(context.ActorToken) && string.IsNullOrEmpty(context.ActorTokenType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0482));
}
if (string.IsNullOrEmpty(context.ActorToken) && !string.IsNullOrEmpty(context.ActorTokenType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0483));
}
break;
}
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
throw context.Options.Registrations.Count is 0 ?
new InvalidOperationException(SR.GetResourceString(SR.ID0304)) :
new InvalidOperationException(SR.GetResourceString(SR.ID0355));
}
break;
@ -2295,15 +2318,16 @@ public static partial class OpenIddictClientHandlers
// standard grant type associated), never send a token request.
null when context.ResponseType is ResponseTypes.None => false,
// For client credentials, device authorization, resource owner password
// credentials and refresh token requests, always send a token request.
GrantTypes.ClientCredentials or GrantTypes.DeviceCode or
GrantTypes.Password or GrantTypes.RefreshToken => true,
// For the non-interactive grant types, always send a token request.
GrantTypes.ClientCredentials or GrantTypes.DeviceCode or
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange => true,
// By default, always send a token request for custom grant types.
not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.DeviceCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken) => true,
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange) => true,
_ => false
};
@ -2454,6 +2478,7 @@ public static partial class OpenIddictClientHandlers
// 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.
//
// If the implicit flow was selected during the challenge phase and an authorization code
// was returned, this very likely means that the hybrid flow was used. In this case,
// use grant_type=authorization_code when communicating with the remote token endpoint.
@ -2463,13 +2488,25 @@ public static partial class OpenIddictClientHandlers
string value => value
};
if (context.Scopes.Count is > 0 &&
context.TokenRequest.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.DeviceCode))
if (context.TokenRequest.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.DeviceCode))
{
// Note: the final OAuth 2.0 specification requires using a space as the scope separator.
// Clients that need to deal with older or non-compliant implementations can register
// a custom handler to use a different separator (typically, a comma).
context.TokenRequest.Scope = string.Join(" ", context.Scopes);
if (context.Audiences.Count is > 0)
{
context.TokenRequest.Audiences = [.. context.Audiences];
}
if (context.Resources.Count is > 0)
{
context.TokenRequest.Resources = [.. context.Resources];
}
if (context.Scopes.Count is > 0)
{
// Note: the final OAuth 2.0 specification requires using a space as the scope separator.
// Clients that need to deal with older or non-compliant implementations can register
// a custom handler to use a different separator (typically, a comma).
context.TokenRequest.Scope = string.Join(" ", context.Scopes);
}
}
// If the token request uses an authorization code grant, retrieve the code_verifier and
@ -2510,6 +2547,21 @@ public static partial class OpenIddictClientHandlers
context.TokenRequest.RefreshToken = context.RefreshToken;
}
// If the token request uses a token exchange grant, attach the
// subject token (and actor token, if available) to the request.
else if (context.TokenRequest.GrantType is GrantTypes.TokenExchange)
{
Debug.Assert(!string.IsNullOrEmpty(context.SubjectToken), SR.GetResourceString(SR.ID4010));
context.TokenRequest.RequestedTokenType = context.RequestedTokenType;
context.TokenRequest.SubjectToken = context.SubjectToken;
context.TokenRequest.SubjectTokenType = context.SubjectTokenType;
context.TokenRequest.ActorToken = context.ActorToken;
context.TokenRequest.ActorTokenType = context.ActorTokenType;
}
return default;
}
}
@ -2852,11 +2904,18 @@ public static partial class OpenIddictClientHandlers
GrantTypes.Password or GrantTypes.RefreshToken
=> (true, true, false, false),
// While the OAuth 2.0 token exchange flow always uses the "access_token" parameter
// for all types of tokens, the returned token may not be an access token. To reduce
// ambiguities, OpenIddict uses the standard "access_token" parameter for this flow
// but uses a different name (issued token) to represent the returned token.
GrantTypes.TokenExchange => (false, false, false, false),
// By default, always extract and require a backchannel
// access token for custom grant types, but don't validate it.
not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.DeviceCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken)
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange)
=> (true, true, false, false),
_ => (false, false, false, false)
@ -2878,13 +2937,14 @@ public static partial class OpenIddictClientHandlers
context.StateTokenPrincipal is ClaimsPrincipal principal &&
principal.HasScope(Scopes.OpenId) => (true, true, true, true),
// The client credentials, device code and resource owner password credentials grants
// don't have an equivalent in OpenID Connect so an identity token is typically never
// returned when using them. 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.ClientCredentials or GrantTypes.DeviceCode or GrantTypes.Password
// The client credentials, device code, resource owner password credentials and token
// exchange grants don't have an equivalent in OpenID Connect so an identity token is
// typically never returned when using them. 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.ClientCredentials or GrantTypes.DeviceCode or
GrantTypes.Password or GrantTypes.TokenExchange
=> (true, false, true, false),
// An identity token may or may not be returned as part of refresh token responses
@ -2897,12 +2957,24 @@ public static partial class OpenIddictClientHandlers
// types and validate it when present, but don't require that one be returned.
not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.DeviceCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken)
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange)
=> (true, false, true, false),
_ => (false, false, false, false)
};
(context.ExtractIssuedToken,
context.RequireIssuedToken,
context.ValidateIssuedToken,
context.RejectIssuedToken) = context.GrantType switch
{
// An issued token is always returned as part of a token exchange response.
GrantTypes.TokenExchange => (true, true, false, false),
_ => (false, false, false, false)
};
(context.ExtractRefreshToken,
context.RequireRefreshToken,
context.ValidateRefreshToken,
@ -2922,19 +2994,21 @@ public static partial class OpenIddictClientHandlers
types.Contains(ResponseTypes.Code)
=> (true, false, false, false),
// A refresh token may or may not be returned as part of client credentials,
// device code, resource owner password credentials and refresh token responses
// A refresh token may or may not be returned as part of client credentials, device code,
// resource owner password credentials, refresh token and token exchange responses
// depending on the policy adopted by the remote authorization server. As such,
// a refresh token is never considered required for such token responses.
GrantTypes.ClientCredentials or GrantTypes.DeviceCode or
GrantTypes.Password or GrantTypes.RefreshToken
GrantTypes.ClientCredentials or GrantTypes.DeviceCode or
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange
=> (true, false, false, false),
// By default, always try to extract a refresh token for
// custom grant types, but don't require or validate it.
not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.DeviceCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken)
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange)
=> (true, false, false, false),
_ => (false, false, false, false)
@ -2969,20 +3043,36 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
context.BackchannelAccessToken = context.ExtractBackchannelAccessToken ?
context.TokenResponse.AccessToken : null;
context.BackchannelAccessTokenExpirationDate =
context.ExtractBackchannelAccessToken &&
context.TokenResponse.ExpiresIn is long value
if (context.ExtractBackchannelAccessToken)
{
context.BackchannelAccessToken = context.TokenResponse.AccessToken;
context.BackchannelAccessTokenExpirationDate = context.TokenResponse.ExpiresIn is long value
? context.Options.TimeProvider.GetUtcNow().AddSeconds(value)
: null;
}
context.BackchannelIdentityToken = context.ExtractBackchannelIdentityToken ?
context.TokenResponse.IdToken : null;
if (context.ExtractBackchannelIdentityToken)
{
context.BackchannelIdentityToken = context.TokenResponse.IdToken;
}
context.RefreshToken = context.ExtractRefreshToken ?
context.TokenResponse.RefreshToken : null;
// Note: the OAuth 2.0 token exchange specification uses the "access_token" parameter
// to convey the issued token, even when the issued token is not an access token.
//
// See https://datatracker.ietf.org/doc/html/rfc8693#section-2.2.1 for more information.
if (context.ExtractIssuedToken)
{
context.IssuedToken = context.TokenResponse.AccessToken;
context.IssuedTokenExpirationDate = context.TokenResponse.ExpiresIn is long value
? context.Options.TimeProvider.GetUtcNow().AddSeconds(value)
: null;
context.IssuedTokenType = context.TokenResponse.IssuedTokenType ?? TokenTypeIdentifiers.AccessToken;
}
if (context.ExtractRefreshToken)
{
context.RefreshToken = context.TokenResponse.RefreshToken;
}
return default;
}
@ -3016,6 +3106,7 @@ public static partial class OpenIddictClientHandlers
if ((context.RequireBackchannelAccessToken && string.IsNullOrEmpty(context.BackchannelAccessToken)) ||
(context.RequireBackchannelIdentityToken && string.IsNullOrEmpty(context.BackchannelIdentityToken)) ||
(context.RequireIssuedToken && string.IsNullOrEmpty(context.IssuedToken)) ||
(context.RequireRefreshToken && string.IsNullOrEmpty(context.RefreshToken)))
{
context.Reject(
@ -3589,6 +3680,78 @@ public static partial class OpenIddictClientHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the issued token resolved from the context.
/// </summary>
public sealed class ValidateIssuedToken : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
public ValidateIssuedToken(IOpenIddictClientDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireIssuedTokenValidated>()
.UseScopedHandler<ValidateIssuedToken>()
.SetOrder(ValidateBackchannelAccessToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.IssuedToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
Token = context.IssuedToken,
ValidTokenTypes = { context.IssuedTokenType! }
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
if (context.RejectIssuedToken)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
return;
}
context.IssuedTokenPrincipal = notification.Principal;
}
}
/// <summary>
/// Contains the logic responsible for validating the refresh token resolved from the context.
/// Note: this handler is typically not used for standard-compliant implementations as refresh tokens
@ -3608,7 +3771,7 @@ public static partial class OpenIddictClientHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRefreshTokenValidated>()
.UseScopedHandler<ValidateRefreshToken>()
.SetOrder(ValidateBackchannelAccessToken.Descriptor.Order + 1_000)
.SetOrder(ValidateIssuedToken.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
@ -3703,10 +3866,17 @@ public static partial class OpenIddictClientHandlers
(!string.IsNullOrEmpty(context.BackchannelAccessToken) ||
!string.IsNullOrEmpty(context.FrontchannelAccessToken)) => true,
// For the OAuth 2.0 token exchange grant, only send a userinfo request by default if the issued
// token is an access token, unless userinfo retrieval was explicitly disabled by the user.
GrantTypes.TokenExchange when context.Configuration.UserInfoEndpoint is not null &&
context.IssuedTokenType is TokenTypeIdentifiers.AccessToken &&
!context.DisableUserInfoRetrieval && !string.IsNullOrEmpty(context.IssuedToken) => true,
// Apply the same logic for custom grant types.
not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.DeviceCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken)
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange)
when context.Configuration.UserInfoEndpoint is not null && !context.DisableUserInfoRetrieval &&
(!string.IsNullOrEmpty(context.BackchannelAccessToken) ||
!string.IsNullOrEmpty(context.FrontchannelAccessToken)) => true,
@ -3735,10 +3905,16 @@ public static partial class OpenIddictClientHandlers
// responses if the openid scope was explicitly added by the user to the list of requested scopes.
GrantTypes.RefreshToken => !context.Scopes.Contains(Scopes.OpenId),
// Note: when using the OAuth 2.0 token exchange flow, it is not possible to determine whether the
// issued token will allow retrieving a standard userinfo response. In this case, only validate userinfo
// responses if the openid scope was explicitly added by the user to the list of requested scopes.
GrantTypes.TokenExchange => !context.Scopes.Contains(Scopes.OpenId),
// For unknown grant types, disable userinfo validation unless the openid scope was explicitly added.
not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.DeviceCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken)
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange)
=> !context.Scopes.Contains(Scopes.OpenId),
_ => true
@ -3855,10 +4031,23 @@ public static partial class OpenIddictClientHandlers
// Attach a new request instance if necessary.
context.UserInfoRequest ??= new OpenIddictRequest();
// 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.
context.UserInfoRequest.AccessToken = context.BackchannelAccessToken ?? context.FrontchannelAccessToken ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0162));
context.UserInfoRequest.AccessToken ??= context.GrantType switch
{
// Note: for interactive flows, when both a frontchannel token and a backchannel token are available,
// 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.
GrantTypes.AuthorizationCode or GrantTypes.Implicit
=> context.BackchannelAccessToken ?? context.FrontchannelAccessToken ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0162)),
// For the OAuth 2.0 token exchange flow, use the issued token as the access token,
// but only if the "issued_token_type" node indicates it's an access token.
GrantTypes.TokenExchange when context.IssuedTokenType is TokenTypeIdentifiers.AccessToken
=> context.IssuedToken ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0162)),
// Otherwise, always use the backchannel access token.
_ => context.BackchannelAccessToken ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0162))
};
return default;
}
@ -3957,10 +4146,11 @@ public static partial class OpenIddictClientHandlers
// By default, OpenIddict doesn't require that userinfo tokens be used even for
// user flows but they are extracted and validated when a userinfo request was sent.
GrantTypes.AuthorizationCode or GrantTypes.Implicit or
GrantTypes.DeviceCode or GrantTypes.Password or GrantTypes.RefreshToken
GrantTypes.DeviceCode or GrantTypes.Password or
GrantTypes.RefreshToken or GrantTypes.TokenExchange
when context.SendUserInfoRequest => (true, false, true, true),
// UserInfo tokens are typically not used with the client credentials grant,
// Userinfo tokens are typically not used with the client credentials grant,
// but they are extracted and validated when a userinfo request was sent.
GrantTypes.ClientCredentials when context.SendUserInfoRequest
=> (true, false, true, true),
@ -3969,7 +4159,8 @@ public static partial class OpenIddictClientHandlers
// but extract and validate them when a userinfo request was sent.
not null and not (GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.DeviceCode or GrantTypes.Implicit or
GrantTypes.Password or GrantTypes.RefreshToken)
GrantTypes.Password or GrantTypes.RefreshToken or
GrantTypes.TokenExchange)
when context.SendUserInfoRequest => (true, false, true, true),
_ => (false, false, false, false),
@ -4241,8 +4432,7 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// Create a composite principal containing claims resolved from the frontchannel
// and backchannel identity tokens and the userinfo token principal, if available.
// Create a composite principal containing claims resolved from the token principals that were extracted.
context.MergedPrincipal = CreateMergedPrincipal(
context.FrontchannelIdentityTokenPrincipal,
context.BackchannelIdentityTokenPrincipal,
@ -5505,6 +5695,16 @@ public static partial class OpenIddictClientHandlers
context.Request.ResponseType = context.ResponseType;
context.Request.ResponseMode = context.ResponseMode;
if (context.Audiences.Count is > 0)
{
context.Request.Audiences = [.. context.Audiences];
}
if (context.Resources.Count is > 0)
{
context.Request.Resources = [.. context.Resources];
}
if (context.Scopes.Count is > 0)
{
// Note: the final OAuth 2.0 specification requires using a space as the scope separator.

216
src/OpenIddict.Client/OpenIddictClientModels.cs

@ -137,6 +137,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalAuthorizationRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -217,6 +222,11 @@ public static class OpenIddictClientModels
[EditorBrowsable(EditorBrowsableState.Advanced)]
public string? ResponseType { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -258,6 +268,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalEndSessionRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -294,6 +309,11 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -335,6 +355,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -360,6 +385,11 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -462,6 +492,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -497,6 +532,11 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -581,6 +621,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -629,6 +674,11 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -714,6 +764,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalDeviceAuthorizationRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -739,6 +794,11 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -891,6 +951,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -926,6 +991,11 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -1015,6 +1085,11 @@ public static class OpenIddictClientModels
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
@ -1046,6 +1121,11 @@ public static class OpenIddictClientModels
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
@ -1125,6 +1205,142 @@ public static class OpenIddictClientModels
public required ClaimsPrincipal? UserInfoTokenPrincipal { get; init; }
}
/// <summary>
/// Represents a token exchange authentication request.
/// </summary>
public sealed record class TokenExchangeAuthenticationRequest
{
/// <summary>
/// Gets or sets the actor token that will be sent to the authorization server, if applicable.
/// </summary>
public string? ActorToken { get; init; }
/// <summary>
/// Gets or sets the type of the actor token, if applicable.
/// </summary>
public string? ActorTokenType { get; init; }
/// <summary>
/// Gets the audiences that will be sent to the authorization server.
/// </summary>
public List<string>? Audiences { get; init; }
/// <summary>
/// Gets or sets the parameters that will be added to the token request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets a boolean indicating whether userinfo should be disabled, which may be
/// required when receiving an access token that cannot be used with the userinfo endpoint.
/// </summary>
/// <remarks>
/// Note: by default, a userinfo request is only sent when an access token is returned by the server.
/// </remarks>
public bool DisableUserInfo { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets or sets the type of the requested token, if applicable.
/// </summary>
public string? RequestedTokenType { get; init; }
/// <summary>
/// Gets the resources that will be sent to the authorization server.
/// </summary>
public List<string>? Resources { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the subject token that will be sent to the authorization server.
/// </summary>
public required string SubjectToken { get; init; }
/// <summary>
/// Gets or sets the type of the subject token.
/// </summary>
public required string SubjectTokenType { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents a token exchange authentication result.
/// </summary>
public sealed record class TokenExchangeAuthenticationResult
{
/// <summary>
/// Gets or sets the issued token.
/// </summary>
public required string IssuedToken { get; init; }
/// <summary>
/// Gets or sets the expiration date of the issued token, if available.
/// </summary>
public required DateTimeOffset? IssuedTokenExpirationDate { get; init; }
/// <summary>
/// Gets or sets the type of the issued token.
/// </summary>
public required string IssuedTokenType { get; init; }
/// <summary>
/// Gets or sets a merged principal containing all the claims
/// extracted from the identity token and userinfo token principals.
/// </summary>
public required ClaimsPrincipal Principal { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
/// <summary>
/// Gets or sets the refresh token, if available.
/// </summary>
public required string? RefreshToken { get; init; }
/// <summary>
/// Gets or sets the token response.
/// </summary>
public required OpenIddictResponse TokenResponse { get; init; }
}
/// <summary>
/// Represents an revocation request.
/// </summary>

2
src/OpenIddict.Client/OpenIddictClientOptions.cs

@ -27,7 +27,7 @@ public sealed class OpenIddictClientOptions
/// Note: the list is automatically sorted based on the order assigned to each handler descriptor.
/// As such, it MUST NOT be mutated after options initialization to preserve the exact order.
/// </summary>
public List<OpenIddictClientHandlerDescriptor> Handlers { get; } = new(DefaultHandlers);
public List<OpenIddictClientHandlerDescriptor> Handlers { get; } = [.. DefaultHandlers];
/// <summary>
/// Gets the list of encryption credentials used by the OpenIddict client services.

205
src/OpenIddict.Client/OpenIddictClientService.cs

@ -280,7 +280,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -353,7 +353,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -379,6 +379,16 @@ public class OpenIddictClientService
ResponseType = request.ResponseType
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
@ -429,7 +439,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -445,9 +455,19 @@ public class OpenIddictClientService
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new(),
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
@ -512,7 +532,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -532,6 +552,16 @@ public class OpenIddictClientService
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
@ -596,7 +626,7 @@ public class OpenIddictClientService
try
{
// 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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -616,9 +646,19 @@ public class OpenIddictClientService
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new(),
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
@ -694,7 +734,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -716,6 +756,16 @@ public class OpenIddictClientService
Request = new()
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
@ -767,7 +817,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -790,6 +840,16 @@ public class OpenIddictClientService
Username = request.Username
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
@ -830,6 +890,95 @@ public class OpenIddictClientService
};
}
/// <summary>
/// Authenticates using token exchange.
/// </summary>
/// <param name="request">The token exchange authentication request.</param>
/// <returns>The token exchange authentication result.</returns>
public async ValueTask<TokenExchangeAuthenticationResult> AuthenticateWithTokenExchangeAsync(
TokenExchangeAuthenticationRequest request)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
request.CancellationToken.ThrowIfCancellationRequested();
// Note: this service is registered as a singleton service. As such, it cannot
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync();
var context = new ProcessAuthenticationContext(transaction)
{
ActorToken = request.ActorToken,
ActorTokenType = request.ActorTokenType,
CancellationToken = request.CancellationToken,
DisableUserInfoRetrieval = request.DisableUserInfo,
DisableUserInfoValidation = request.DisableUserInfo,
GrantType = GrantTypes.TokenExchange,
Issuer = request.Issuer,
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
RequestedTokenType = request.RequestedTokenType,
SubjectToken = request.SubjectToken,
SubjectTokenType = request.SubjectTokenType,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
}
if (request.Properties is { Count: > 0 })
{
foreach (var property in request.Properties)
{
context.Properties[property.Key] = property.Value;
}
}
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
throw new ProtocolException(
SR.FormatID0485(context.Error, context.ErrorDescription, context.ErrorUri),
context.Error, context.ErrorDescription, context.ErrorUri);
}
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
return new()
{
IssuedToken = context.IssuedToken!,
IssuedTokenExpirationDate = context.IssuedTokenExpirationDate,
IssuedTokenType = context.IssuedTokenType!,
Principal = context.MergedPrincipal,
Properties = context.Properties,
RefreshToken = context.RefreshToken,
TokenResponse = context.TokenResponse
};
}
/// <summary>
/// Authenticates using the specified refresh token.
/// </summary>
@ -846,7 +995,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -865,9 +1014,19 @@ public class OpenIddictClientService
RefreshToken = request.RefreshToken,
RegistrationId = request.RegistrationId,
TokenRequest = request.AdditionalTokenRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new(),
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
if (request.Audiences is { Count: > 0 })
{
context.Audiences.UnionWith(request.Audiences);
}
if (request.Resources is { Count: > 0 })
{
context.Resources.UnionWith(request.Resources);
}
if (request.Scopes is { Count: > 0 })
{
context.Scopes.UnionWith(request.Scopes);
@ -923,7 +1082,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -986,7 +1145,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -1061,7 +1220,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -1188,7 +1347,7 @@ public class OpenIddictClientService
request.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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -1207,7 +1366,7 @@ public class OpenIddictClientService
ProviderName = request.ProviderName,
RegistrationId = request.RegistrationId,
Request = request.AdditionalEndSessionRequestParameters
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new(),
is Dictionary<string, OpenIddictParameter> parameters ? new(parameters) : new()
};
if (request.Properties is { Count: > 0 })
@ -1267,7 +1426,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -1422,7 +1581,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -1575,7 +1734,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -1735,7 +1894,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -1888,7 +2047,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -2046,7 +2205,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -2199,7 +2358,7 @@ public class OpenIddictClientService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();

8
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreConstants.cs

@ -14,6 +14,7 @@ public static class OpenIddictServerAspNetCoreConstants
public static class Properties
{
public const string AccessTokenPrincipal = ".access_token_principal";
public const string ActorTokenPrincipal = ".actor_token_principal";
public const string ClientAssertionPrincipal = ".client_assertion_principal";
public const string AuthorizationCodePrincipal = ".authorization_code_principal";
public const string DeviceCodePrincipal = ".device_code_principal";
@ -24,18 +25,23 @@ public static class OpenIddictServerAspNetCoreConstants
public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string RequestTokenPrincipal = ".request_token_principal";
public const string Scope = ".scope";
public const string SubjectTokenPrincipal = ".subject_token_principal";
public const string UserCodePrincipal = ".user_code_principal";
}
public static class Tokens
{
public const string AccessToken = "access_token";
public const string ActorToken = "actor_token";
public const string ActorTokenType = "actor_token_type";
public const string AuthorizationCode = "authorization_code";
public const string ClientAssertion = "client_assertion";
public const string DeviceCode = "device_code";
public const string IdentityToken = "id_token";
public const string RequestToken = "request_token";
public const string RefreshToken = "refresh_token";
public const string RequestToken = "request_token";
public const string SubjectToken = "subject_token";
public const string SubjectTokenType = "subject_token_type";
public const string UserCode = "user_code";
}
}

52
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs

@ -186,6 +186,8 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
=> context.DeviceCodePrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> context.RefreshTokenPrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType()
=> context.SubjectTokenPrincipal,
OpenIddictServerEndpointType.UserInfo => context.AccessTokenPrincipal,
@ -227,6 +229,26 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
});
}
if (!string.IsNullOrEmpty(context.ActorToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.ActorToken,
Value = context.ActorToken
});
}
if (!string.IsNullOrEmpty(context.ActorTokenType))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.ActorTokenType,
Value = context.ActorTokenType
});
}
if (!string.IsNullOrEmpty(context.AuthorizationCode))
{
tokens ??= new(capacity: 1);
@ -287,6 +309,26 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
});
}
if (!string.IsNullOrEmpty(context.SubjectToken))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.SubjectToken,
Value = context.SubjectToken
});
}
if (!string.IsNullOrEmpty(context.SubjectTokenType))
{
tokens ??= new(capacity: 1);
tokens.Add(new AuthenticationToken
{
Name = Tokens.SubjectTokenType,
Value = context.SubjectTokenType
});
}
if (!string.IsNullOrEmpty(context.UserCode))
{
tokens ??= new(capacity: 1);
@ -302,6 +344,11 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
properties.SetParameter(Properties.AccessTokenPrincipal, context.AccessTokenPrincipal);
}
if (context.ActorTokenPrincipal is not null)
{
properties.SetParameter(Properties.ActorTokenPrincipal, context.ActorTokenPrincipal);
}
if (context.AuthorizationCodePrincipal is not null)
{
properties.SetParameter(Properties.AuthorizationCodePrincipal, context.AuthorizationCodePrincipal);
@ -332,6 +379,11 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Op
properties.SetParameter(Properties.RequestTokenPrincipal, context.RequestTokenPrincipal);
}
if (context.SubjectTokenPrincipal is not null)
{
properties.SetParameter(Properties.SubjectTokenPrincipal, context.SubjectTokenPrincipal);
}
if (context.UserCodePrincipal is not null)
{
properties.SetParameter(Properties.UserCodePrincipal, context.UserCodePrincipal);

8
src/OpenIddict.Server.Owin/OpenIddictServerOwinConstants.cs

@ -24,16 +24,10 @@ public static class OpenIddictServerOwinConstants
public static class Properties
{
public const string AccessTokenPrincipal = ".access_token_principal";
public const string AuthorizationCodePrincipal = ".authorization_code_principal";
public const string DeviceCodePrincipal = ".device_code_principal";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";
public const string IdentityTokenPrincipal = ".identity_token_principal";
public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string Scope = ".scope";
public const string UserCodePrincipal = ".user_code_principal";
}
public static class PropertyTypes
@ -47,12 +41,14 @@ public static class OpenIddictServerOwinConstants
public static class Tokens
{
public const string AccessToken = "access_token";
public const string ActorToken = "actor_token";
public const string AuthorizationCode = "authorization_code";
public const string ClientAssertion = "client_assertion";
public const string DeviceCode = "device_code";
public const string IdentityToken = "id_token";
public const string RefreshToken = "refresh_token";
public const string RequestToken = "request_token";
public const string SubjectToken = "subject_token";
public const string UserCode = "user_code";
}
}

12
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs

@ -179,6 +179,8 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
=> context.DeviceCodePrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> context.RefreshTokenPrincipal,
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType()
=> context.SubjectTokenPrincipal,
OpenIddictServerEndpointType.UserInfo => context.AccessTokenPrincipal,
@ -211,6 +213,11 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
properties.Dictionary[Tokens.AccessToken] = context.AccessToken;
}
if (!string.IsNullOrEmpty(context.ActorToken))
{
properties.Dictionary[Tokens.ActorToken] = context.ActorToken;
}
if (!string.IsNullOrEmpty(context.AuthorizationCode))
{
properties.Dictionary[Tokens.AuthorizationCode] = context.AuthorizationCode;
@ -241,6 +248,11 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
properties.Dictionary[Tokens.RequestToken] = context.RequestToken;
}
if (!string.IsNullOrEmpty(context.SubjectToken))
{
properties.Dictionary[Tokens.SubjectToken] = context.SubjectToken;
}
if (!string.IsNullOrEmpty(context.UserCode))
{
properties.Dictionary[Tokens.UserCode] = context.UserCode;

19
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1089,6 +1089,15 @@ public sealed class OpenIddictServerBuilder
options.Scopes.Add(Scopes.OfflineAccess);
});
/// <summary>
/// Enables token exchange flow support. For more information about this
/// specific OAuth 2.0 flow, visit https://datatracker.ietf.org/doc/html/rfc8693.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictServerBuilder AllowTokenExchangeFlow()
=> Configure(options => options.GrantTypes.Add(GrantTypes.TokenExchange));
/// <summary>
/// Sets the relative or absolute URIs associated to the authorization endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
@ -1851,6 +1860,16 @@ public sealed class OpenIddictServerBuilder
public OpenIddictServerBuilder SetIdentityTokenLifetime(TimeSpan? lifetime)
=> Configure(options => options.IdentityTokenLifetime = lifetime);
/// <summary>
/// Sets the issued token lifetime, which is used as a fallback value when the
/// issued token type isn't natively supported by OpenIddict (e.g an access token).
/// While discouraged, <see langword="null"/> can be specified to issue tokens that never expire.
/// </summary>
/// <param name="lifetime">The issued token lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/> instance.</returns>
public OpenIddictServerBuilder SetIssuedTokenLifetime(TimeSpan? lifetime)
=> Configure(options => options.IssuedTokenLifetime = lifetime);
/// <summary>
/// Sets the refresh token lifetime, after which client applications must get
/// a new authorization from the user. When sliding expiration is enabled,

44
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -193,6 +193,50 @@ public sealed class OpenIddictServerConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0367));
}
// Ensure at least one subject token type is configured when the token exchange grant is enabled.
if (options.SubjectTokenTypes.Count is 0 && options.GrantTypes.Contains(GrantTypes.TokenExchange))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0486));
}
// Prevent internal token types from being used as subject, actor or requested token types.
if (options.SubjectTokenTypes.Any(static type => type.StartsWith(
TokenTypeIdentifiers.Prefixes.OpenIddict, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0487));
}
if (options.ActorTokenTypes.Any(static type => type.StartsWith(
TokenTypeIdentifiers.Prefixes.OpenIddict, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0488));
}
if (options.RequestedTokenTypes.Any(static type => type.StartsWith(
TokenTypeIdentifiers.Prefixes.OpenIddict, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0489));
}
// Ensure that the default requested token type (used when the caller doesn't specify a
// requested_token_type parameter during an OAuth 2.0 token exchange flow) was configured
// and that the configured value is also present in the list of allowed token types.
if (string.IsNullOrEmpty(options.DefaultRequestedTokenType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0490));
}
if (options.DefaultRequestedTokenType.StartsWith(
TokenTypeIdentifiers.Prefixes.OpenIddict, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0491));
}
if (!options.RequestedTokenTypes.Contains(options.DefaultRequestedTokenType))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0492));
}
if (options.EncryptionCredentials.Count is 0)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0085));

2
src/OpenIddict.Server/OpenIddictServerEvents.Device.cs

@ -194,7 +194,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the security principal extracted from the user code, if applicable.
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
public ClaimsPrincipal? UserCodePrincipal { get; set; }
}
/// <summary>

50
src/OpenIddict.Server/OpenIddictServerEvents.Exchange.cs

@ -58,10 +58,29 @@ public static partial class OpenIddictServerEvents
}
/// <summary>
/// Gets or sets the security principal extracted from the authorization
/// code or the refresh token, if applicable to the current token request.
/// Gets or sets the security principal extracted from the actor token, if applicable.
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
public ClaimsPrincipal? ActorTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the authorization code, if applicable.
/// </summary>
public ClaimsPrincipal? AuthorizationCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the device code, if applicable.
/// </summary>
public ClaimsPrincipal? DeviceCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the refresh token, if applicable.
/// </summary>
public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the subject token, if applicable.
/// </summary>
public ClaimsPrincipal? SubjectTokenPrincipal { get; set; }
}
/// <summary>
@ -87,6 +106,31 @@ public static partial class OpenIddictServerEvents
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the security principal extracted from the actor token, if applicable.
/// </summary>
public ClaimsPrincipal? ActorTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the authorization code, if applicable.
/// </summary>
public ClaimsPrincipal? AuthorizationCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the device code, if applicable.
/// </summary>
public ClaimsPrincipal? DeviceCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the refresh token, if applicable.
/// </summary>
public ClaimsPrincipal? RefreshTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the security principal extracted from the subject token, if applicable.
/// </summary>
public ClaimsPrincipal? SubjectTokenPrincipal { get; set; }
/// <summary>
/// Gets the additional parameters returned to the client application.
/// </summary>

4
src/OpenIddict.Server/OpenIddictServerEvents.Introspection.cs

@ -66,7 +66,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the security principal extracted from the introspected token, if available.
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
public ClaimsPrincipal? GenericTokenPrincipal { get; set; }
}
/// <summary>
@ -95,7 +95,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the security principal extracted from the introspected token.
/// </summary>
public ClaimsPrincipal Principal { get; set; } = default!;
public ClaimsPrincipal GenericTokenPrincipal { get; set; } = default!;
/// <summary>
/// Gets the additional claims returned to the client application.

4
src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs

@ -66,7 +66,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the security principal extracted from the revoked token, if available.
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
public ClaimsPrincipal? GenericTokenPrincipal { get; set; }
}
/// <summary>
@ -95,7 +95,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the security principal extracted from the revoked token.
/// </summary>
public ClaimsPrincipal Principal { get; set; } = default!;
public ClaimsPrincipal GenericTokenPrincipal { get; set; } = default!;
}
/// <summary>

4
src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs

@ -61,7 +61,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the security principal extracted from the access token, if available.
/// </summary>
public ClaimsPrincipal? Principal { get; set; }
public ClaimsPrincipal? AccessTokenPrincipal { get; set; }
}
/// <summary>
@ -90,7 +90,7 @@ public static partial class OpenIddictServerEvents
/// <summary>
/// Gets or sets the security principal extracted from the access token.
/// </summary>
public ClaimsPrincipal Principal { get; set; } = default!;
public ClaimsPrincipal AccessTokenPrincipal { get; set; } = default!;
/// <summary>
/// Gets the additional claims returned to the client application.

138
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -327,6 +327,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ExtractAccessToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an actor
/// token should be extracted from the current context.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ExtractActorToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an authorization
/// code should be extracted from the current context.
@ -390,6 +399,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ExtractRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a subject
/// token should be extracted from the current context.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ExtractSubjectToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user
/// code should be extracted from the current context.
@ -408,6 +426,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RequireAccessToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an actor token
/// must be resolved for the authentication to be considered valid.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RequireActorToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an authorization code
/// must be resolved for the authentication to be considered valid.
@ -471,6 +498,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RequireRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a subject token
/// must be resolved for the authentication to be considered valid.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RequireSubjectToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user code
/// must be resolved for the authentication to be considered valid.
@ -489,6 +525,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ValidateAccessToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the actor
/// token extracted from the current request should be validated.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ValidateActorToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the authorization
/// code extracted from the current request should be validated.
@ -552,6 +597,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool ValidateRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the subject
/// token extracted from the current request should be validated.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool ValidateSubjectToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the user
/// code extracted from the current request should be validated.
@ -570,6 +624,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RejectAccessToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid actor token
/// will cause the authentication demand to be rejected or will be ignored.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RejectActorToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid authorization code
/// will cause the authentication demand to be rejected or will be ignored.
@ -633,6 +696,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool RejectRequestToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid subject token
/// will cause the authentication demand to be rejected or will be ignored.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool RejectSubjectToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an invalid user code
/// will cause the authentication demand to be rejected or will be ignored.
@ -652,6 +724,21 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? AccessTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the actor token to validate, if applicable.
/// </summary>
public string? ActorToken { get; set; }
/// <summary>
/// Gets or sets the type of the actor token, if applicable.
/// </summary>
public string? ActorTokenType { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the actor token, if applicable.
/// </summary>
public ClaimsPrincipal? ActorTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the authorization code to validate, if applicable.
/// </summary>
@ -732,6 +819,21 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? RequestTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the subject token to validate, if applicable.
/// </summary>
public string? SubjectToken { get; set; }
/// <summary>
/// Gets or sets the type of the subject token, if applicable.
/// </summary>
public string? SubjectTokenType { get; set; }
/// <summary>
/// Gets or sets the principal extracted from the subject token, if applicable.
/// </summary>
public ClaimsPrincipal? SubjectTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the user code to validate, if applicable.
/// </summary>
@ -853,6 +955,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool GenerateDeviceCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an issued token
/// should be generated (and optionally returned to the client).
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool GenerateIssuedToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an identity token
/// should be generated (and optionally returned to the client).
@ -916,6 +1027,15 @@ public static partial class OpenIddictServerEvents
/// </remarks>
public bool IncludeDeviceCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated issued token
/// should be returned to the client application as part of the response.
/// </summary>
/// <remarks>
/// Note: overriding the value of this property is generally not recommended.
/// </remarks>
public bool IncludeIssuedToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the generated identity token
/// should be returned to the client application as part of the response.
@ -991,6 +1111,24 @@ public static partial class OpenIddictServerEvents
/// </summary>
public ClaimsPrincipal? DeviceCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the generated issued token, if applicable.
/// The issued token will only be returned if
/// <see cref="IncludeAccessToken"/> is set to <see langword="true"/>.
/// </summary>
public string? IssuedToken { get; set; }
/// <summary>
/// Gets or sets the type of the issued token requested by the client, if applicable.
/// </summary>
public string? IssuedTokenType { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the issued token, if applicable.
/// </summary>
public ClaimsPrincipal? IssuedTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the generated identity token, if applicable.
/// The identity token will only be returned if

5
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -42,6 +42,7 @@ public static class OpenIddictServerExtensions
// Register the built-in filters used by the default OpenIddict server event handlers.
builder.Services.TryAddSingleton<RequireAccessTokenGenerated>();
builder.Services.TryAddSingleton<RequireAccessTokenValidated>();
builder.Services.TryAddSingleton<RequireActorTokenValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeGenerated>();
builder.Services.TryAddSingleton<RequireAuthorizationCodeValidated>();
builder.Services.TryAddSingleton<RequireAuthorizationIdResolved>();
@ -64,6 +65,7 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireIdentityTokenGenerated>();
builder.Services.TryAddSingleton<RequireIdentityTokenValidated>();
builder.Services.TryAddSingleton<RequireIntrospectionRequest>();
builder.Services.TryAddSingleton<RequireIssuedTokenGenerated>();
builder.Services.TryAddSingleton<RequireJsonWebKeySetRequest>();
builder.Services.TryAddSingleton<RequireJsonWebTokenFormat>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectUriParameter>();
@ -77,9 +79,10 @@ public static class OpenIddictServerExtensions
builder.Services.TryAddSingleton<RequireRequestTokenValidated>();
builder.Services.TryAddSingleton<RequireResponseTypePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireRevocationRequest>();
builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>();
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();
builder.Services.TryAddSingleton<RequireSlidingRefreshTokenExpirationEnabled>();
builder.Services.TryAddSingleton<RequireSubjectTokenValidated>();
builder.Services.TryAddSingleton<RequireTokenAudienceValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEntryCreated>();
builder.Services.TryAddSingleton<RequireTokenIdResolved>();

51
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -45,6 +45,23 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no actor token is validated.
/// </summary>
public sealed class RequireActorTokenValidated : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.ValidateActorToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no authorization code is generated.
/// </summary>
@ -419,6 +436,23 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no issued token is generated.
/// </summary>
public sealed class RequireIssuedTokenGenerated : IOpenIddictServerHandlerFilter<ProcessSignInContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.GenerateIssuedToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the request is not a JSON Web Key Set request.
/// </summary>
@ -691,6 +725,23 @@ public static class OpenIddictServerHandlerFilters
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no subject token is validated.
/// </summary>
public sealed class RequireSubjectTokenValidated : IOpenIddictServerHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.ValidateSubjectToken);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if token audience validation was disabled.
/// </summary>

4
src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs

@ -1102,7 +1102,7 @@ public static partial class OpenIddictServerHandlers
}
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.UserCodePrincipal;
context.UserCodePrincipal = notification.UserCodePrincipal;
}
}
@ -1133,7 +1133,7 @@ public static partial class OpenIddictServerHandlers
typeof(ValidateEndUserVerificationRequestContext).FullName!) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
context.UserCodePrincipal ??= notification.Principal;
context.UserCodePrincipal ??= notification.UserCodePrincipal;
return default;
}

485
src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs

@ -43,6 +43,7 @@ public static partial class OpenIddictServerHandlers
ValidateClientCredentialsParameters.Descriptor,
ValidateDeviceCodeParameter.Descriptor,
ValidateRefreshTokenParameter.Descriptor,
ValidateTokenExchangeParameters.Descriptor,
ValidateResourceOwnerCredentialsParameters.Descriptor,
ValidateProofKeyForCodeExchangeParameters.Descriptor,
ValidateScopeParameter.Descriptor,
@ -52,7 +53,7 @@ public static partial class OpenIddictServerHandlers
ValidateGrantTypePermissions.Descriptor,
ValidateScopePermissions.Descriptor,
ValidateProofKeyForCodeExchangeRequirement.Descriptor,
ValidatePresenters.Descriptor,
ValidateAuthorizedParty.Descriptor,
ValidateRedirectUri.Descriptor,
ValidateCodeVerifier.Descriptor,
ValidateGrantedScopes.Descriptor,
@ -682,6 +683,130 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for rejecting token requests that
/// specify invalid parameters for the token exchange grant type.
/// </summary>
public sealed class ValidateTokenExchangeParameters : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidateTokenExchangeParameters>()
.SetOrder(ValidateRefreshTokenParameter.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ValidateTokenRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Request.IsTokenExchangeGrantType())
{
return default;
}
// Reject token exchange requests missing the mandatory subject token.
//
// See https://datatracker.ietf.org/doc/html/rfc8693#section-2.1 for more information.
if (string.IsNullOrEmpty(context.Request.SubjectToken))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6077), Parameters.SubjectToken);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2029(Parameters.SubjectToken),
uri: SR.FormatID8000(SR.ID2029));
return default;
}
// Reject token exchange requests missing the mandatory subject token type.
if (string.IsNullOrEmpty(context.Request.SubjectTokenType))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6077), Parameters.SubjectTokenType);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2029(Parameters.SubjectTokenType),
uri: SR.FormatID8000(SR.ID2029));
return default;
}
// Reject token exchange requests that specify an actor token but don't include an actor token type.
if (!string.IsNullOrEmpty(context.Request.ActorToken) &&
string.IsNullOrEmpty(context.Request.ActorTokenType))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6077), Parameters.ActorTokenType);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ActorToken, Parameters.ActorTokenType),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Reject token exchange requests that specify an actor token type but don't include an actor token.
if (string.IsNullOrEmpty(context.Request.ActorToken) &&
!string.IsNullOrEmpty(context.Request.ActorTokenType))
{
context.Logger.LogInformation(SR.GetResourceString(SR.ID6077), Parameters.ActorToken);
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2037(Parameters.ActorTokenType, Parameters.ActorToken),
uri: SR.FormatID8000(SR.ID2037));
return default;
}
// Reject token exchange requests that specify an unsupported subject token type.
if (!context.Options.SubjectTokenTypes.Contains(context.Request.SubjectTokenType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2032(Parameters.SubjectTokenType),
uri: SR.FormatID8000(SR.ID2032));
return default;
}
// Reject token exchange requests that specify an unsupported actor token type.
if (!string.IsNullOrEmpty(context.Request.ActorTokenType) &&
!context.Options.ActorTokenTypes.Contains(context.Request.ActorTokenType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2032(Parameters.ActorTokenType),
uri: SR.FormatID8000(SR.ID2032));
return default;
}
// Reject token exchange requests that specify an unsupported requested token type.
if (!string.IsNullOrEmpty(context.Request.RequestedTokenType) &&
!context.Options.RequestedTokenTypes.Contains(context.Request.RequestedTokenType))
{
context.Reject(
error: Errors.InvalidRequest,
description: SR.FormatID2032(Parameters.RequestedTokenType),
uri: SR.FormatID8000(SR.ID2032));
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for rejecting token requests
/// that specify invalid parameters for the password grant type.
@ -694,7 +819,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidateResourceOwnerCredentialsParameters>()
.SetOrder(ValidateRefreshTokenParameter.Descriptor.Order + 1_000)
.SetOrder(ValidateTokenExchangeParameters.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -955,10 +1080,12 @@ public static partial class OpenIddictServerHandlers
return;
}
// Attach the security principal extracted from the token to the validation context.
context.Principal = context.Request.IsAuthorizationCodeGrantType() ? notification.AuthorizationCodePrincipal :
context.Request.IsDeviceCodeGrantType() ? notification.DeviceCodePrincipal :
context.Request.IsRefreshTokenGrantType() ? notification.RefreshTokenPrincipal : null;
// Attach the security principals extracted from the tokens to the validation context.
context.ActorTokenPrincipal = notification.ActorTokenPrincipal;
context.AuthorizationCodePrincipal = notification.AuthorizationCodePrincipal;
context.DeviceCodePrincipal = notification.DeviceCodePrincipal;
context.RefreshTokenPrincipal = notification.RefreshTokenPrincipal;
context.SubjectTokenPrincipal = notification.SubjectTokenPrincipal;
}
}
@ -1218,17 +1345,17 @@ public static partial class OpenIddictServerHandlers
}
/// <summary>
/// Contains the logic responsible for rejecting token requests that use an authorization code,
/// a device code or a refresh token that was issued for a different client application.
/// Contains the logic responsible for rejecting token requests that specify a token
/// that cannot be used by the client application sending the token request.
/// </summary>
public sealed class ValidatePresenters : IOpenIddictServerHandler<ValidateTokenRequestContext>
public sealed class ValidateAuthorizedParty : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidatePresenters>()
.UseSingletonHandler<ValidateAuthorizedParty>()
.SetOrder(ValidateProofKeyForCodeExchangeRequirement.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -1241,69 +1368,263 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsDeviceCodeGrantType() &&
!context.Request.IsRefreshTokenGrantType())
if (context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsDeviceCodeGrantType() ||
context.Request.IsRefreshTokenGrantType())
{
return default;
}
var principal = context.Request.GrantType switch
{
GrantTypes.AuthorizationCode => context.AuthorizationCodePrincipal,
GrantTypes.DeviceCode => context.DeviceCodePrincipal,
GrantTypes.RefreshToken => context.RefreshTokenPrincipal,
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
_ => null
};
var presenters = context.Principal.GetPresenters();
if (presenters.IsDefaultOrEmpty)
{
// Note: presenters may be empty during a grant_type=refresh_token request if the refresh token
// was issued to a public client but cannot be null for an authorization or device code grant request.
if (context.Request.IsAuthorizationCodeGrantType())
Debug.Assert(principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
if (!principal.HasClaim(Claims.Private.Presenter))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0043));
// Note: presenters may be missing during a grant_type=refresh_token request if the refresh token was
// issued to a public client but cannot be missing for an authorization or device code grant request.
if (context.Request.IsAuthorizationCodeGrantType())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0043));
}
if (context.Request.IsDeviceCodeGrantType())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0044));
}
// Note: when using the refresh token grant, client_id is optional but MUST be validated if present.
//
// See https://tools.ietf.org/html/rfc6749#section-6
// and http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken for more information.
return default;
}
if (context.Request.IsDeviceCodeGrantType())
// If at least one presenter was associated to the authorization code/device code/refresh token,
// reject the request if the client_id of the caller cannot be retrieved or inferred.
if (string.IsNullOrEmpty(context.ClientId))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0044));
context.Logger.LogInformation(6090, SR.GetResourceString(SR.ID6090));
context.Reject(
error: Errors.InvalidGrant,
description: context.Request.GrantType switch
{
GrantTypes.AuthorizationCode => SR.GetResourceString(SR.ID2066),
GrantTypes.DeviceCode => SR.GetResourceString(SR.ID2067),
_ => SR.GetResourceString(SR.ID2068)
},
uri: context.Request.GrantType switch
{
GrantTypes.AuthorizationCode => SR.FormatID8000(SR.ID2066),
GrantTypes.DeviceCode => SR.FormatID8000(SR.ID2067),
_ => SR.FormatID8000(SR.ID2068)
});
return default;
}
return default;
// Ensure the authorization code/device code/refresh token was issued to the client making the token request.
if (!principal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6091, SR.GetResourceString(SR.ID6091));
context.Reject(
error: Errors.InvalidGrant,
description: context.Request.GrantType switch
{
GrantTypes.AuthorizationCode => SR.GetResourceString(SR.ID2069),
GrantTypes.DeviceCode => SR.GetResourceString(SR.ID2070),
_ => SR.GetResourceString(SR.ID2071)
},
uri: context.Request.GrantType switch
{
GrantTypes.AuthorizationCode => SR.FormatID8000(SR.ID2069),
GrantTypes.DeviceCode => SR.FormatID8000(SR.ID2070),
_ => SR.FormatID8000(SR.ID2071)
});
return default;
}
}
// If at least one presenter was associated to the authorization code/device code/refresh token,
// reject the request if the client_id of the caller cannot be retrieved or inferred.
if (string.IsNullOrEmpty(context.ClientId))
else if (context.Request.IsTokenExchangeGrantType())
{
context.Logger.LogInformation(6090, SR.GetResourceString(SR.ID6090));
switch (context.SubjectTokenPrincipal?.GetTokenType())
{
case TokenTypeIdentifiers.AccessToken:
case TokenTypeIdentifiers.IdentityToken:
{
// If the token can be used with any audience and/or presenter, allow any caller to use it.
if (!context.SubjectTokenPrincipal.HasClaim(Claims.Private.Audience) ||
!context.SubjectTokenPrincipal.HasClaim(Claims.Private.Presenter))
{
break;
}
// When the token is both presenter/sender-constrained and audience/receiver-constrained,
// reject the request if the client identifier of the caller cannot be retrieved or inferred.
if (string.IsNullOrEmpty(context.ClientId))
{
context.Logger.LogInformation(6268, SR.GetResourceString(SR.ID6268));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2186),
uri: SR.FormatID8000(SR.ID2186));
return default;
}
// Reject the request if the caller is neither a valid audience nor a valid presenter.
if (!context.SubjectTokenPrincipal.HasAudience(context.ClientId) &&
!context.SubjectTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6269, SR.GetResourceString(SR.ID6269));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2187),
uri: SR.FormatID8000(SR.ID2187));
return default;
}
break;
}
context.Reject(
error: Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ? SR.GetResourceString(SR.ID2066) :
context.Request.IsDeviceCodeGrantType() ? SR.GetResourceString(SR.ID2067) :
SR.GetResourceString(SR.ID2068),
uri: context.Request.IsAuthorizationCodeGrantType() ? SR.FormatID8000(SR.ID2066) :
context.Request.IsDeviceCodeGrantType() ? SR.FormatID8000(SR.ID2067) :
SR.FormatID8000(SR.ID2068));
case TokenTypeIdentifiers.RefreshToken:
{
// If the token can be used with any presenter, allow any caller to use it.
if (!context.SubjectTokenPrincipal.HasClaim(Claims.Private.Presenter))
{
break;
}
// When the token is presenter/sender-constrained, reject the request
// if the client identifier of the caller cannot be retrieved or inferred.
if (string.IsNullOrEmpty(context.ClientId))
{
context.Logger.LogInformation(6268, SR.GetResourceString(SR.ID6268));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2186),
uri: SR.FormatID8000(SR.ID2186));
return default;
}
// Reject the request if the caller is not a valid presenter.
if (!context.SubjectTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6269, SR.GetResourceString(SR.ID6269));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2187),
uri: SR.FormatID8000(SR.ID2187));
return default;
}
break;
}
return default;
}
// Other types of tokens (e.g generic JWT assertions) are not supported by this event
// handler and are expected to be validated using the regular token validation routine.
// Alternatively, a custom event handler can also be implemented to use a different logic.
}
// Ensure the authorization code/device code/refresh token was issued to the client making the token request.
// Note: when using the refresh token grant, client_id is optional but MUST be validated if present.
// See https://tools.ietf.org/html/rfc6749#section-6
// and http://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken.
if (!presenters.Contains(context.ClientId))
{
context.Logger.LogWarning(6091, SR.GetResourceString(SR.ID6091));
switch (context.ActorTokenPrincipal?.GetTokenType())
{
case TokenTypeIdentifiers.AccessToken:
case TokenTypeIdentifiers.IdentityToken:
{
// If the token can be used with any audience and/or presenter, allow any caller to use it.
if (!context.ActorTokenPrincipal.HasClaim(Claims.Private.Audience) ||
!context.ActorTokenPrincipal.HasClaim(Claims.Private.Presenter))
{
break;
}
// When the token is both presenter/sender-constrained and audience/receiver-constrained,
// reject the request if the client identifier of the caller cannot be retrieved or inferred.
if (string.IsNullOrEmpty(context.ClientId))
{
context.Logger.LogInformation(6270, SR.GetResourceString(SR.ID6270));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2188),
uri: SR.FormatID8000(SR.ID2188));
return default;
}
// Reject the request if the caller is neither a valid audience nor a valid presenter.
if (!context.ActorTokenPrincipal.HasAudience(context.ClientId) &&
!context.ActorTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6271, SR.GetResourceString(SR.ID6271));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2189),
uri: SR.FormatID8000(SR.ID2189));
return default;
}
break;
}
context.Reject(
error: Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ? SR.GetResourceString(SR.ID2069) :
context.Request.IsDeviceCodeGrantType() ? SR.GetResourceString(SR.ID2070) :
SR.GetResourceString(SR.ID2071),
uri: context.Request.IsAuthorizationCodeGrantType() ? SR.FormatID8000(SR.ID2069) :
context.Request.IsDeviceCodeGrantType() ? SR.FormatID8000(SR.ID2070) :
SR.FormatID8000(SR.ID2071));
case TokenTypeIdentifiers.RefreshToken:
{
// If the token can be used with any presenter, allow any caller to use it.
if (!context.ActorTokenPrincipal.HasClaim(Claims.Private.Presenter))
{
break;
}
// When the token is presenter/sender-constrained, reject the request
// if the client identifier of the caller cannot be retrieved or inferred.
if (string.IsNullOrEmpty(context.ClientId))
{
context.Logger.LogInformation(6270, SR.GetResourceString(SR.ID6270));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2188),
uri: SR.FormatID8000(SR.ID2188));
return default;
}
// Reject the request if the caller is not a valid presenter.
if (!context.ActorTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6271, SR.GetResourceString(SR.ID6271));
context.Reject(
error: Errors.InvalidGrant,
description: SR.GetResourceString(SR.ID2189),
uri: SR.FormatID8000(SR.ID2189));
return default;
}
break;
}
return default;
// Other types of tokens (e.g generic JWT assertions) are not supported by this event
// handler and are expected to be validated using the regular token validation routine.
// Alternatively, a custom event handler can also be implemented to use a different logic.
}
}
return default;
@ -1321,7 +1642,7 @@ public static partial class OpenIddictServerHandlers
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidateRedirectUri>()
.SetOrder(ValidatePresenters.Descriptor.Order + 1_000)
.SetOrder(ValidateAuthorizedParty.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -1338,7 +1659,7 @@ public static partial class OpenIddictServerHandlers
return default;
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.AuthorizationCodePrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Validate the redirect_uri sent by the client application as part of this token request.
// Note: for pure OAuth 2.0 requests, redirect_uri is only mandatory if the authorization request
@ -1347,7 +1668,7 @@ public static partial class OpenIddictServerHandlers
// if the authorization request didn't contain an explicit redirect_uri.
// See https://tools.ietf.org/html/rfc6749#section-4.1.3
// and http://openid.net/specs/openid-connect-core-1_0.html#TokenRequestValidation.
var uri = context.Principal.GetClaim(Claims.Private.RedirectUri);
var uri = context.AuthorizationCodePrincipal.GetClaim(Claims.Private.RedirectUri);
if (string.IsNullOrEmpty(uri))
{
return default;
@ -1409,7 +1730,7 @@ public static partial class OpenIddictServerHandlers
return default;
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.AuthorizationCodePrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Note: the ValidateProofKeyForCodeExchangeRequirement handler (invoked earlier) ensures
// a code_verifier is specified if the proof key for code exchange requirement was enforced
@ -1417,7 +1738,7 @@ public static partial class OpenIddictServerHandlers
// is active even if the degraded mode is enabled and ensures that a code_verifier is sent if a
// code_challenge was stored in the authorization code when the authorization request was handled.
var challenge = context.Principal.GetClaim(Claims.Private.CodeChallenge);
var challenge = context.AuthorizationCodePrincipal.GetClaim(Claims.Private.CodeChallenge);
if (string.IsNullOrEmpty(challenge))
{
// Validate that the token request does not include a code_verifier parameter
@ -1450,7 +1771,7 @@ public static partial class OpenIddictServerHandlers
return default;
}
var comparand = context.Principal.GetClaim(Claims.Private.CodeChallengeMethod) switch
var comparand = context.AuthorizationCodePrincipal.GetClaim(Claims.Private.CodeChallengeMethod) switch
{
// Note: when using the "plain" code challenge method, no hashing is actually performed.
// In this case, the raw bytes of the verifier are directly compared to the challenge.
@ -1513,12 +1834,12 @@ public static partial class OpenIddictServerHandlers
return default;
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.RefreshTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// When an explicit scope parameter has been included in the token request
// but was missing from the initial request, the request MUST be rejected.
// See http://tools.ietf.org/html/rfc6749#section-6 for more information.
var scopes = context.Principal.GetScopes().ToHashSet(StringComparer.Ordinal);
var scopes = context.RefreshTokenPrincipal.GetScopes().ToHashSet(StringComparer.Ordinal);
if (scopes.Count is 0)
{
context.Logger.LogInformation(6094, SR.GetResourceString(SR.ID6094), Parameters.Scope);
@ -1575,7 +1896,10 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType())
if (!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsDeviceCodeGrantType() &&
!context.Request.IsRefreshTokenGrantType() &&
!context.Request.IsTokenExchangeGrantType())
{
return default;
}
@ -1584,7 +1908,30 @@ public static partial class OpenIddictServerHandlers
typeof(ValidateTokenRequestContext).FullName!) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
context.Principal ??= notification.Principal;
context.ActorTokenPrincipal = notification.ActorTokenPrincipal;
context.AuthorizationCodePrincipal = notification.AuthorizationCodePrincipal;
context.DeviceCodePrincipal = notification.DeviceCodePrincipal;
context.RefreshTokenPrincipal = notification.RefreshTokenPrincipal;
context.SubjectTokenPrincipal = notification.SubjectTokenPrincipal;
// By default, use the principal extracted from the authorization code/device code/
// refresh token/subject token as the principal that will be used in the response.
context.Principal ??= context.Request.GrantType switch
{
GrantTypes.AuthorizationCode => notification.AuthorizationCodePrincipal,
GrantTypes.DeviceCode => notification.DeviceCodePrincipal,
GrantTypes.RefreshToken => notification.RefreshTokenPrincipal,
// Do not flow the internal claims when using the OAuth 2.0 Token Exchange grant to
// avoid binding the resulting issued token to the token used as the subject token.
// This means that, by default, the scopes present in the subject tokens won't be
// reused as-is and that a new ad-hoc authorization, separate from the one attached
// to the subject token will be created and attached to the issued token by OpenIddict.
GrantTypes.TokenExchange => notification.SubjectTokenPrincipal
?.Clone(claim => !claim.Type.StartsWith(Claims.Prefixes.Private)),
_ => null
};
return default;
}
@ -1618,12 +1965,12 @@ public static partial class OpenIddictServerHandlers
return default;
}
// If the error indicates an invalid token caused by an invalid authorization,
// device code or refresh token, return a standard invalid_grant.
// If the error indicates an invalid token, return a standard invalid_grant.
if (context.Request is null || !(context.Request.IsAuthorizationCodeGrantType() ||
context.Request.IsDeviceCodeGrantType() ||
context.Request.IsRefreshTokenGrantType()))
context.Request.IsRefreshTokenGrantType() ||
context.Request.IsTokenExchangeGrantType()))
{
return default;
}

58
src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs

@ -505,7 +505,7 @@ public static partial class OpenIddictServerHandlers
}
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.GenericTokenPrincipal;
context.GenericTokenPrincipal = notification.GenericTokenPrincipal;
}
}
@ -587,10 +587,10 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
if (!context.Principal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
!context.Principal.HasTokenType(TokenTypeIdentifiers.RefreshToken))
if (!context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
!context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.RefreshToken))
{
context.Logger.LogInformation(6104, SR.GetResourceString(SR.ID6104));
@ -608,7 +608,7 @@ public static partial class OpenIddictServerHandlers
/// <summary>
/// Contains the logic responsible for rejecting introspection requests that specify a token
/// that cannot be introspected by the client application sending the introspection requests.
/// that cannot be introspected by the client application sending the introspection request.
/// </summary>
public sealed class ValidateAuthorizedParty : IOpenIddictServerHandler<ValidateIntrospectionRequestContext>
{
@ -635,15 +635,15 @@ public static partial class OpenIddictServerHandlers
}
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// When the introspected token is an access token, the caller must be listed either as a presenter
// (i.e the party the token was issued to) or as an audience (i.e a resource server/API).
// If the access token doesn't contain any explicit presenter/audience, the token is assumed
// to be not specific to any resource server/client application and the check is bypassed.
if (context.Principal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
context.Principal.HasClaim(Claims.Private.Audience) && !context.Principal.HasAudience(context.ClientId) &&
context.Principal.HasClaim(Claims.Private.Presenter) && !context.Principal.HasPresenter(context.ClientId))
if (context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
context.GenericTokenPrincipal.HasClaim(Claims.Private.Audience) && !context.GenericTokenPrincipal.HasAudience(context.ClientId) &&
context.GenericTokenPrincipal.HasClaim(Claims.Private.Presenter) && !context.GenericTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6106, SR.GetResourceString(SR.ID6106));
@ -659,8 +659,8 @@ public static partial class OpenIddictServerHandlers
// listed as a presenter (i.e the party the token was issued to).
// If the refresh token doesn't contain any explicit presenter, the token is
// assumed to be not specific to any client application and the check is bypassed.
if (context.Principal.HasTokenType(TokenTypeIdentifiers.RefreshToken) &&
context.Principal.HasClaim(Claims.Private.Presenter) && !context.Principal.HasPresenter(context.ClientId))
if (context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.RefreshToken) &&
context.GenericTokenPrincipal.HasClaim(Claims.Private.Presenter) && !context.GenericTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6108, SR.GetResourceString(SR.ID6108));
@ -704,9 +704,9 @@ public static partial class OpenIddictServerHandlers
typeof(ValidateIntrospectionRequestContext).FullName!) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
Debug.Assert(notification.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(notification.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
context.Principal ??= notification.Principal;
context.GenericTokenPrincipal ??= notification.GenericTokenPrincipal;
return default;
}
@ -735,14 +735,14 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
context.Issuer = context.Options.Issuer ?? context.BaseUri;
context.TokenId = context.Principal.GetClaim(Claims.JwtId);
context.Subject = context.Principal.GetClaim(Claims.Subject);
context.TokenId = context.GenericTokenPrincipal.GetClaim(Claims.JwtId);
context.Subject = context.GenericTokenPrincipal.GetClaim(Claims.Subject);
context.TokenUsage = context.Principal.GetTokenType() switch
context.TokenUsage = context.GenericTokenPrincipal.GetTokenType() switch
{
TokenTypeIdentifiers.AccessToken => "access_token",
TokenTypeIdentifiers.Private.AuthorizationCode => "authorization_code",
@ -754,18 +754,18 @@ public static partial class OpenIddictServerHandlers
_ => null
};
context.IssuedAt = context.NotBefore = context.Principal.GetCreationDate();
context.ExpiresAt = context.Principal.GetExpirationDate();
context.IssuedAt = context.NotBefore = context.GenericTokenPrincipal.GetCreationDate();
context.ExpiresAt = context.GenericTokenPrincipal.GetExpirationDate();
// Infer the audiences/client_id from the claims stored in the security principal.
context.Audiences.UnionWith(context.Principal.GetAudiences());
context.ClientId = context.Principal.GetClaim(Claims.ClientId) ??
context.Principal.GetPresenters().FirstOrDefault();
context.Audiences.UnionWith(context.GenericTokenPrincipal.GetAudiences());
context.ClientId = context.GenericTokenPrincipal.GetClaim(Claims.ClientId) ??
context.GenericTokenPrincipal.FindFirst(Claims.Private.Presenter)?.Value;
// Note: only set "token_type" when the received token is an access token.
// See https://tools.ietf.org/html/rfc7662#section-2.2
// and https://tools.ietf.org/html/rfc6749#section-5.1 for more information.
if (context.Principal.HasTokenType(TokenTypeIdentifiers.AccessToken))
if (context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken))
{
context.TokenType = TokenTypes.Bearer;
}
@ -808,17 +808,17 @@ public static partial class OpenIddictServerHandlers
}
Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), SR.FormatID4000(Parameters.ClientId));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Don't return application-specific claims if the token is not an access token.
if (!context.Principal.HasTokenType(TokenTypeIdentifiers.AccessToken))
if (!context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken))
{
return;
}
// Only specified audiences (that were explicitly defined as allowed resources) can access
// the sensitive application-specific claims contained in the introspected access token.
if (!context.Principal.HasAudience(context.Request.ClientId))
if (!context.GenericTokenPrincipal.HasAudience(context.Request.ClientId))
{
context.Logger.LogInformation(6105, SR.GetResourceString(SR.ID6105), context.Request.ClientId);
@ -836,10 +836,10 @@ public static partial class OpenIddictServerHandlers
return;
}
context.Username = context.Principal.Identity.Name;
context.Scopes.UnionWith(context.Principal.GetScopes());
context.Username = context.GenericTokenPrincipal.Identity.Name;
context.Scopes.UnionWith(context.GenericTokenPrincipal.GetScopes());
foreach (var group in context.Principal.Claims.GroupBy(claim => claim.Type))
foreach (var group in context.GenericTokenPrincipal.Claims.GroupBy(claim => claim.Type))
{
// Exclude standard claims, that are already handled via strongly-typed properties.
// Make sure to always update this list when adding new built-in claim properties.

30
src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs

@ -452,7 +452,7 @@ public static partial class OpenIddictServerHandlers
}
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.GenericTokenPrincipal;
context.GenericTokenPrincipal = notification.GenericTokenPrincipal;
}
}
@ -534,10 +534,10 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
if (!context.Principal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
!context.Principal.HasTokenType(TokenTypeIdentifiers.RefreshToken))
if (!context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
!context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.RefreshToken))
{
context.Logger.LogInformation(6117, SR.GetResourceString(SR.ID6117));
@ -555,7 +555,7 @@ public static partial class OpenIddictServerHandlers
/// <summary>
/// Contains the logic responsible for rejecting revocation requests that specify a token
/// that cannot be revoked by the client application sending the revocation requests.
/// that cannot be revoked by the client application sending the revocation request.
/// </summary>
public sealed class ValidateAuthorizedParty : IOpenIddictServerHandler<ValidateRevocationRequestContext>
{
@ -582,15 +582,15 @@ public static partial class OpenIddictServerHandlers
}
Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// When the revoked token is an access token, the caller must be listed either as a presenter
// (i.e the party the token was issued to) or as an audience (i.e a resource server/API).
// If the access token doesn't contain any explicit presenter/audience, the token is assumed
// to be not specific to any resource server/client application and the check is bypassed.
if (context.Principal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
context.Principal.HasClaim(Claims.Private.Audience) && !context.Principal.HasAudience(context.ClientId) &&
context.Principal.HasClaim(Claims.Private.Presenter) && !context.Principal.HasPresenter(context.ClientId))
if (context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken) &&
context.GenericTokenPrincipal.HasClaim(Claims.Private.Audience) && !context.GenericTokenPrincipal.HasAudience(context.ClientId) &&
context.GenericTokenPrincipal.HasClaim(Claims.Private.Presenter) && !context.GenericTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6119, SR.GetResourceString(SR.ID6119));
@ -606,8 +606,8 @@ public static partial class OpenIddictServerHandlers
// listed as a presenter (i.e the party the token was issued to).
// If the refresh token doesn't contain any explicit presenter, the token is
// assumed to be not specific to any client application and the check is bypassed.
if (context.Principal.HasTokenType(TokenTypeIdentifiers.RefreshToken) &&
context.Principal.HasClaim(Claims.Private.Presenter) && !context.Principal.HasPresenter(context.ClientId))
if (context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.RefreshToken) &&
context.GenericTokenPrincipal.HasClaim(Claims.Private.Presenter) && !context.GenericTokenPrincipal.HasPresenter(context.ClientId))
{
context.Logger.LogWarning(6121, SR.GetResourceString(SR.ID6121));
@ -651,9 +651,9 @@ public static partial class OpenIddictServerHandlers
typeof(ValidateRevocationRequestContext).FullName!) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
Debug.Assert(notification.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(notification.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
context.Principal ??= notification.Principal;
context.GenericTokenPrincipal ??= notification.GenericTokenPrincipal;
return default;
}
@ -691,10 +691,10 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Extract the token identifier from the authentication principal.
var identifier = context.Principal.GetTokenId();
var identifier = context.GenericTokenPrincipal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
context.Logger.LogInformation(6122, SR.GetResourceString(SR.ID6122));

30
src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs

@ -400,7 +400,7 @@ public static partial class OpenIddictServerHandlers
}
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.AccessTokenPrincipal;
context.AccessTokenPrincipal = notification.AccessTokenPrincipal;
}
}
@ -432,9 +432,9 @@ public static partial class OpenIddictServerHandlers
typeof(ValidateUserInfoRequestContext).FullName!) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0007));
Debug.Assert(notification.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(notification.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
context.Principal ??= notification.Principal;
context.AccessTokenPrincipal ??= notification.AccessTokenPrincipal;
return default;
}
@ -463,12 +463,12 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Note: when receiving an access token, its audiences list cannot be used for the "aud" claim
// as the client application is not the intented audience but only an authorized presenter.
// See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse
context.Audiences.UnionWith(context.Principal.GetPresenters());
context.Audiences.UnionWith(context.AccessTokenPrincipal.GetPresenters());
return default;
}
@ -497,29 +497,29 @@ public static partial class OpenIddictServerHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
Debug.Assert(context.AccessTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
context.Issuer = context.Options.Issuer ?? context.BaseUri;
context.Subject = context.Principal.GetClaim(Claims.Subject);
context.Subject = context.AccessTokenPrincipal.GetClaim(Claims.Subject);
// The following claims are all optional and should be excluded when
// no corresponding value has been found in the authentication principal:
if (context.Principal.HasScope(Scopes.Profile))
if (context.AccessTokenPrincipal.HasScope(Scopes.Profile))
{
context.FamilyName = context.Principal.GetClaim(Claims.FamilyName);
context.GivenName = context.Principal.GetClaim(Claims.GivenName);
context.BirthDate = context.Principal.GetClaim(Claims.Birthdate);
context.FamilyName = context.AccessTokenPrincipal.GetClaim(Claims.FamilyName);
context.GivenName = context.AccessTokenPrincipal.GetClaim(Claims.GivenName);
context.BirthDate = context.AccessTokenPrincipal.GetClaim(Claims.Birthdate);
}
if (context.Principal.HasScope(Scopes.Email))
if (context.AccessTokenPrincipal.HasScope(Scopes.Email))
{
context.Email = context.Principal.GetClaim(Claims.Email);
context.Email = context.AccessTokenPrincipal.GetClaim(Claims.Email);
}
if (context.Principal.HasScope(Scopes.Phone))
if (context.AccessTokenPrincipal.HasScope(Scopes.Phone))
{
context.PhoneNumber = context.Principal.GetClaim(Claims.PhoneNumber);
context.PhoneNumber = context.AccessTokenPrincipal.GetClaim(Claims.PhoneNumber);
}
return default;

761
src/OpenIddict.Server/OpenIddictServerHandlers.cs

@ -52,6 +52,8 @@ public static partial class OpenIddictServerHandlers
ValidateGenericToken.Descriptor,
ValidateIdentityToken.Descriptor,
ValidateRefreshToken.Descriptor,
ValidateSubjectToken.Descriptor,
ValidateActorToken.Descriptor,
ValidateUserCode.Descriptor,
ResolveHostAuthenticationProperties.Descriptor,
@ -82,6 +84,7 @@ public static partial class OpenIddictServerHandlers
PrepareAccessTokenPrincipal.Descriptor,
PrepareAuthorizationCodePrincipal.Descriptor,
PrepareDeviceCodePrincipal.Descriptor,
PrepareIssuedTokenPrincipal.Descriptor,
PrepareRequestTokenPrincipal.Descriptor,
PrepareRefreshTokenPrincipal.Descriptor,
PrepareIdentityTokenPrincipal.Descriptor,
@ -90,6 +93,7 @@ public static partial class OpenIddictServerHandlers
GenerateAccessToken.Descriptor,
GenerateAuthorizationCode.Descriptor,
GenerateDeviceCode.Descriptor,
GenerateIssuedToken.Descriptor,
GenerateRequestToken.Descriptor,
GenerateRefreshToken.Descriptor,
@ -295,6 +299,18 @@ public static partial class OpenIddictServerHandlers
_ => (false, false, false, false)
};
(context.ExtractActorToken,
context.RequireActorToken,
context.ValidateActorToken,
context.RejectActorToken) = context.EndpointType switch
{
// The actor token is optional for the token exchange grant.
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType()
=> (true, false, true, true),
_ => (false, false, false, false)
};
(context.ExtractAuthorizationCode,
context.RequireAuthorizationCode,
context.ValidateAuthorizationCode,
@ -389,6 +405,18 @@ public static partial class OpenIddictServerHandlers
_ => (false, false, false, false)
};
(context.ExtractSubjectToken,
context.RequireSubjectToken,
context.ValidateSubjectToken,
context.RejectSubjectToken) = context.EndpointType switch
{
// The subject token is mandatory for the token exchange grant.
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType()
=> (true, true, true, true),
_ => (false, false, false, false)
};
(context.ExtractUserCode,
context.RequireUserCode,
context.ValidateUserCode,
@ -436,6 +464,14 @@ public static partial class OpenIddictServerHandlers
_ => null
};
(context.ActorToken, context.ActorTokenType) = context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.ExtractActorToken
=> (context.Request.ActorToken, context.Request.ActorTokenType),
_ => (null, null)
};
context.AuthorizationCode = context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.ExtractAuthorizationCode
@ -506,6 +542,14 @@ public static partial class OpenIddictServerHandlers
_ => null
};
(context.SubjectToken, context.SubjectTokenType) = context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.ExtractSubjectToken
=> (context.Request.SubjectToken, context.Request.SubjectTokenType),
_ => (null, null)
};
context.UserCode = context.EndpointType switch
{
OpenIddictServerEndpointType.EndUserVerification when context.ExtractUserCode
@ -544,6 +588,7 @@ public static partial class OpenIddictServerHandlers
}
if ((context.RequireAccessToken && string.IsNullOrEmpty(context.AccessToken)) ||
(context.RequireActorToken && string.IsNullOrEmpty(context.ActorToken)) ||
(context.RequireAuthorizationCode && string.IsNullOrEmpty(context.AuthorizationCode)) ||
(context.RequireClientAssertion && string.IsNullOrEmpty(context.ClientAssertion)) ||
(context.RequireDeviceCode && string.IsNullOrEmpty(context.DeviceCode)) ||
@ -551,6 +596,7 @@ public static partial class OpenIddictServerHandlers
(context.RequireIdentityToken && string.IsNullOrEmpty(context.IdentityToken)) ||
(context.RequireRefreshToken && string.IsNullOrEmpty(context.RefreshToken)) ||
(context.RequireRequestToken && string.IsNullOrEmpty(context.RequestToken)) ||
(context.RequireSubjectToken && string.IsNullOrEmpty(context.SubjectToken)) ||
(context.RequireUserCode && string.IsNullOrEmpty(context.UserCode)))
{
context.Reject(
@ -607,7 +653,9 @@ public static partial class OpenIddictServerHandlers
Token = context.ClientAssertion,
TokenFormat = context.ClientAssertionType switch
{
ClientAssertionTypes.JwtBearer => TokenFormats.Private.JsonWebToken,
ClientAssertionTypes.JwtBearer => TokenFormats.Private.JsonWebToken,
ClientAssertionTypes.Saml2Bearer => TokenFormats.Private.Saml2,
_ => null
},
ValidTokenTypes = { TokenTypeIdentifiers.Private.ClientAssertion }
@ -931,36 +979,19 @@ public static partial class OpenIddictServerHandlers
return true;
}
// If the current request is a device request, consider the audience valid
// if the address matches one of the URIs assigned to the device authorization endpoint.
if (context.EndpointType is OpenIddictServerEndpointType.DeviceAuthorization &&
MatchesAnyUri(uri, context.Options.DeviceAuthorizationEndpointUris))
{
return true;
}
// If the current request is an introspection request, consider the audience valid
// if the address matches one of the URIs assigned to the introspection endpoint.
else if (context.EndpointType is OpenIddictServerEndpointType.Introspection &&
MatchesAnyUri(uri, context.Options.IntrospectionEndpointUris))
// Consider the audience valid if it matches one of the URIs
// assigned to the endpoint that received the request.
switch (context.EndpointType)
{
return true;
}
// If the current request is a pushed authorization request, consider the audience valid
// if the address matches one of the URIs assigned to the pushed authorization endpoint.
else if (context.EndpointType is OpenIddictServerEndpointType.PushedAuthorization &&
MatchesAnyUri(uri, context.Options.PushedAuthorizationEndpointUris))
{
return true;
}
// If the current request is a revocation request, consider the audience valid
// if the address matches one of the URIs assigned to the revocation endpoint.
else if (context.EndpointType is OpenIddictServerEndpointType.Revocation &&
MatchesAnyUri(uri, context.Options.RevocationEndpointUris))
{
return true;
case OpenIddictServerEndpointType.DeviceAuthorization
when MatchesAnyUri(uri, context.Options.DeviceAuthorizationEndpointUris):
case OpenIddictServerEndpointType.Introspection
when MatchesAnyUri(uri, context.Options.IntrospectionEndpointUris):
case OpenIddictServerEndpointType.PushedAuthorization
when MatchesAnyUri(uri, context.Options.PushedAuthorizationEndpointUris):
case OpenIddictServerEndpointType.Revocation
when MatchesAnyUri(uri, context.Options.RevocationEndpointUris):
return true;
}
}
@ -1527,7 +1558,8 @@ public static partial class OpenIddictServerHandlers
DisableAudienceValidation = true,
// Presenter validation is disabled for the token endpoint as this endpoint
// implements a specialized event handler that uses more complex rules.
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token &&
context.Request.IsAuthorizationCodeGrantType(),
Token = context.AuthorizationCode,
ValidTokenTypes = { TokenTypeIdentifiers.Private.AuthorizationCode }
};
@ -1608,7 +1640,8 @@ public static partial class OpenIddictServerHandlers
DisableAudienceValidation = true,
// Presenter validation is disabled for the token endpoint as this endpoint
// implements a specialized event handler that uses more complex rules.
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token &&
context.Request.IsDeviceCodeGrantType(),
Token = context.DeviceCode,
ValidTokenTypes = { TokenTypeIdentifiers.Private.DeviceCode }
};
@ -1874,7 +1907,8 @@ public static partial class OpenIddictServerHandlers
DisableAudienceValidation = true,
// Presenter validation is disabled for the token endpoint as this endpoint
// implements a specialized event handler that uses more complex rules.
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token,
DisablePresenterValidation = context.EndpointType is OpenIddictServerEndpointType.Token &&
context.Request.IsRefreshTokenGrantType(),
Token = context.RefreshToken,
ValidTokenTypes = { TokenTypeIdentifiers.RefreshToken }
};
@ -1916,6 +1950,224 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for validating the actor token resolved from the context.
/// </summary>
public sealed class ValidateSubjectToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictServerDispatcher _dispatcher;
public ValidateSubjectToken(IOpenIddictServerDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireSubjectTokenValidated>()
.UseScopedHandler<ValidateSubjectToken>()
.SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.SubjectToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
DisableAudienceValidation = context.SubjectTokenType switch
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0493)),
// Audience validation is disabled for the access tokens, identity tokens and
// refresh tokens used as subject tokens and received by the token endpoint as this
// endpoint implements a specialized event handler that uses more complex rules.
TokenTypeIdentifiers.AccessToken or
TokenTypeIdentifiers.IdentityToken or TokenTypeIdentifiers.RefreshToken
when context.EndpointType is OpenIddictServerEndpointType.Token &&
context.Request.IsTokenExchangeGrantType() => true,
// Other types of tokens (e.g generic JWT assertions) are not supported by the specialized
// event handler and are expected to be validated using the regular token validation routine.
_ => false,
},
DisablePresenterValidation = context.SubjectTokenType switch
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0493)),
// Presenter validation is disabled for the access tokens, identity tokens and
// refresh tokens used as subject tokens and received by the token endpoint as this
// endpoint implements a specialized event handler that uses more complex rules.
TokenTypeIdentifiers.AccessToken or
TokenTypeIdentifiers.IdentityToken or TokenTypeIdentifiers.RefreshToken
when context.EndpointType is OpenIddictServerEndpointType.Token &&
context.Request.IsTokenExchangeGrantType() => true,
// Other types of tokens (e.g generic JWT assertions) are not supported by the specialized
// event handler and are expected to be validated using the regular token validation routine.
_ => false,
},
Token = context.SubjectToken,
ValidTokenTypes = { context.SubjectTokenType }
};
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
if (context.RejectSubjectToken)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
return;
}
context.SubjectTokenPrincipal = notification.Principal;
}
}
/// <summary>
/// Contains the logic responsible for validating the actor token resolved from the context.
/// </summary>
public sealed class ValidateActorToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictServerDispatcher _dispatcher;
public ValidateActorToken(IOpenIddictServerDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireActorTokenValidated>()
.UseScopedHandler<ValidateActorToken>()
.SetOrder(ValidateSubjectToken.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.ActorToken))
{
return;
}
var notification = new ValidateTokenContext(context.Transaction)
{
DisableAudienceValidation = context.ActorTokenType switch
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0494)),
// Audience validation is disabled for the access tokens, identity tokens and
// refresh tokens used as actor tokens and received by the token endpoint as this
// endpoint implements a specialized event handler that uses more complex rules.
TokenTypeIdentifiers.AccessToken or
TokenTypeIdentifiers.IdentityToken or TokenTypeIdentifiers.RefreshToken
when context.EndpointType is OpenIddictServerEndpointType.Token &&
context.Request.IsTokenExchangeGrantType() => true,
// Other types of tokens (e.g generic JWT assertions) are not supported by the specialized
// event handler and are expected to be validated using the regular token validation routine.
_ => false,
},
DisablePresenterValidation = context.ActorTokenType switch
{
null or { Length: 0 } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0494)),
// Presenter validation is disabled for the access tokens, identity tokens and
// refresh tokens used as actor tokens and received by the token endpoint as this
// endpoint implements a specialized event handler that uses more complex rules.
TokenTypeIdentifiers.AccessToken or
TokenTypeIdentifiers.IdentityToken or TokenTypeIdentifiers.RefreshToken
when context.EndpointType is OpenIddictServerEndpointType.Token &&
context.Request.IsTokenExchangeGrantType() => true,
// Other types of tokens (e.g generic JWT assertions) are not supported by the specialized
// event handler and are expected to be validated using the regular token validation routine.
_ => false,
},
Token = context.ActorToken,
ValidTokenTypes = { context.ActorTokenType }
};
if (!string.IsNullOrEmpty(context.ClientId))
{
notification.ValidPresenters.Add(context.ClientId);
}
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
if (context.RejectActorToken)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
return;
}
context.ActorTokenPrincipal = notification.Principal;
}
}
/// <summary>
/// Contains the logic responsible for validating the user code resolved from the context.
/// </summary>
@ -1933,7 +2185,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireUserCodeValidated>()
.UseScopedHandler<ValidateUserCode>()
.SetOrder(ValidateRefreshToken.Descriptor.Order + 1_000)
.SetOrder(ValidateActorToken.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -2036,7 +2288,7 @@ public static partial class OpenIddictServerHandlers
_ => null
};
if (principal?.GetClaim(Claims.Private.HostProperties) is string value && !string.IsNullOrEmpty(value))
if (principal?.GetClaim(Claims.Private.HostProperties) is { Length: > 0 } value)
{
using var document = JsonDocument.Parse(value);
@ -2516,6 +2768,12 @@ public static partial class OpenIddictServerHandlers
ClaimValueTypes.Integer64 or ClaimValueTypes.Double or
ClaimValueTypes.UInteger32 or ClaimValueTypes.UInteger64 }],
// The following claims MUST be represented as unique JSON objects.
Claims.Actor or Claims.Address or Claims.AuthorizedActor
=> values is [{ ValueType: JsonClaimValueTypes.Json, Value: string value }] &&
JsonSerializer.Deserialize(value, OpenIddictSerializer.Default.JsonElement)
is { ValueKind: JsonValueKind.Object },
// Claims that are not in the well-known list can be of any type.
_ => true
};
@ -2598,8 +2856,8 @@ public static partial class OpenIddictServerHandlers
return;
}
// Extract the token identifier from the authentication principal.
// If no token identifier can be found, this indicates that the token has no backing database entry.
// Extract the token identifier from the authentication principal. If no token identifier
// can be found, this indicates that the token has no backing database entry.
var identifier = principal.GetTokenId();
if (string.IsNullOrEmpty(identifier))
{
@ -2868,7 +3126,7 @@ public static partial class OpenIddictServerHandlers
context.Principal.SetResources(context.Principal.GetAudiences());
}
// Reset the audiences collection, as it's later set, based on the token type.
// Reset the audiences collection, as it's set later, based on the token type.
context.Principal.SetAudiences([]);
return default;
@ -2915,8 +3173,11 @@ public static partial class OpenIddictServerHandlers
OpenIddictServerEndpointType.Authorization when context.Request.HasResponseType(ResponseTypes.Token)
=> (true, true),
// For token requests, always generate and return an access token.
OpenIddictServerEndpointType.Token => (true, true),
// For token requests, always generate and return an access token, except when token exchange
// is used: in that case, the "access_token" parameter returned as part of the token response
// is determined by the "requested_token_type" parameter and may not be an access token (that
// token is named "issued token" but is returned using the standard "access_token" parameter).
OpenIddictServerEndpointType.Token when !context.Request.IsTokenExchangeGrantType() => (true, true),
_ => (false, false)
};
@ -2967,12 +3228,34 @@ public static partial class OpenIddictServerHandlers
context.Principal.HasScope(Scopes.OpenId) &&
context.Request.HasResponseType(ResponseTypes.IdToken) => (true, true),
// For token requests, only generate and return an identity token if the openid scope was granted.
// For token requests using the OAuth 2.0 Token Exchange grant, never return an identity token as-is:
// clients that need to retrieve an identity token can explicitly request an identity token using the
// standard "requested_token_type" parameter. In that case, the identity token will be returned via
// the "access_token", as defined and required by the OAuth 2.0 Token Exchange specification.
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType() => (false, false),
// For token requests using other grant types (even for those that don't define the id_token as a
// standard concept), only generate and return an identity token if the openid scope was granted.
OpenIddictServerEndpointType.Token when context.Principal.HasScope(Scopes.OpenId) => (true, true),
_ => (false, false)
};
(context.GenerateIssuedToken, context.IncludeIssuedToken, context.IssuedTokenType) = context.EndpointType switch
{
// For token exchange requests, generate an issued token whose type is determined
// dynamically by the caller when the "requested_token_type" parameter is present.
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType() &&
context.Request.RequestedTokenType is { Length: > 0 } type => (true, true, type),
// For token exchange requests that don't specify an explicit token type, generate
// an issued token using the default requested token type set in the server options.
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType()
=> (true, true, context.Options.DefaultRequestedTokenType),
_ => (false, false, null)
};
(context.GenerateRequestToken, context.IncludeRequestToken) = context.EndpointType switch
{
// Always generate a request token if request caching was enabled and the
@ -2995,6 +3278,19 @@ public static partial class OpenIddictServerHandlers
(context.GenerateRefreshToken, context.IncludeRefreshToken) = context.EndpointType switch
{
// For token exchange requests, do not generate and return a second refresh
// token if the issued token requested by the client is already a refresh token.
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType() &&
context.Request.RequestedTokenType is TokenTypeIdentifiers.RefreshToken
=> (false, false),
// For token exchange requests that don't specify an explicit token type, do not generate
// a refresh token if the default requested token type is already a refresh token.
OpenIddictServerEndpointType.Token when context.Request.IsTokenExchangeGrantType() &&
string.IsNullOrEmpty(context.Request.RequestedTokenType) &&
context.Options.DefaultRequestedTokenType is TokenTypeIdentifiers.RefreshToken
=> (false, false),
// For token requests, allow a refresh token to be returned
// if the special offline_access protocol scope was granted.
OpenIddictServerEndpointType.Token when context.Principal.HasScope(Scopes.OfflineAccess)
@ -3056,8 +3352,10 @@ public static partial class OpenIddictServerHandlers
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// If no authorization code, device code or refresh token is returned, don't create an authorization.
if (!context.GenerateAuthorizationCode && !context.GenerateDeviceCode && !context.GenerateRefreshToken)
// If no authorization code, device code or refresh token (including when it's represented as an issued
// token during an OAuth 2.0 Token Exchange grant) is returned, don't create an ad-hoc authorization.
if (!context.GenerateAuthorizationCode && !context.GenerateDeviceCode && !context.GenerateRefreshToken &&
(!context.GenerateIssuedToken || context.IssuedTokenType is not TokenTypeIdentifiers.RefreshToken))
{
return;
}
@ -3506,6 +3804,272 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal
/// used to generate the issued token, if one is going to be returned.
/// </summary>
public sealed class PrepareIssuedTokenPrincipal : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictApplicationManager? _applicationManager;
public PrepareIssuedTokenPrincipal(IOpenIddictApplicationManager? applicationManager = null)
=> _applicationManager = applicationManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireIssuedTokenGenerated>()
.UseScopedHandler(static provider =>
{
// Note: the application manager is only resolved if the degraded mode was not enabled to ensure
// invalid core configuration exceptions are not thrown even if the managers were registered.
var options = provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>().CurrentValue;
return options.EnableDegradedMode ?
new PrepareIssuedTokenPrincipal() :
new PrepareIssuedTokenPrincipal(provider.GetService<IOpenIddictApplicationManager>() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)));
})
.SetOrder(PrepareDeviceCodePrincipal.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Create a new principal containing only the filtered claims.
// Actors identities are also filtered (delegation scenarios).
var principal = context.IssuedTokenType switch
{
// Note: unlike other types of tokens, issued tokens always have a type determined at runtime
// (e.g based on the "requested_token_type" parameter sent by the client or chosen by the server).
//
// As such, the claim filter is different depending on the type of token and requires using different
// destinations: "access_token" when the returned token is an access token, "id_token" when it's an
// identity token or "issued_token" when it's any other type, including arbitrary JWT assertions.
TokenTypeIdentifiers.AccessToken => context.Principal.Clone(claim =>
{
// Never exclude the subject and authorization identifier claims.
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never exclude the presenters and scope private claims.
if (string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Always exclude private claims, whose values must generally be kept secret.
if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Claims whose destination is not explicitly referenced or doesn't
// contain "access_token" are not included in the access token.
if (!claim.HasDestination(Destinations.AccessToken))
{
context.Logger.LogDebug(6009, SR.GetResourceString(SR.ID6009), claim.Type);
return false;
}
return true;
}),
TokenTypeIdentifiers.RefreshToken => context.Principal.Clone(claim =>
{
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Other claims are always included in the authorization code, even private claims.
return true;
}),
_ => context.Principal.Clone(claim =>
{
// Never exclude the subject and authorization identifier claims.
if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never exclude the presenters and scope private claims.
if (string.Equals(claim.Type, Claims.Private.Presenter, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.Scope, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Never include the public or internal token identifiers to ensure the identifiers
// that are automatically inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Never include the creation and expiration dates that are automatically
// inherited from the parent token are not reused for the new token.
if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) ||
string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Always exclude private claims, whose values must generally be kept secret.
if (claim.Type.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Claims whose destination is not explicitly referenced or doesn't
// contain "issued_token" are not included in the issued token.
if (!claim.HasDestination(Destinations.IssuedToken))
{
return false;
}
return true;
})
};
// When the issued token is not a refresh token (for which destinations must be preserved
// so they can be reused when the refresh token is extracted and used to create another
// set of tokens), remove the destinations from the claim properties.
if (context.IssuedTokenType is not TokenTypeIdentifiers.RefreshToken)
{
foreach (var claim in principal.Claims)
{
claim.Properties.Remove(Properties.Destinations);
}
}
principal.SetCreationDate(context.Options.TimeProvider.GetUtcNow());
// If a specific token lifetime was attached to the principal, prefer it over any other value.
var lifetime = context.IssuedTokenType switch
{
TokenTypeIdentifiers.AccessToken => context.Principal.GetAccessTokenLifetime(),
TokenTypeIdentifiers.IdentityToken => context.Principal.GetIdentityTokenLifetime(),
TokenTypeIdentifiers.RefreshToken => context.Principal.GetRefreshTokenLifetime(),
_ => context.Principal.GetIssuedTokenLifetime()
};
// If the client to which the token is returned is known, use the attached setting if available.
if (lifetime is null && !context.Options.EnableDegradedMode && !string.IsNullOrEmpty(context.ClientId))
{
if (_applicationManager is null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0016));
}
var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0017));
var name = context.IssuedTokenType switch
{
TokenTypeIdentifiers.AccessToken => Settings.TokenLifetimes.AccessToken,
TokenTypeIdentifiers.IdentityToken => Settings.TokenLifetimes.IdentityToken,
TokenTypeIdentifiers.RefreshToken => Settings.TokenLifetimes.RefreshToken,
_ => Settings.TokenLifetimes.IssuedToken
};
var settings = await _applicationManager.GetSettingsAsync(application);
if (settings.TryGetValue(name, out string? setting) &&
TimeSpan.TryParse(setting, CultureInfo.InvariantCulture, out var value))
{
lifetime = value;
}
}
// Otherwise, fall back to the global value.
lifetime ??= context.IssuedTokenType switch
{
TokenTypeIdentifiers.AccessToken => context.Options.AccessTokenLifetime,
TokenTypeIdentifiers.IdentityToken => context.Options.IdentityTokenLifetime,
TokenTypeIdentifiers.RefreshToken => context.Options.RefreshTokenLifetime,
_ => context.Options.IssuedTokenLifetime
};
if (lifetime.HasValue)
{
principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value);
}
// Use the server identity as the token issuer.
principal.SetClaim(Claims.Private.Issuer, (context.Options.Issuer ?? context.BaseUri)?.AbsoluteUri);
// Set the audiences based on the resource claims stored in the principal.
principal.SetAudiences(context.Principal.GetResources());
// When the issued token is an identity token, use the client_id as the authorized party.
if (context.IssuedTokenType is TokenTypeIdentifiers.IdentityToken && !string.IsNullOrEmpty(context.ClientId))
{
principal.SetClaim(Claims.AuthorizedParty, context.ClientId);
}
// When the issued token is not a refresh token, store the client identifier in the public client_id
// claim, if available. See https://datatracker.ietf.org/doc/html/rfc9068 for more information.
if (context.IssuedTokenType is not TokenTypeIdentifiers.RefreshToken)
{
principal.SetClaim(Claims.ClientId, context.ClientId);
}
context.IssuedTokenPrincipal = principal;
}
}
/// <summary>
/// Contains the logic responsible for preparing and attaching the claims principal used
/// to generate the request token, if one is going to be returned.
@ -3622,7 +4186,7 @@ public static partial class OpenIddictServerHandlers
//
// Note: parameters used for client authentication are deliberately filtered out.
var parameters = from parameter in context.Request.GetParameters()
where parameter.Key is not (Parameters.ClientAssertion or
where parameter.Key is not (Parameters.ClientAssertion or
Parameters.ClientAssertionType or
Parameters.ClientSecret)
select parameter;
@ -4241,6 +4805,85 @@ public static partial class OpenIddictServerHandlers
}
}
/// <summary>
/// Contains the logic responsible for generating an issued token for the current sign-in operation.
/// </summary>
public sealed class GenerateIssuedToken : IOpenIddictServerHandler<ProcessSignInContext>
{
private readonly IOpenIddictServerDispatcher _dispatcher;
public GenerateIssuedToken(IOpenIddictServerDispatcher dispatcher)
=> _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireIssuedTokenGenerated>()
.UseScopedHandler<GenerateIssuedToken>()
.SetOrder(GenerateDeviceCode.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ProcessSignInContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var notification = new GenerateTokenContext(context.Transaction)
{
ClientId = context.ClientId,
CreateTokenEntry = !context.Options.DisableTokenStorage,
IsReferenceToken = context.IssuedTokenType switch
{
TokenTypeIdentifiers.AccessToken => context.Options.UseReferenceAccessTokens,
TokenTypeIdentifiers.RefreshToken => context.Options.UseReferenceRefreshTokens,
_ => false
},
PersistTokenPayload = context.IssuedTokenType switch
{
TokenTypeIdentifiers.AccessToken => context.Options.UseReferenceAccessTokens,
TokenTypeIdentifiers.RefreshToken => context.Options.UseReferenceRefreshTokens,
_ => false
},
Principal = context.IssuedTokenPrincipal!,
TokenFormat = TokenFormats.Private.JsonWebToken,
TokenType = context.IssuedTokenType!
};
await _dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
return;
}
else if (notification.IsRequestSkipped)
{
context.SkipRequest();
return;
}
else if (notification.IsRejected)
{
context.Reject(
error: notification.Error ?? Errors.InvalidRequest,
description: notification.ErrorDescription,
uri: notification.ErrorUri);
return;
}
context.IssuedToken = notification.Token;
}
}
/// <summary>
/// Contains the logic responsible for generating a request token for the current sign-in operation.
/// </summary>
@ -4258,7 +4901,7 @@ public static partial class OpenIddictServerHandlers
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSignInContext>()
.AddFilter<RequireRequestTokenGenerated>()
.UseScopedHandler<GenerateRequestToken>()
.SetOrder(GenerateDeviceCode.Descriptor.Order + 1_000)
.SetOrder(GenerateIssuedToken.Descriptor.Order + 1_000)
.SetType(OpenIddictServerHandlerType.BuiltIn)
.Build();
@ -4845,6 +5488,34 @@ public static partial class OpenIddictServerHandlers
context.Response.IdToken = context.IdentityToken;
}
if (context.IncludeIssuedToken)
{
if (context.IncludeAccessToken)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0484));
}
// Note: the OAuth 2.0 token exchange specification deliberately reuses the standard "access_token" parameter
// to return the issued token (even when it's not an access token). In that case, the "token_type" node
// is set to "N_A" to indicate when the token used as the "access_token" parameter is not an access token.
context.Response.AccessToken = context.IssuedToken;
context.Response.IssuedTokenType = context.IssuedTokenType;
context.Response.TokenType = context.IssuedTokenType is TokenTypeIdentifiers.AccessToken ?
TokenTypes.Bearer : TokenTypes.NotApplicable;
// If the principal is available, attach additional metadata.
if (context.IssuedTokenPrincipal is not null)
{
// If an expiration date was set on the access token principal, return it to the client application.
if (context.IssuedTokenPrincipal.GetExpirationDate() is DateTimeOffset date &&
date > context.Options.TimeProvider.GetUtcNow())
{
context.Response.ExpiresIn = (long)
((date - context.Options.TimeProvider.GetUtcNow()).TotalSeconds + .5);
}
}
}
if (context.IncludeRequestToken)
{
if (context.EndpointType is OpenIddictServerEndpointType.Authorization or

54
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -210,6 +210,17 @@ public sealed class OpenIddictServerOptions
/// </summary>
public TimeSpan? IdentityTokenLifetime { get; set; } = TimeSpan.FromMinutes(20);
/// <summary>
/// Gets or sets the period of time issued tokens remain valid after being issued. The default value is 1 hour.
/// The client application is expected to refresh or acquire a new issued token after the token has expired.
/// While not recommended, this property can be set to <see langword="null"/> to issue issued tokens that never expire.
/// </summary>
/// <remarks>
/// Note: this property is not used when the requested token type is recognized and matches a token type internally
/// supported (e.g access token): in that case, the dedicated lifetime option is used instead of this value.
/// </remarks>
public TimeSpan? IssuedTokenLifetime { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Gets or sets the period of time request tokens remain valid after being issued. The default value is 1 hour.
/// The client application is expected to start a whole new authentication flow after the request token has expired.
@ -281,7 +292,7 @@ public sealed class OpenIddictServerOptions
/// Note: the list is automatically sorted based on the order assigned to each handler descriptor.
/// As such, it MUST NOT be mutated after options initialization to preserve the exact order.
/// </summary>
public List<OpenIddictServerHandlerDescriptor> Handlers { get; } = new(OpenIddictServerHandlers.DefaultHandlers);
public List<OpenIddictServerHandlerDescriptor> Handlers { get; } = [.. OpenIddictServerHandlers.DefaultHandlers];
/// <summary>
/// Gets or sets a boolean determining whether client identification is optional.
@ -364,6 +375,17 @@ public sealed class OpenIddictServerOptions
/// </summary>
public bool EnableEndSessionRequestCaching { get; set; }
/// <summary>
/// Gets the OAuth 2.0 token exchange actor token types enabled for this application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> ActorTokenTypes { get; } = new(StringComparer.Ordinal)
{
OpenIddictConstants.TokenTypeIdentifiers.AccessToken,
OpenIddictConstants.TokenTypeIdentifiers.IdentityToken,
OpenIddictConstants.TokenTypeIdentifiers.RefreshToken
};
/// <summary>
/// Gets the OAuth 2.0 client assertion types enabled for this application.
/// </summary>
@ -414,6 +436,15 @@ public sealed class OpenIddictServerOptions
OpenIddictConstants.PromptValues.SelectAccount
};
/// <summary>
/// Gets the OAuth 2.0 token exchange requested token types enabled for this application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> RequestedTokenTypes { get; } = new(StringComparer.Ordinal)
{
OpenIddictConstants.TokenTypeIdentifiers.AccessToken
};
/// <summary>
/// Gets or sets a boolean indicating whether PKCE must be used by client applications
/// when requesting an authorization code (e.g when using the code or hybrid flows).
@ -447,6 +478,17 @@ public sealed class OpenIddictServerOptions
OpenIddictConstants.ResponseModes.Query
};
/// <summary>
/// Gets the OAuth 2.0 token exchange subject token types enabled for this application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public HashSet<string> SubjectTokenTypes { get; } = new(StringComparer.Ordinal)
{
OpenIddictConstants.TokenTypeIdentifiers.AccessToken,
OpenIddictConstants.TokenTypeIdentifiers.IdentityToken,
OpenIddictConstants.TokenTypeIdentifiers.RefreshToken
};
/// <summary>
/// Gets the OpenID Connect subject types enabled for this application.
/// </summary>
@ -456,6 +498,16 @@ public sealed class OpenIddictServerOptions
OpenIddictConstants.SubjectTypes.Public
};
/// <summary>
/// Gets or sets the default token type that is used as the requested token type when no
/// explicit value is requested by the client during an OAuth 2.0 token exchange flow.
/// </summary>
/// <remarks>
/// By default, an access token is always returned when no explicit value is requested.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public string DefaultRequestedTokenType { get; set; } = TokenTypeIdentifiers.AccessToken;
/// <summary>
/// Gets or sets a boolean indicating whether endpoint permissions should be ignored.
/// Setting this property to <see langword="true"/> is NOT recommended.

6
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreConstants.cs

@ -11,12 +11,6 @@ namespace OpenIddict.Validation.AspNetCore;
/// </summary>
public static class OpenIddictValidationAspNetCoreConstants
{
public static class Cache
{
public const string AuthorizationRequest = "openiddict-authorization-request:";
public const string EndSessionRequest = "openiddict-logout-request:";
}
public static class Properties
{
public const string AccessTokenPrincipal = ".access_token_principal";

7
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinConstants.cs

@ -11,12 +11,6 @@ namespace OpenIddict.Validation.Owin;
/// </summary>
public static class OpenIddictValidationOwinConstants
{
public static class Cache
{
public const string AuthorizationRequest = "openiddict-authorization-request:";
public const string EndSessionRequest = "openiddict-logout-request:";
}
public static class Headers
{
public const string Authorization = "Authorization";
@ -30,7 +24,6 @@ public static class OpenIddictValidationOwinConstants
public static class Properties
{
public const string AccessTokenPrincipal = ".access_token_principal";
public const string Error = ".error";
public const string ErrorDescription = ".error_description";
public const string ErrorUri = ".error_uri";

6
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -672,7 +672,8 @@ public static partial class OpenIddictValidationHandlers
// If a "token_usage" claim can be extracted from the principal, use it to determine whether
// the token details returned by the authorization server correspond to an access token.
var usage = context.AccessTokenPrincipal.GetClaim(Claims.TokenUsage);
if (!string.IsNullOrEmpty(usage) && usage is not "access_token")
if (!string.IsNullOrEmpty(usage) &&
!string.Equals(usage, "access_token", StringComparison.OrdinalIgnoreCase))
{
context.Reject(
error: Errors.InvalidToken,
@ -682,8 +683,7 @@ public static partial class OpenIddictValidationHandlers
return default;
}
// Note: if no token usage could be resolved, the token is assumed to be an access token.
context.AccessTokenPrincipal = context.AccessTokenPrincipal.SetTokenType(usage ?? TokenTypeIdentifiers.AccessToken);
context.AccessTokenPrincipal.SetTokenType(TokenTypeIdentifiers.AccessToken);
return default;
}

2
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -69,7 +69,7 @@ public sealed class OpenIddictValidationOptions
/// Note: the list is automatically sorted based on the order assigned to each handler descriptor.
/// As such, it MUST NOT be mutated after options initialization to preserve the exact order.
/// </summary>
public List<OpenIddictValidationHandlerDescriptor> Handlers { get; } = new(OpenIddictValidationHandlers.DefaultHandlers);
public List<OpenIddictValidationHandlerDescriptor> Handlers { get; } = [.. OpenIddictValidationHandlers.DefaultHandlers];
/// <summary>
/// Gets or sets the type of validation used by the OpenIddict validation services.

8
src/OpenIddict.Validation/OpenIddictValidationService.cs

@ -44,7 +44,7 @@ public class OpenIddictValidationService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -92,7 +92,7 @@ public class OpenIddictValidationService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -217,7 +217,7 @@ public class OpenIddictValidationService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();
@ -357,7 +357,7 @@ public class OpenIddictValidationService
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
// directly depend on scoped services like the event dispatcher. To work around
// this limitation, a scope is manually created for each method to this service.
await using var scope = _provider.CreateAsyncScope();

4099
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Exchange.cs

File diff suppressed because it is too large

3
test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs

@ -4116,7 +4116,8 @@ public abstract partial class OpenIddictServerIntegrationTests
.AllowImplicitFlow()
.AllowNoneFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
.AllowRefreshTokenFlow()
.AllowTokenExchangeFlow();
// Accept anonymous clients by default.
options.AcceptAnonymousClients();

48
test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs

@ -638,6 +638,22 @@ public class OpenIddictServerBuilderTests
Assert.Contains(GrantTypes.RefreshToken, options.GrantTypes);
}
[Fact]
public void AllowTokenExchangeFlow_TokenExchangeFlowIsAdded()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.AllowTokenExchangeFlow();
var options = GetOptions(services);
// Assert
Assert.Contains(GrantTypes.TokenExchange, options.GrantTypes);
}
[Fact]
public void DisableAccessTokenEncryption_AccessTokenEncryptionIsDisabled()
{
@ -1702,6 +1718,38 @@ public class OpenIddictServerBuilderTests
Assert.Null(options.IdentityTokenLifetime);
}
[Fact]
public void SetIssuedTokenLifetime_DefaultIssuedTokenLifetimeIsReplaced()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetIssuedTokenLifetime(TimeSpan.FromMinutes(42));
var options = GetOptions(services);
// Assert
Assert.Equal(TimeSpan.FromMinutes(42), options.IssuedTokenLifetime);
}
[Fact]
public void SetIssuedTokenLifetime_IssuedTokenLifetimeCanBeSetToNull()
{
// Arrange
var services = CreateServices();
var builder = CreateBuilder(services);
// Act
builder.SetIssuedTokenLifetime(null);
var options = GetOptions(services);
// Assert
Assert.Null(options.IssuedTokenLifetime);
}
[Fact]
public void SetDeviceCodeLifetimeLifetime_DefaultDeviceCodeLifetimeIsReplaced()
{

Loading…
Cancel
Save