Browse Source

Add device code flow support

pull/844/head
Kévin Chalet 6 years ago
parent
commit
225b8ad9f5
  1. 165
      samples/Mvc.Server/Controllers/AuthorizationController.cs
  2. 9
      samples/Mvc.Server/Startup.cs
  3. 7
      samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs
  4. 11
      samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs
  5. 26
      samples/Mvc.Server/ViewModels/Authorization/VerifyViewModel.cs
  6. 7
      samples/Mvc.Server/Views/Authorization/Authorize.cshtml
  7. 6
      samples/Mvc.Server/Views/Authorization/Logout.cshtml
  8. 49
      samples/Mvc.Server/Views/Authorization/Verify.cshtml
  9. 31
      src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs
  10. 31
      src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs
  11. 21
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  12. 137
      src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
  13. 63
      src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs
  14. 18
      src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs
  15. 18
      src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs
  16. 100
      src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs
  17. 125
      src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs
  18. 3
      src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs
  19. 3
      src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs
  20. 3
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs
  21. 3
      src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs
  22. 5
      src/OpenIddict.NHibernate/Mappings/OpenIddictAuthorizationMapping.cs
  23. 5
      src/OpenIddict.NHibernate/Mappings/OpenIddictTokenMapping.cs
  24. 10
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs
  25. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
  26. 52
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  27. 22
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs
  28. 34
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs
  29. 48
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs
  30. 12
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs
  31. 107
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  32. 8
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs
  33. 2
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs
  34. 2
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs
  35. 727
      src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs
  36. 10
      src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs
  37. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
  38. 62
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  39. 22
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs
  40. 34
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs
  41. 47
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs
  42. 12
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs
  43. 110
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  44. 11
      src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs
  45. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs
  46. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs
  47. 148
      src/OpenIddict.Server/OpenIddictServerBuilder.cs
  48. 102
      src/OpenIddict.Server/OpenIddictServerConfiguration.cs
  49. 5
      src/OpenIddict.Server/OpenIddictServerConstants.cs
  50. 12
      src/OpenIddict.Server/OpenIddictServerEndpointType.cs
  51. 14
      src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs
  52. 151
      src/OpenIddict.Server/OpenIddictServerEvents.Device.cs
  53. 5
      src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs
  54. 7
      src/OpenIddict.Server/OpenIddictServerEvents.Session.cs
  55. 46
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  56. 5
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  57. 54
      src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs
  58. 128
      src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs
  59. 1205
      src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs
  60. 12
      src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs
  61. 123
      src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs
  62. 20
      src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs
  63. 26
      src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs
  64. 129
      src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs
  65. 20
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  66. 2191
      src/OpenIddict.Server/OpenIddictServerHandlers.cs
  67. 80
      src/OpenIddict.Server/OpenIddictServerHelpers.cs
  68. 44
      src/OpenIddict.Server/OpenIddictServerOptions.cs
  69. 131
      src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs
  70. 16
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs
  71. 11
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs
  72. 1
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddlewareFactory.cs
  73. 2
      src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs
  74. 2
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  75. 15
      src/OpenIddict.Validation/OpenIddictValidationBuilder.cs
  76. 16
      src/OpenIddict.Validation/OpenIddictValidationConstants.cs
  77. 5
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  78. 3
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  79. 22
      src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs
  80. 155
      src/OpenIddict.Validation/OpenIddictValidationHandlers.cs
  81. 14
      src/OpenIddict.Validation/OpenIddictValidationOptions.cs

165
samples/Mvc.Server/Controllers/AuthorizationController.cs

@ -17,7 +17,6 @@ using Microsoft.AspNetCore.Mvc;
using Mvc.Server.Helpers;
using Mvc.Server.Models;
using Mvc.Server.ViewModels.Authorization;
using Mvc.Server.ViewModels.Shared;
using OpenIddict.Abstractions;
using OpenIddict.Core;
using OpenIddict.EntityFrameworkCore.Models;
@ -42,36 +41,23 @@ namespace Mvc.Server
_userManager = userManager;
}
#region Authorization code, implicit and implicit flows
#region Authorization code, implicit and hybrid flows
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
[Authorize, HttpGet("~/connect/authorize")]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request == null)
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
}
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null)
{
return View("Error", new ErrorViewModel
{
Error = Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
var application = await _applicationManager.FindByClientIdAsync(request.ClientId) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Parameters = request.GetFlattenedParameters(),
Scope = request.Scope
});
}
@ -80,22 +66,12 @@ namespace Mvc.Server
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request == null)
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
}
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return View("Error", new ErrorViewModel
{
Error = Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
var user = await _userManager.GetUserAsync(User) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
var principal = await _signInManager.CreateUserPrincipalAsync(user);
@ -119,27 +95,114 @@ namespace Mvc.Server
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
public IActionResult Deny() => Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
#endregion
// Note: the logout action is only useful when implementing interactive
// flows like the authorization code flow or the implicit flow.
#region Device flow
// Note: to support the device flow, you must provide your own verification endpoint action:
[Authorize, HttpGet("~/connect/verify")]
public async Task<IActionResult> Verify()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
[HttpGet("~/connect/logout")]
public IActionResult Logout()
// If the user code was not specified in the query string (e.g as part of the verification_uri_complete),
// render a form to ask the user to enter the user code manually (non-digit chars are automatically ignored).
if (string.IsNullOrEmpty(request.UserCode))
{
return View(new VerifyViewModel());
}
// Retrieve the claims principal associated with the user code.
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
if (result.Succeeded)
{
// Retrieve the application details from the database using the client_id stored in the principal.
var application = await _applicationManager.FindByClientIdAsync(result.Principal.GetClaim(Claims.ClientId)) ??
throw new InvalidOperationException("Details concerning the calling client application cannot be found.");
// Render a form asking the user to confirm the authorization demand.
return View(new VerifyViewModel
{
ApplicationName = await _applicationManager.GetDisplayNameAsync(application),
Scope = string.Join(" ", result.Principal.GetScopes()),
UserCode = request.UserCode
});
}
// Redisplay the form when the user code is not valid.
return View(new VerifyViewModel
{
Error = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
ErrorDescription = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription)
});
}
[Authorize, FormValueRequired("submit.Accept")]
[HttpPost("~/connect/verify"), ValidateAntiForgeryToken]
public async Task<IActionResult> VerifyAccept()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request == null)
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User) ??
throw new InvalidOperationException("The user details cannot be retrieved.");
// Retrieve the claims principal associated with the user code.
var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
if (result.Succeeded)
{
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// 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.
principal.SetScopes(result.Principal.GetScopes());
principal.SetResources("resource_server");
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
var properties = new AuthenticationProperties
{
// This property points to the address OpenIddict will automatically
// redirect the user to after validating the authorization demand.
RedirectUri = "/"
};
return SignIn(principal, properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
// Flow the request_id to allow OpenIddict to restore
// the original logout request from the distributed cache.
return View(new LogoutViewModel
// Redisplay the form when the user code is not valid.
return View(new VerifyViewModel
{
Parameters = request.GetFlattenedParameters()
Error = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.Error),
ErrorDescription = result.Properties.GetString(OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription)
});
}
[Authorize, FormValueRequired("submit.Deny")]
[HttpPost("~/connect/verify"), ValidateAntiForgeryToken]
// Notify OpenIddict that the authorization grant has been denied by the resource owner.
public IActionResult VerifyDeny()
{
var properties = new AuthenticationProperties
{
// This property points to the address OpenIddict will automatically
// redirect the user to after rejecting the authorization demand.
RedirectUri = "/"
};
return Forbid(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
#endregion
#region Logout support for interactive flows like code and implicit
// Note: the logout action is only useful when implementing interactive
// flows like the authorization code flow or the implicit flow.
[HttpGet("~/connect/logout")]
public IActionResult Logout() => View();
[ActionName(nameof(Logout)), HttpPost("~/connect/logout"), ValidateAntiForgeryToken]
public async Task<IActionResult> LogoutPost()
{
@ -148,24 +211,26 @@ namespace Mvc.Server
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
var properties = new AuthenticationProperties
{
RedirectUri = "/"
};
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
return SignOut(properties, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
#endregion
#region Password, authorization code and refresh token flows
#region Password, authorization code, device and refresh token flows
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest();
if (request == null)
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
}
if (request.IsPasswordGrantType())
{
@ -211,9 +276,9 @@ namespace Mvc.Server
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
else if (request.IsAuthorizationCodeGrantType() || request.IsDeviceCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/refresh token.
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the user profile corresponding to the authorization code/refresh token.

9
samples/Mvc.Server/Startup.cs

@ -66,13 +66,16 @@ namespace Mvc.Server
{
// Enable the authorization, logout, token and userinfo endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetDeviceEndpointUris("/connect/device")
.SetLogoutEndpointUris("/connect/logout")
.SetTokenEndpointUris("/connect/token")
.SetUserinfoEndpointUris("/connect/userinfo");
.SetUserinfoEndpointUris("/connect/userinfo")
.SetVerificationEndpointUris("/connect/verify");
// Note: the Mvc.Client sample only uses the code flow and the password flow, but you
// can enable the other flows if you need to support implicit or client credentials.
options.AllowAuthorizationCodeFlow()
.AllowDeviceCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow();
@ -92,6 +95,7 @@ namespace Mvc.Server
.EnableLogoutEndpointPassthrough()
.EnableTokenEndpointPassthrough()
.EnableUserinfoEndpointPassthrough()
.EnableVerificationEndpointPassthrough()
.DisableTransportSecurityRequirement(); // During development, you can disable the HTTPS requirement.
// Note: if you don't want to specify a client_id when sending
@ -215,9 +219,12 @@ namespace Mvc.Server
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Device,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.DeviceCode,
OpenIddictConstants.Permissions.GrantTypes.Password,
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
OpenIddictConstants.Permissions.Scopes.Email,
OpenIddictConstants.Permissions.Scopes.Profile,
OpenIddictConstants.Permissions.Scopes.Roles

7
samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs

@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.ComponentModel.DataAnnotations;
namespace Mvc.Server.ViewModels.Authorization
{
@ -9,9 +7,6 @@ namespace Mvc.Server.ViewModels.Authorization
[Display(Name = "Application")]
public string ApplicationName { get; set; }
[BindNever]
public IEnumerable<KeyValuePair<string, string>> Parameters { get; set; }
[Display(Name = "Scope")]
public string Scope { get; set; }
}

11
samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs

@ -1,11 +0,0 @@
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Mvc.Server.ViewModels.Authorization
{
public class LogoutViewModel
{
[BindNever]
public IEnumerable<KeyValuePair<string, string>> Parameters { get; set; }
}
}

26
samples/Mvc.Server/ViewModels/Authorization/VerifyViewModel.cs

@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using OpenIddict.Abstractions;
namespace Mvc.Server.ViewModels.Authorization
{
public class VerifyViewModel
{
[Display(Name = "Application")]
public string ApplicationName { get; set; }
[BindNever, Display(Name = "Error")]
public string Error { get; set; }
[BindNever, Display(Name = "Error description")]
public string ErrorDescription { get; set; }
[Display(Name = "Scope")]
public string Scope { get; set; }
[FromQuery(Name = OpenIddictConstants.Parameters.UserCode)]
[Display(Name = "User code")]
public string UserCode { get; set; }
}
}

7
samples/Mvc.Server/Views/Authorization/Authorize.cshtml

@ -1,4 +1,5 @@
@model AuthorizeViewModel
@using Microsoft.Extensions.Primitives
@model AuthorizeViewModel
<div class="jumbotron">
<h1>Authorization</h1>
@ -6,7 +7,9 @@
<p class="lead text-left">Do you want to grant <strong>@Model.ApplicationName</strong> access to your data? (scopes requested: @Model.Scope)</p>
<form asp-controller="Authorization" asp-action="Authorize" method="post">
@foreach (var parameter in Model.Parameters)
@* Flow the request parameters so they can be received by the Accept/Reject actions: *@
@foreach (var parameter in Context.Request.HasFormContentType ?
(IEnumerable<KeyValuePair<string, StringValues>>) Context.Request.Form : Context.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}

6
samples/Mvc.Server/Views/Authorization/Logout.cshtml

@ -1,11 +1,13 @@
@model LogoutViewModel
@using Microsoft.Extensions.Primitives
<div class="jumbotron">
<h1>Log out</h1>
<p class="lead text-left">Are you sure you want to sign out?</p>
<form asp-controller="Authorization" asp-action="Logout" method="post">
@foreach (var parameter in Model.Parameters)
@* Flow the request parameters so they can be received by the LogoutPost action: *@
@foreach (var parameter in Context.Request.HasFormContentType ?
(IEnumerable<KeyValuePair<string, StringValues>>) Context.Request.Form : Context.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}

49
samples/Mvc.Server/Views/Authorization/Verify.cshtml

@ -0,0 +1,49 @@
@using Microsoft.Extensions.Primitives
@model VerifyViewModel
<div class="jumbotron">
<h1>Authorization</h1>
@if (string.IsNullOrEmpty(Model.UserCode) || !string.IsNullOrEmpty(Model.Error))
{
@if (!string.IsNullOrEmpty(Model.Error) && !string.IsNullOrEmpty(Model.ErrorDescription))
{
<p class="lead text-center alert alert-warning">
An error occurred:
<br />
@Model.ErrorDescription (@Model.Error)
</p>
}
<p class="lead text-left">Enter the user code given by the client application:</p>
<form asp-controller="Authorization" asp-action="Verify" method="get">
<div class="form-check">
<input class="form-control" name="user_code" type="text" />
</div>
<input class="btn btn-lg btn-success" type="submit" value="Submit" />
</form>
}
else
{
<p class="lead text-left">Do you want to grant <strong>@Model.ApplicationName</strong> access to your data? (scopes requested: @Model.Scope)</p>
<p class="lead text-center alert alert-warning">
Make sure that the code displayed on the device is <strong>@Model.UserCode</strong>.
<br />
If the two codes don't match, press "No" to reject the authorization demand.
</p>
<form asp-controller="Authorization" asp-action="Verify" method="post">
@* Flow the request parameters so they can be received by the VerifyAccept/VerifyReject actions: *@
@foreach (var parameter in Context.Request.HasFormContentType ?
(IEnumerable<KeyValuePair<string, StringValues>>) Context.Request.Form : Context.Request.Query)
{
<input type="hidden" name="@parameter.Key" value="@parameter.Value" />
}
<input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
<input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />
</form>
}
</div>

31
src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs

@ -270,36 +270,13 @@ namespace OpenIddict.Abstractions
ValueTask<bool> HasScopesAsync([NotNull] object authorization, ImmutableArray<string> scopes, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given authorization is ad hoc.
/// Determines whether a given authorization has the specified status.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The expected status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is ad hoc, <c>false</c> otherwise.</returns>
ValueTask<bool> IsAdHocAsync([NotNull] object authorization, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given authorization is permanent.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is permanent, <c>false</c> otherwise.</returns>
ValueTask<bool> IsPermanentAsync([NotNull] object authorization, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given authorization has been revoked.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization has been revoked, <c>false</c> otherwise.</returns>
ValueTask<bool> IsRevokedAsync([NotNull] object authorization, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given authorization is valid.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is valid, <c>false</c> otherwise.</returns>
ValueTask<bool> IsValidAsync([NotNull] object authorization, CancellationToken cancellationToken = default);
/// <returns><c>true</c> if the authorization has the specified status, <c>false</c> otherwise.</returns>
ValueTask<bool> HasStatusAsync([NotNull] object authorization, [NotNull] string status, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.

31
src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs

@ -296,28 +296,13 @@ namespace OpenIddict.Abstractions
ValueTask<string> GetTypeAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given token has already been redemeed.
/// Determines whether a given token has the specified status.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The expected status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has already been redemeed, <c>false</c> otherwise.</returns>
ValueTask<bool> IsRedeemedAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given token has been revoked.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has been revoked, <c>false</c> otherwise.</returns>
ValueTask<bool> IsRevokedAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Determines whether a given token is valid.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token is valid, <c>false</c> otherwise.</returns>
ValueTask<bool> IsValidAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <returns><c>true</c> if the token has the specified status, <c>false</c> otherwise.</returns>
ValueTask<bool> HasStatusAsync([NotNull] object token, [NotNull] string status, CancellationToken cancellationToken = default);
/// <summary>
/// Executes the specified query and returns all the corresponding elements.
@ -422,6 +407,14 @@ namespace OpenIddict.Abstractions
/// <returns><c>true</c> if the token was successfully redemeed, <c>false</c> otherwise.</returns>
ValueTask<bool> TryRedeemAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to reject a token.
/// </summary>
/// <param name="token">The token to reject.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token was successfully redemeed, <c>false</c> otherwise.</returns>
ValueTask<bool> TryRejectAsync([NotNull] object token, CancellationToken cancellationToken = default);
/// <summary>
/// Tries to revoke a token.
/// </summary>

21
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -93,6 +93,8 @@ namespace OpenIddict.Abstractions
public const string ClaimDestinations = "oi_cl_dstn";
public const string CodeChallenge = "oi_cd_chlg";
public const string CodeChallengeMethod = "oi_cd_chlg_meth";
public const string DeviceCodeId = "oi_dvc_id";
public const string DeviceCodeLifetime = "oi_dvc_lft";
public const string IdentityTokenLifetime = "oi_idt_lft";
public const string Nonce = "oi_nce";
public const string Presenters = "oi_prst";
@ -102,6 +104,7 @@ namespace OpenIddict.Abstractions
public const string Scopes = "oi_scp";
public const string TokenId = "oi_tkn_id";
public const string TokenUsage = "oi_tkn_use";
public const string UserCodeLifetime = "oi_usrc_lft";
}
}
@ -141,7 +144,9 @@ namespace OpenIddict.Abstractions
{
public const string AccessDenied = "access_denied";
public const string AccountSelectionRequired = "account_selection_required";
public const string AuthorizationPending = "authorization_pending";
public const string ConsentRequired = "consent_required";
public const string ExpiredToken = "expired_token";
public const string InteractionRequired = "interaction_required";
public const string InvalidClient = "invalid_client";
public const string InvalidGrant = "invalid_grant";
@ -155,6 +160,7 @@ namespace OpenIddict.Abstractions
public const string RequestNotSupported = "request_not_supported";
public const string RequestUriNotSupported = "request_uri_not_supported";
public const string ServerError = "server_error";
public const string SlowDown = "slow_down";
public const string TemporarilyUnavailable = "temporarily_unavailable";
public const string UnauthorizedClient = "unauthorized_client";
public const string UnsupportedGrantType = "unsupported_grant_type";
@ -166,6 +172,7 @@ namespace OpenIddict.Abstractions
{
public const string AuthorizationCode = "authorization_code";
public const string ClientCredentials = "client_credentials";
public const string DeviceCode = "urn:ietf:params:oauth:grant-type:device_code";
public const string Implicit = "implicit";
public const string Password = "password";
public const string RefreshToken = "refresh_token";
@ -180,6 +187,7 @@ namespace OpenIddict.Abstractions
public const string ClaimsSupported = "claims_supported";
public const string ClaimTypesSupported = "claim_types_supported";
public const string CodeChallengeMethodsSupported = "code_challenge_methods_supported";
public const string DeviceAuthorizationEndpoint = "device_authorization_endpoint";
public const string DisplayValuesSupported = "display_values_supported";
public const string EndSessionEndpoint = "end_session_endpoint";
public const string GrantTypesSupported = "grant_types_supported";
@ -234,6 +242,7 @@ namespace OpenIddict.Abstractions
public const string CodeChallenge = "code_challenge";
public const string CodeChallengeMethod = "code_challenge_method";
public const string CodeVerifier = "code_verifier";
public const string DeviceCode = "device_code";
public const string Display = "display";
public const string Error = "error";
public const string ErrorDescription = "error_description";
@ -266,7 +275,10 @@ namespace OpenIddict.Abstractions
public const string TokenType = "token_type";
public const string TokenTypeHint = "token_type_hint";
public const string UiLocales = "ui_locales";
public const string UserCode = "user_code";
public const string Username = "username";
public const string VerificationUri = "verification_uri";
public const string VerificationUriComplete = "verification_uri_complete";
}
public static class Permissions
@ -274,6 +286,7 @@ namespace OpenIddict.Abstractions
public static class Endpoints
{
public const string Authorization = "ept:authorization";
public const string Device = "ept:device";
public const string Introspection = "ept:introspection";
public const string Logout = "ept:logout";
public const string Revocation = "ept:revocation";
@ -284,6 +297,7 @@ namespace OpenIddict.Abstractions
{
public const string AuthorizationCode = "gt:authorization_code";
public const string ClientCredentials = "gt:client_credentials";
public const string DeviceCode = "gt:urn:ietf:params:oauth:grant-type:device_code";
public const string Implicit = "gt:implicit";
public const string Password = "gt:password";
public const string RefreshToken = "gt:refresh_token";
@ -337,6 +351,7 @@ namespace OpenIddict.Abstractions
public static class Separators
{
public static readonly char[] Ampersand = { '&' };
public static readonly char[] Dash = { '-' };
public static readonly char[] Space = { ' ' };
}
@ -359,7 +374,9 @@ namespace OpenIddict.Abstractions
public static class Statuses
{
public const string Inactive = "inactive";
public const string Redeemed = "redeemed";
public const string Rejected = "rejected";
public const string Revoked = "revoked";
public const string Valid = "valid";
}
@ -374,8 +391,10 @@ namespace OpenIddict.Abstractions
{
public const string AccessToken = "access_token";
public const string AuthorizationCode = "authorization_code";
public const string DeviceCode = "device_code";
public const string IdToken = "id_token";
public const string RefreshToken = "refresh_token";
public const string UserCode = "user_code";
}
public static class TokenTypes
@ -387,8 +406,10 @@ namespace OpenIddict.Abstractions
{
public const string AccessToken = "access_token";
public const string AuthorizationCode = "authorization_code";
public const string DeviceCode = "device_code";
public const string IdToken = "id_token";
public const string RefreshToken = "refresh_token";
public const string UserCode = "user_code";
}
}
}

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

@ -25,41 +25,6 @@ namespace OpenIddict.Abstractions
/// </summary>
public static class OpenIddictExtensions
{
/// <summary>
/// Gets all the parameters associated with the specified message as a flattened collection:
/// array parameters are automatically converted to multiple parameters and parameters that
/// can't be converted to string instances are ignored and excluded from the returned collection.
/// This extension is primarily intended to be used by components that need to represent
/// an OpenID Connect message as a query string or as a list of key/value pairs in a HTTP form.
/// </summary>
/// <param name="message">The <see cref="OpenIddictMessage"/> instance.</param>
/// <returns>The parameters, as a flattened collection.</returns>
public static ImmutableList<KeyValuePair<string, string>> GetFlattenedParameters([NotNull] this OpenIddictMessage message)
{
if (message == null)
{
throw new ArgumentNullException(nameof(message));
}
var parameters = ImmutableList.CreateBuilder<KeyValuePair<string, string>>();
foreach (var parameter in message.GetParameters())
{
var values = (string[]) parameter.Value;
if (values == null)
{
continue;
}
foreach (var value in values)
{
parameters.Add(new KeyValuePair<string, string>(parameter.Key, value));
}
}
return parameters.ToImmutable();
}
/// <summary>
/// Extracts the authentication context class values from an <see cref="OpenIddictRequest"/>.
/// </summary>
@ -506,6 +471,22 @@ namespace OpenIddict.Abstractions
return string.Equals(request.GrantType, GrantTypes.ClientCredentials, StringComparison.Ordinal);
}
/// <summary>
/// Determines whether the "grant_type" parameter corresponds to the device code grant.
/// See https://tools.ietf.org/html/rfc8628 for more information.
/// </summary>
/// <param name="request">The <see cref="OpenIddictRequest"/> instance.</param>
/// <returns><c>true</c> if the request is a device code grant request, <c>false</c> otherwise.</returns>
public static bool IsDeviceCodeGrantType([NotNull] this OpenIddictRequest request)
{
if (request == null)
{
throw new ArgumentNullException(nameof(request));
}
return string.Equals(request.GrantType, GrantTypes.DeviceCode, StringComparison.Ordinal);
}
/// <summary>
/// Determines whether the "grant_type" parameter corresponds to the password grant.
/// See http://tools.ietf.org/html/rfc6749#section-4.3.2 for more information.
@ -1201,6 +1182,33 @@ namespace OpenIddict.Abstractions
return null;
}
/// <summary>
/// Gets the device code lifetime associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns>The device code lifetime or <c>null</c> if the claim cannot be found.</returns>
public static TimeSpan? GetDeviceCodeLifetime([NotNull] this ClaimsPrincipal principal)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
var value = principal.GetClaim(Claims.Private.DeviceCodeLifetime);
if (string.IsNullOrEmpty(value))
{
return null;
}
if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result))
{
return TimeSpan.FromSeconds(result);
}
return null;
}
/// <summary>
/// Gets the identity token lifetime associated with the claims principal.
/// </summary>
@ -1255,6 +1263,33 @@ namespace OpenIddict.Abstractions
return null;
}
/// <summary>
/// Gets the user code lifetime associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <returns>The user code lifetime or <c>null</c> if the claim cannot be found.</returns>
public static TimeSpan? GetUserCodeLifetime([NotNull] this ClaimsPrincipal principal)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
var value = principal.GetClaim(Claims.Private.UserCodeLifetime);
if (string.IsNullOrEmpty(value))
{
return null;
}
if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double result))
{
return TimeSpan.FromSeconds(result);
}
return null;
}
/// <summary>
/// Gets the internal authorization identifier associated with the claims principal.
/// </summary>
@ -1717,6 +1752,22 @@ namespace OpenIddict.Abstractions
return principal.SetClaim(Claims.Private.AuthorizationCodeLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Sets the device code lifetime associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <param name="lifetime">The device code lifetime to store.</param>
/// <returns>The claims principal.</returns>
public static ClaimsPrincipal SetDeviceCodeLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
return principal.SetClaim(Claims.Private.DeviceCodeLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Sets the identity token lifetime associated with the claims principal.
/// </summary>
@ -1749,6 +1800,22 @@ namespace OpenIddict.Abstractions
return principal.SetClaim(Claims.Private.RefreshTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Sets the user code lifetime associated with the claims principal.
/// </summary>
/// <param name="principal">The claims principal.</param>
/// <param name="lifetime">The user code lifetime to store.</param>
/// <returns>The claims principal.</returns>
public static ClaimsPrincipal SetUserCodeLifetime([NotNull] this ClaimsPrincipal principal, TimeSpan? lifetime)
{
if (principal == null)
{
throw new ArgumentNullException(nameof(principal));
}
return principal.SetClaim(Claims.Private.UserCodeLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture));
}
/// <summary>
/// Sets the internal authorization identifier associated with the claims principal.
/// </summary>

63
src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs

@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@ -319,50 +318,50 @@ namespace OpenIddict.Abstractions
{
var builder = new StringBuilder();
using (var writer = new JsonTextWriter(new StringWriter(builder, CultureInfo.InvariantCulture)))
using var writer = new JsonTextWriter(new StringWriter(builder, CultureInfo.InvariantCulture))
{
writer.Formatting = Formatting.Indented;
Formatting = Formatting.Indented
};
writer.WriteStartObject();
writer.WriteStartObject();
foreach (var parameter in Parameters)
{
writer.WritePropertyName(parameter.Key);
// Remove sensitive parameters from the generated payload.
switch (parameter.Key)
{
case OpenIddictConstants.Parameters.AccessToken:
case OpenIddictConstants.Parameters.Assertion:
case OpenIddictConstants.Parameters.ClientAssertion:
case OpenIddictConstants.Parameters.ClientSecret:
case OpenIddictConstants.Parameters.Code:
case OpenIddictConstants.Parameters.IdToken:
case OpenIddictConstants.Parameters.IdTokenHint:
case OpenIddictConstants.Parameters.Password:
case OpenIddictConstants.Parameters.RefreshToken:
case OpenIddictConstants.Parameters.Token:
{
writer.WriteValue("[removed for security reasons]");
continue;
}
}
foreach (var parameter in Parameters)
{
writer.WritePropertyName(parameter.Key);
var token = (JToken) parameter.Value;
if (token == null)
// Remove sensitive parameters from the generated payload.
switch (parameter.Key)
{
case OpenIddictConstants.Parameters.AccessToken:
case OpenIddictConstants.Parameters.Assertion:
case OpenIddictConstants.Parameters.ClientAssertion:
case OpenIddictConstants.Parameters.ClientSecret:
case OpenIddictConstants.Parameters.Code:
case OpenIddictConstants.Parameters.IdToken:
case OpenIddictConstants.Parameters.IdTokenHint:
case OpenIddictConstants.Parameters.Password:
case OpenIddictConstants.Parameters.RefreshToken:
case OpenIddictConstants.Parameters.Token:
{
writer.WriteNull();
writer.WriteValue("[removed for security reasons]");
continue;
}
}
var token = (JToken) parameter.Value;
if (token == null)
{
writer.WriteNull();
token.WriteTo(writer);
continue;
}
writer.WriteEndObject();
token.WriteTo(writer);
}
writer.WriteEndObject();
return builder.ToString();
}
}

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

@ -192,6 +192,15 @@ namespace OpenIddict.Abstractions
set => SetParameter(OpenIddictConstants.Parameters.CodeVerifier, value);
}
/// <summary>
/// Gets or sets the "device_code" parameter.
/// </summary>
public string DeviceCode
{
get => (string) GetParameter(OpenIddictConstants.Parameters.DeviceCode);
set => SetParameter(OpenIddictConstants.Parameters.DeviceCode, value);
}
/// <summary>
/// Gets or sets the "display" parameter.
/// </summary>
@ -408,6 +417,15 @@ namespace OpenIddict.Abstractions
set => SetParameter(OpenIddictConstants.Parameters.UiLocales, value);
}
/// <summary>
/// Gets or sets the "user_code" parameter.
/// </summary>
public string UserCode
{
get => (string) GetParameter(OpenIddictConstants.Parameters.UserCode);
set => SetParameter(OpenIddictConstants.Parameters.UserCode, value);
}
/// <summary>
/// Gets or sets the "username" parameter.
/// </summary>

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

@ -84,6 +84,15 @@ namespace OpenIddict.Abstractions
set => SetParameter(OpenIddictConstants.Parameters.Code, value);
}
/// <summary>
/// Gets or sets the "device_code" parameter.
/// </summary>
public string DeviceCode
{
get => (string) GetParameter(OpenIddictConstants.Parameters.DeviceCode);
set => SetParameter(OpenIddictConstants.Parameters.DeviceCode, value);
}
/// <summary>
/// Gets or sets the "error" parameter.
/// </summary>
@ -164,5 +173,14 @@ namespace OpenIddict.Abstractions
get => (string) GetParameter(OpenIddictConstants.Parameters.TokenType);
set => SetParameter(OpenIddictConstants.Parameters.TokenType, value);
}
/// <summary>
/// Gets or sets the "user_code" parameter.
/// </summary>
public string UserCode
{
get => (string) GetParameter(OpenIddictConstants.Parameters.UserCode);
set => SetParameter(OpenIddictConstants.Parameters.UserCode, value);
}
}
}

100
src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs

@ -702,94 +702,26 @@ namespace OpenIddict.Core
}
/// <summary>
/// Determines whether a given authorization is ad hoc.
/// Determines whether a given authorization has the specified status.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="status">The expected status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is ad hoc, <c>false</c> otherwise.</returns>
public async ValueTask<bool> IsAdHocAsync([NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var type = await GetTypeAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(type))
{
return false;
}
return string.Equals(type, OpenIddictConstants.AuthorizationTypes.AdHoc, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given authorization is permanent.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is permanent, <c>false</c> otherwise.</returns>
public async ValueTask<bool> IsPermanentAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var type = await GetTypeAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(type))
{
return false;
}
return string.Equals(type, OpenIddictConstants.AuthorizationTypes.Permanent, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given authorization has been revoked.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization has been revoked, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> IsRevokedAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var status = await Store.GetStatusAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given authorization is valid.
/// </summary>
/// <param name="authorization">The authorization.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the authorization is valid, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> IsValidAsync(
[NotNull] TAuthorization authorization, CancellationToken cancellationToken = default)
/// <returns><c>true</c> if the authorization has the specified status, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> HasStatusAsync([NotNull] TAuthorization authorization,
[NotNull] string status, CancellationToken cancellationToken = default)
{
if (authorization == null)
{
throw new ArgumentNullException(nameof(authorization));
}
var status = await Store.GetStatusAsync(authorization, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
throw new ArgumentException("The status cannot be null or empty.", nameof(status));
}
return string.Equals(status, OpenIddictConstants.Statuses.Valid, StringComparison.OrdinalIgnoreCase);
return string.Equals(await Store.GetStatusAsync(authorization, cancellationToken), status, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
@ -1077,11 +1009,6 @@ namespace OpenIddict.Core
yield return new ValidationResult("The status cannot be null or empty.");
}
if (string.IsNullOrEmpty(await Store.GetSubjectAsync(authorization, cancellationToken)))
{
yield return new ValidationResult("The subject cannot be null or empty.");
}
// Ensure that the scopes are not null or empty and do not contain spaces.
foreach (var scope in await Store.GetScopesAsync(authorization, cancellationToken))
{
@ -1167,17 +1094,8 @@ namespace OpenIddict.Core
ValueTask<bool> IOpenIddictAuthorizationManager.HasScopesAsync(object authorization, ImmutableArray<string> scopes, CancellationToken cancellationToken)
=> HasScopesAsync((TAuthorization) authorization, scopes, cancellationToken);
ValueTask<bool> IOpenIddictAuthorizationManager.IsAdHocAsync(object authorization, CancellationToken cancellationToken)
=> IsAdHocAsync((TAuthorization) authorization, cancellationToken);
ValueTask<bool> IOpenIddictAuthorizationManager.IsPermanentAsync(object authorization, CancellationToken cancellationToken)
=> IsPermanentAsync((TAuthorization) authorization, cancellationToken);
ValueTask<bool> IOpenIddictAuthorizationManager.IsRevokedAsync(object authorization, CancellationToken cancellationToken)
=> IsRevokedAsync((TAuthorization) authorization, cancellationToken);
ValueTask<bool> IOpenIddictAuthorizationManager.IsValidAsync(object authorization, CancellationToken cancellationToken)
=> IsValidAsync((TAuthorization) authorization, cancellationToken);
ValueTask<bool> IOpenIddictAuthorizationManager.HasStatusAsync(object authorization, string status, CancellationToken cancellationToken)
=> HasStatusAsync((TAuthorization) authorization, status, cancellationToken);
IAsyncEnumerable<object> IOpenIddictAuthorizationManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken)
=> ListAsync(count, offset, cancellationToken).OfType<object>();

125
src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs

@ -736,69 +736,25 @@ namespace OpenIddict.Core
}
/// <summary>
/// Determines whether a given token has already been redemeed.
/// Determines whether a given token has the specified status.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="status">The expected status.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has already been redemeed, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> IsRedeemedAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
/// <returns><c>true</c> if the token has the specified status, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> HasStatusAsync([NotNull] TToken token, [NotNull] string status, CancellationToken cancellationToken = default)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Redeemed, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given token has been revoked.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token has been revoked, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> IsRevokedAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
}
return string.Equals(status, OpenIddictConstants.Statuses.Revoked, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// Determines whether a given token is valid.
/// </summary>
/// <param name="token">The token.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token is valid, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> IsValidAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.IsNullOrEmpty(status))
{
return false;
throw new ArgumentException("The status cannot be null or empty.", nameof(status));
}
return string.Equals(status, OpenIddictConstants.Statuses.Valid, StringComparison.OrdinalIgnoreCase);
return string.Equals(await Store.GetStatusAsync(token, cancellationToken), status, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
@ -1078,6 +1034,54 @@ namespace OpenIddict.Core
}
}
/// <summary>
/// Tries to reject a token.
/// </summary>
/// <param name="token">The token to reject.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns><c>true</c> if the token was successfully redemeed, <c>false</c> otherwise.</returns>
public virtual async ValueTask<bool> TryRejectAsync([NotNull] TToken token, CancellationToken cancellationToken = default)
{
if (token == null)
{
throw new ArgumentNullException(nameof(token));
}
var status = await Store.GetStatusAsync(token, cancellationToken);
if (string.Equals(status, OpenIddictConstants.Statuses.Rejected, StringComparison.OrdinalIgnoreCase))
{
return true;
}
await Store.SetStatusAsync(token, OpenIddictConstants.Statuses.Rejected, cancellationToken);
try
{
await UpdateAsync(token, cancellationToken);
Logger.LogInformation("The token '{Identifier}' was successfully marked as rejected.",
await Store.GetIdAsync(token, cancellationToken));
return true;
}
catch (ConcurrencyException exception)
{
Logger.LogDebug(exception, "A concurrency exception occurred while trying to reject the token '{Identifier}'.",
await Store.GetIdAsync(token, cancellationToken));
return false;
}
catch (Exception exception)
{
Logger.LogWarning(exception, "An exception occurred while trying to reject the token '{Identifier}'.",
await Store.GetIdAsync(token, cancellationToken));
return false;
}
}
/// <summary>
/// Tries to revoke a token.
/// </summary>
@ -1242,7 +1246,9 @@ namespace OpenIddict.Core
else if (!string.Equals(type, OpenIddictConstants.TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.TokenUsages.AuthorizationCode, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase))
!string.Equals(type, OpenIddictConstants.TokenUsages.DeviceCode, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.TokenUsages.RefreshToken, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.TokenUsages.UserCode, StringComparison.OrdinalIgnoreCase))
{
yield return new ValidationResult("The specified token type is not supported by the default token manager.");
}
@ -1252,7 +1258,9 @@ namespace OpenIddict.Core
yield return new ValidationResult("The status cannot be null or empty.");
}
if (string.IsNullOrEmpty(await Store.GetSubjectAsync(token, cancellationToken)))
if (string.IsNullOrEmpty(await Store.GetSubjectAsync(token, cancellationToken)) &&
!string.Equals(type, OpenIddictConstants.TokenUsages.DeviceCode, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(type, OpenIddictConstants.TokenUsages.UserCode, StringComparison.OrdinalIgnoreCase))
{
yield return new ValidationResult("The subject cannot be null or empty.");
}
@ -1355,14 +1363,8 @@ namespace OpenIddict.Core
ValueTask<string> IOpenIddictTokenManager.GetTypeAsync(object token, CancellationToken cancellationToken)
=> GetTypeAsync((TToken) token, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.IsRedeemedAsync(object token, CancellationToken cancellationToken)
=> IsRedeemedAsync((TToken) token, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.IsRevokedAsync(object token, CancellationToken cancellationToken)
=> IsRevokedAsync((TToken) token, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.IsValidAsync(object token, CancellationToken cancellationToken)
=> IsValidAsync((TToken) token, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.HasStatusAsync(object token, string status, CancellationToken cancellationToken)
=> HasStatusAsync((TToken) token, status, cancellationToken);
IAsyncEnumerable<object> IOpenIddictTokenManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken)
=> ListAsync(count, offset, cancellationToken).OfType<object>();
@ -1394,6 +1396,9 @@ namespace OpenIddict.Core
ValueTask<bool> IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken)
=> TryRedeemAsync((TToken) token, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.TryRejectAsync(object token, CancellationToken cancellationToken)
=> TryRejectAsync((TToken) token, cancellationToken);
ValueTask<bool> IOpenIddictTokenManager.TryRevokeAsync(object token, CancellationToken cancellationToken)
=> TryRevokeAsync((TToken) token, cancellationToken);

3
src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs

@ -54,8 +54,7 @@ namespace OpenIddict.EntityFramework
.IsRequired();
Property(authorization => authorization.Subject)
.HasMaxLength(450)
.IsRequired();
.HasMaxLength(450);
Property(authorization => authorization.Type)
.HasMaxLength(25)

3
src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs

@ -63,8 +63,7 @@ namespace OpenIddict.EntityFramework
.IsRequired();
Property(token => token.Subject)
.HasMaxLength(450)
.IsRequired();
.HasMaxLength(450);
Property(token => token.Type)
.HasMaxLength(25)

3
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs

@ -55,8 +55,7 @@ namespace OpenIddict.EntityFrameworkCore
.IsRequired();
builder.Property(authorization => authorization.Subject)
.HasMaxLength(450)
.IsRequired();
.HasMaxLength(450);
builder.Property(authorization => authorization.Type)
.HasMaxLength(25)

3
src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs

@ -64,8 +64,7 @@ namespace OpenIddict.EntityFrameworkCore
.IsRequired();
builder.Property(token => token.Subject)
.HasMaxLength(450)
.IsRequired();
.HasMaxLength(450);
builder.Property(token => token.Type)
.HasMaxLength(25)

5
src/OpenIddict.NHibernate/Mappings/OpenIddictAuthorizationMapping.cs

@ -53,11 +53,6 @@ namespace OpenIddict.NHibernate
map.NotNullable(true);
});
Property(authorization => authorization.Subject, map =>
{
map.NotNullable(true);
});
Property(authorization => authorization.Type, map =>
{
map.NotNullable(true);

5
src/OpenIddict.NHibernate/Mappings/OpenIddictTokenMapping.cs

@ -59,11 +59,6 @@ namespace OpenIddict.NHibernate
map.NotNullable(true);
});
Property(token => token.Subject, map =>
{
map.NotNullable(true);
});
Property(token => token.Type, map =>
{
map.NotNullable(true);

10
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs

@ -109,6 +109,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerAspNetCoreBuilder EnableUserinfoEndpointPassthrough()
=> Configure(options => options.EnableUserinfoEndpointPassthrough = true);
/// <summary>
/// Enables the pass-through mode for the OpenID Connect user verification endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
/// <returns>The <see cref="OpenIddictServerAspNetCoreBuilder"/>.</returns>
public OpenIddictServerAspNetCoreBuilder EnableVerificationEndpointPassthrough()
=> Configure(options => options.EnableVerificationEndpointPassthrough = true);
/// <summary>
/// Enables request caching, so that both authorization and logout requests
/// are automatically stored in the distributed cache, which allows flowing

1
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs

@ -53,6 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireStatusCodePagesIntegrationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireUserinfoEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireVerificationEndpointPassthroughEnabled>();
// Register the option initializer used by the OpenIddict ASP.NET Core server integration services.
// Note: TryAddEnumerable() is used here to ensure the initializers are only registered once.

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

@ -5,6 +5,7 @@
*/
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
@ -17,7 +18,6 @@ using Microsoft.Extensions.Options;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
using Properties = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.Properties;
namespace OpenIddict.Server.AspNetCore
{
@ -106,7 +106,7 @@ namespace OpenIddict.Server.AspNetCore
return false;
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction;
if (transaction == null)
@ -114,14 +114,36 @@ namespace OpenIddict.Server.AspNetCore
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
if (transaction.Properties.TryGetValue(OpenIddictServerConstants.Properties.AmbientPrincipal, out var principal))
// Note: in many cases, the authentication token was already validated by the time this action is called
// (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it,
// the authentication context is resolved from the transaction. If it's not available, a new one is created.
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName);
if (context == null)
{
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(
(ClaimsPrincipal) principal,
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)));
context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
}
return Task.FromResult(AuthenticateResult.NoResult());
if (context.IsRequestHandled || context.IsRequestSkipped)
{
return AuthenticateResult.NoResult();
}
else if (context.IsRejected)
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = context.Error,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = context.ErrorDescription,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorUri] = context.ErrorUri
});
return AuthenticateResult.Fail("An unknown error occurred while authenticating the current request.", properties);
}
return AuthenticateResult.Success(new AuthenticationTicket(
context.Principal,
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme));
}
protected override async Task HandleChallengeAsync([CanBeNull] AuthenticationProperties properties)
@ -132,14 +154,11 @@ namespace OpenIddict.Server.AspNetCore
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties();
var context = new ProcessChallengeContext(transaction)
{
Response = new OpenIddictResponse
{
Error = GetProperty(properties, Properties.Error),
ErrorDescription = GetProperty(properties, Properties.ErrorDescription),
ErrorUri = GetProperty(properties, Properties.ErrorUri)
}
Response = new OpenIddictResponse()
};
await _provider.DispatchAsync(context);
@ -174,9 +193,6 @@ namespace OpenIddict.Server.AspNetCore
.Append("was not registered or was explicitly removed from the handlers list.")
.ToString());
}
static string GetProperty(AuthenticationProperties properties, string name)
=> properties != null && properties.Items.TryGetValue(name, out string value) ? value : null;
}
protected override Task HandleForbiddenAsync([CanBeNull] AuthenticationProperties properties)
@ -195,6 +211,8 @@ namespace OpenIddict.Server.AspNetCore
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties();
var context = new ProcessSigninContext(transaction)
{
Principal = user,
@ -248,6 +266,8 @@ namespace OpenIddict.Server.AspNetCore
Response = new OpenIddictResponse()
};
transaction.Properties[typeof(AuthenticationProperties).FullName] = properties ?? new AuthenticationProperties();
await _provider.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)

22
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs

@ -206,5 +206,27 @@ namespace OpenIddict.Server.AspNetCore
return new ValueTask<bool>(_options.CurrentValue.EnableUserinfoEndpointPassthrough);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the verification endpoint.
/// </summary>
public class RequireVerificationEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerAspNetCoreOptions> _options;
public RequireVerificationEndpointPassthroughEnabled([NotNull] IOptionsMonitor<OpenIddictServerAspNetCoreOptions> options)
=> _options = options;
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(_options.CurrentValue.EnableVerificationEndpointPassthrough);
}
}
}
}

34
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
@ -21,7 +22,6 @@ using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters;
@ -369,12 +369,14 @@ namespace OpenIddict.Server.AspNetCore
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
var key = _encoder.Encode(parameter.Key);
var value = _encoder.Encode(parameter.Value);
writer.WriteLine($@"<input type=""hidden"" name=""{key}"" value=""{value}"" />");
writer.WriteLine($@"<input type=""hidden"" name=""{_encoder.Encode(key)}"" value=""{_encoder.Encode(value)}"" />");
}
writer.WriteLine(@"<noscript>Click here to finish the authorization process: <input type=""submit"" /></noscript>");
@ -453,9 +455,14 @@ namespace OpenIddict.Server.AspNetCore
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
location = QueryHelpers.AddQueryString(location, parameter.Key, parameter.Value);
location = QueryHelpers.AddQueryString(location, key, value);
}
response.Redirect(location);
@ -518,12 +525,17 @@ namespace OpenIddict.Server.AspNetCore
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
builder.Append(Contains(builder, '#') ? '&' : '#')
.Append(Uri.EscapeDataString(parameter.Key))
.Append(Uri.EscapeDataString(key))
.Append('=')
.Append(Uri.EscapeDataString(parameter.Value));
.Append(Uri.EscapeDataString(value));
}
response.Redirect(builder.ToString());

48
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs

@ -0,0 +1,48 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters;
using static OpenIddict.Server.OpenIddictServerEvents;
namespace OpenIddict.Server.AspNetCore
{
public static partial class OpenIddictServerAspNetCoreHandlers
{
public static class Device
{
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Device request extraction:
*/
ExtractGetOrPostRequest<ExtractDeviceRequestContext>.Descriptor,
/*
* Device response processing:
*/
ProcessJsonResponse<ApplyDeviceResponseContext>.Descriptor,
/*
* Verification request extraction:
*/
ExtractGetOrPostRequest<ExtractVerificationRequestContext>.Descriptor,
/*
* Verification request handling:
*/
EnablePassthroughMode<HandleVerificationRequestContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor,
/*
* Verification response processing:
*/
ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessStatusCodePagesErrorResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyVerificationResponseContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessEmptyResponse<ApplyVerificationResponseContext>.Descriptor);
}
}
}

12
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
@ -20,7 +21,6 @@ using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters;
@ -50,6 +50,7 @@ namespace OpenIddict.Server.AspNetCore
*/
RemoveCachedRequest.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse<ApplyLogoutResponseContext>.Descriptor,
ProcessStatusCodePagesErrorResponse<ApplyLogoutResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyLogoutResponseContext, RequireLogoutEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyLogoutResponseContext>.Descriptor,
@ -350,9 +351,14 @@ namespace OpenIddict.Server.AspNetCore
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
location = QueryHelpers.AddQueryString(location, parameter.Key, parameter.Value);
location = QueryHelpers.AddQueryString(location, key, value);
}
response.Redirect(location);

107
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs

@ -13,6 +13,7 @@ using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@ -22,6 +23,8 @@ using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters;
using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlers;
using Properties = OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants.Properties;
namespace OpenIddict.Server.AspNetCore
{
@ -34,8 +37,14 @@ namespace OpenIddict.Server.AspNetCore
*/
InferEndpointType.Descriptor,
InferIssuerFromHost.Descriptor,
ValidateTransportSecurityRequirement.Descriptor)
ValidateTransportSecurityRequirement.Descriptor,
/*
* Challenge processing:
*/
AttachHostChallengeError.Descriptor)
.AddRange(Authentication.DefaultHandlers)
.AddRange(Device.DefaultHandlers)
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers)
.AddRange(Introspection.DefaultHandlers)
@ -85,11 +94,13 @@ namespace OpenIddict.Server.AspNetCore
Matches(context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization :
Matches(context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration :
Matches(context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography :
Matches(context.Options.DeviceEndpointUris) ? OpenIddictServerEndpointType.Device :
Matches(context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection :
Matches(context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout :
Matches(context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation :
Matches(context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token :
Matches(context.Options.UserinfoEndpointUris) ? OpenIddictServerEndpointType.Userinfo :
Matches(context.Options.VerificationEndpointUris) ? OpenIddictServerEndpointType.Verification :
OpenIddictServerEndpointType.Unknown;
return default;
@ -264,6 +275,48 @@ namespace OpenIddict.Server.AspNetCore
}
}
/// <summary>
/// Contains the logic responsible of attaching the error details using the ASP.NET Core authentication properties.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class AttachHostChallengeError : IOpenIddictServerHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<AttachHostChallengeError>()
.SetOrder(AttachDefaultChallengeError.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessChallengeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties)
{
context.Response.Error = properties.GetString(Properties.Error);
context.Response.ErrorDescription = properties.GetString(Properties.ErrorDescription);
context.Response.ErrorUri = properties.GetString(Properties.ErrorUri);
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of extracting OpenID Connect requests from GET HTTP requests.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
@ -684,6 +737,58 @@ namespace OpenIddict.Server.AspNetCore
}
}
/// <summary>
/// Contains the logic responsible of processing empty OpenID Connect responses that should trigger a host redirection.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
/// </summary>
public class ProcessHostRedirectionResponse<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ProcessHostRedirectionResponse<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetHttpRequest()?.HttpContext.Response;
if (response == null)
{
throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved.");
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties && !string.IsNullOrEmpty(properties.RedirectUri))
{
response.Redirect(properties.RedirectUri);
context.Logger.LogInformation("The response was successfully returned as a 302 response.");
context.HandleRequest();
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.

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

@ -63,6 +63,14 @@ namespace OpenIddict.Server.AspNetCore
/// </summary>
public bool EnableUserinfoEndpointPassthrough { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the pass-through mode is enabled for the user verification endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
public bool EnableVerificationEndpointPassthrough { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether request caching should be enabled.
/// When enabled, both authorization and logout requests are automatically stored

2
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs

@ -42,7 +42,9 @@ namespace OpenIddict.Server.DataProtection
{
public const string AccessToken = "AccessTokenFormat";
public const string AuthorizationCode = "AuthorizationCodeFormat";
public const string DeviceCode = "DeviceCodeFormat";
public const string RefreshToken = "RefreshTokenFormat";
public const string UserCode = "UserCodeFormat";
}
public static class Handlers

2
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs

@ -254,7 +254,7 @@ namespace OpenIddict.Server.DataProtection
ClaimsPrincipal principal, IReadOnlyDictionary<string, string> properties)
{
writer.Write(version);
writer.Write(scheme);
writer.Write(scheme ?? string.Empty);
// Write the number of identities contained in the principal.
writer.Write(principal.Identities.Count());

727
src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs

@ -9,8 +9,6 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.DataProtection;
@ -20,10 +18,12 @@ using Microsoft.IdentityModel.Tokens;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes;
using static OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionHandlerFilters;
using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlerFilters;
using static OpenIddict.Server.OpenIddictServerHandlers;
using Schemes = OpenIddict.Server.DataProtection.OpenIddictServerDataProtectionConstants.Purposes.Schemes;
namespace OpenIddict.Server.DataProtection
{
@ -34,215 +34,25 @@ namespace OpenIddict.Server.DataProtection
/*
* Authentication processing:
*/
ValidateReferenceDataProtectionToken.Descriptor,
ValidateSelfContainedDataProtectionToken.Descriptor,
ValidateDataProtectionToken.Descriptor,
/*
* Sign-in processing:
*/
AttachReferenceDataProtectionAccessToken.Descriptor,
AttachReferenceDataProtectionAuthorizationCode.Descriptor,
AttachReferenceDataProtectionRefreshToken.Descriptor,
AttachSelfContainedDataProtectionAccessToken.Descriptor,
AttachSelfContainedDataProtectionAuthorizationCode.Descriptor,
AttachSelfContainedDataProtectionRefreshToken.Descriptor);
/// <summary>
/// Contains the logic responsible of rejecting authentication
/// demands that use an invalid reference Data Protection token.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class ValidateReferenceDataProtectionToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public ValidateReferenceDataProtectionToken() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.Append("Alternatively, you can disable the built-in database-based server features by enabling ")
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString());
public ValidateReferenceDataProtectionToken(
[NotNull] IOpenIddictTokenManager tokenManager,
[NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
{
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireReferenceTokensEnabled>()
.UseScopedHandler<ValidateReferenceDataProtectionToken>()
.SetOrder(ValidateReferenceToken.Descriptor.Order + 500)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal != null)
{
return;
}
var identifier = context.EndpointType switch
{
OpenIddictServerEndpointType.Introspection => context.Request.Token,
OpenIddictServerEndpointType.Revocation => context.Request.Token,
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> context.Request.Code,
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> context.Request.RefreshToken,
OpenIddictServerEndpointType.Userinfo => context.Request.AccessToken,
_ => null
};
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (string.IsNullOrEmpty(identifier))
{
return;
}
var token = await _tokenManager.FindByReferenceIdAsync(identifier);
if (token == null || !await IsTokenTypeValidAsync(token))
{
return;
}
var payload = await _tokenManager.GetPayloadAsync(token);
if (string.IsNullOrEmpty(payload))
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The payload associated with a reference token cannot be retrieved.")
.Append("This may indicate that the token entry was corrupted.")
.ToString());
}
var principal = context.EndpointType switch
{
OpenIddictServerEndpointType.Introspection => ValidateToken(payload, TokenUsages.AccessToken) ??
ValidateToken(payload, TokenUsages.RefreshToken) ??
ValidateToken(payload, TokenUsages.AuthorizationCode),
OpenIddictServerEndpointType.Revocation => ValidateToken(payload, TokenUsages.AccessToken) ??
ValidateToken(payload, TokenUsages.RefreshToken) ??
ValidateToken(payload, TokenUsages.AuthorizationCode),
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> ValidateToken(payload, TokenUsages.AuthorizationCode),
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> ValidateToken(payload, TokenUsages.RefreshToken),
OpenIddictServerEndpointType.Userinfo => ValidateToken(payload, TokenUsages.AccessToken),
_ => null
};
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (principal == null)
{
return;
}
// Attach the principal extracted from the authorization code to the parent event context
// and restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal = principal
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetInternalTokenId(await _tokenManager.GetIdAsync(token))
.SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token));
context.Logger.LogTrace("The reference DP token '{Token}' was successfully validated and the following " +
"claims could be extracted: {Claims}.", payload, context.Principal.Claims);
ClaimsPrincipal ValidateToken(string token, string type)
{
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
type switch
{
TokenUsages.AccessToken => Purposes.Formats.AccessToken,
TokenUsages.AuthorizationCode => Purposes.Formats.AuthorizationCode,
TokenUsages.RefreshToken => Purposes.Formats.RefreshToken,
_ => throw new InvalidOperationException("The specified token type is not supported.")
},
Purposes.Features.ReferenceTokens,
Purposes.Schemes.Server);
try
{
using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(token)));
using var reader = new BinaryReader(buffer);
// Note: since the data format relies on a data protector using different "purposes" strings
// per token type, the token processed at this stage is guaranteed to be of the expected type.
return _options.CurrentValue.Formatter.ReadToken(reader)?.SetClaim(Claims.Private.TokenUsage, type);
}
catch (Exception exception)
{
context.Logger.LogTrace(exception, "An exception occured while deserializing the token '{Token}'.", token);
return null;
}
}
async ValueTask<bool> IsTokenTypeValidAsync(object token) => context.EndpointType switch
{
// All types of tokens are accepted by the introspection and revocation endpoints.
OpenIddictServerEndpointType.Introspection => true,
OpenIddictServerEndpointType.Revocation => true,
OpenIddictServerEndpointType.Token => await _tokenManager.GetTypeAsync(token) switch
{
TokenUsages.AuthorizationCode when context.Request.IsAuthorizationCodeGrantType() => true,
TokenUsages.RefreshToken when context.Request.IsRefreshTokenGrantType() => true,
_ => false
},
OpenIddictServerEndpointType.Userinfo => await _tokenManager.GetTypeAsync(token) switch
{
TokenUsages.AccessToken => true,
_ => false
},
_ => false
};
}
}
GenerateDataProtectionAccessToken.Descriptor,
GenerateDataProtectionAuthorizationCode.Descriptor,
GenerateDataProtectionDeviceCode.Descriptor,
GenerateDataProtectionRefreshToken.Descriptor,
GenerateDataProtectionUserCode.Descriptor);
/// <summary>
/// Contains the logic responsible of rejecting authentication demands
/// that specify an invalid self-contained Data Protection token.
/// Contains the logic responsible of validating tokens generated using Data Protection.
/// </summary>
public class ValidateSelfContainedDataProtectionToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
public class ValidateDataProtectionToken : IOpenIddictServerHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public ValidateSelfContainedDataProtectionToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
public ValidateDataProtectionToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
@ -250,9 +60,8 @@ namespace OpenIddict.Server.DataProtection
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.UseSingletonHandler<ValidateSelfContainedDataProtectionToken>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order + 500)
.UseSingletonHandler<ValidateDataProtectionToken>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500)
.Build();
/// <summary>
@ -275,52 +84,12 @@ namespace OpenIddict.Server.DataProtection
return default;
}
var token = context.EndpointType switch
{
OpenIddictServerEndpointType.Introspection => context.Request.Token,
OpenIddictServerEndpointType.Revocation => context.Request.Token,
// This handler doesn't handle reference tokens.
_ when context.Options.UseReferenceTokens => null,
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> context.Request.Code,
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> context.Request.RefreshToken,
OpenIddictServerEndpointType.Userinfo => context.Request.AccessToken,
_ => null
};
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (string.IsNullOrEmpty(token))
{
return default;
}
var principal = context.EndpointType switch
{
OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType()
=> ValidateToken(token, TokenUsages.AuthorizationCode),
OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType()
=> ValidateToken(token, TokenUsages.RefreshToken),
OpenIddictServerEndpointType.Introspection => ValidateToken(token, TokenUsages.AccessToken) ??
ValidateToken(token, TokenUsages.RefreshToken) ??
ValidateToken(token, TokenUsages.AuthorizationCode),
OpenIddictServerEndpointType.Revocation => ValidateToken(token, TokenUsages.AccessToken) ??
ValidateToken(token, TokenUsages.RefreshToken) ??
ValidateToken(token, TokenUsages.AuthorizationCode),
OpenIddictServerEndpointType.Userinfo => ValidateToken(token, TokenUsages.AccessToken),
_ => null
};
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var principal = !string.IsNullOrEmpty(context.TokenType) ?
ValidateToken(context.Token, context.TokenType) :
ValidateToken(context.Token, TokenUsages.AccessToken) ??
ValidateToken(context.Token, TokenUsages.RefreshToken) ??
ValidateToken(context.Token, TokenUsages.AuthorizationCode);
if (principal == null)
{
return default;
@ -328,25 +97,39 @@ namespace OpenIddict.Server.DataProtection
context.Principal = principal;
context.Logger.LogTrace("The self-contained DP token '{Token}' was successfully validated and the following " +
"claims could be extracted: {Claims}.", token, context.Principal.Claims);
context.Logger.LogTrace("The DP token '{Token}' was successfully validated and the following claims " +
"could be extracted: {Claims}.", context.Token, context.Principal.Claims);
return default;
ClaimsPrincipal ValidateToken(string token, string type)
{
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
type switch
{
TokenUsages.AccessToken => Purposes.Formats.AccessToken,
TokenUsages.AuthorizationCode => Purposes.Formats.AuthorizationCode,
TokenUsages.RefreshToken => Purposes.Formats.RefreshToken,
_ => throw new InvalidOperationException("The specified token type is not supported.")
},
Purposes.Schemes.Server);
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(type switch
{
TokenUsages.AccessToken when context.Options.UseReferenceAccessTokens
=> new[] { Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server },
TokenUsages.AuthorizationCode when !context.Options.DisableTokenStorage
=> new[] { Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server },
TokenUsages.DeviceCode when !context.Options.DisableTokenStorage
=> new[] { Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server },
TokenUsages.RefreshToken when !context.Options.DisableTokenStorage
=> new[] { Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server },
TokenUsages.UserCode when !context.Options.DisableTokenStorage
=> new[] { Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server, },
TokenUsages.AccessToken => new[] { Handlers.Server, Formats.AccessToken, Schemes.Server },
TokenUsages.AuthorizationCode => new[] { Handlers.Server, Formats.AuthorizationCode, Schemes.Server },
TokenUsages.DeviceCode => new[] { Handlers.Server, Formats.DeviceCode, Schemes.Server },
TokenUsages.RefreshToken => new[] { Handlers.Server, Formats.RefreshToken, Schemes.Server },
TokenUsages.UserCode => new[] { Handlers.Server, Formats.UserCode, Schemes.Server },
_ => throw new InvalidOperationException("The specified token type is not supported.")
});
try
{
@ -369,46 +152,24 @@ namespace OpenIddict.Server.DataProtection
}
/// <summary>
/// Contains the logic responsible of generating and attaching the
/// reference Data Protection access token returned as part of the response.
/// Note: this handler is not used when the degraded mode is enabled.
/// Contains the logic responsible of generating an access token using Data Protection.
/// </summary>
public class AttachReferenceDataProtectionAccessToken : IOpenIddictServerHandler<ProcessSigninContext>
public class GenerateDataProtectionAccessToken : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachReferenceDataProtectionAccessToken() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.Append("Alternatively, you can disable the built-in database-based server features by enabling ")
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString());
public AttachReferenceDataProtectionAccessToken(
[NotNull] IOpenIddictApplicationManager applicationManager,
[NotNull] IOpenIddictTokenManager tokenManager,
[NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
{
_applicationManager = applicationManager;
_tokenManager = tokenManager;
_options = options;
}
public GenerateDataProtectionAccessToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireAccessTokenIncluded>()
.AddFilter<RequireReferenceTokensEnabled>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseScopedHandler<AttachReferenceDataProtectionAccessToken>()
.SetOrder(AttachReferenceAccessToken.Descriptor.Order - 500)
.UseSingletonHandler<GenerateDataProtectionAccessToken>()
.SetOrder(GenerateIdentityModelAccessToken.Descriptor.Order - 500)
.Build();
/// <summary>
@ -418,7 +179,7 @@ namespace OpenIddict.Server.DataProtection
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ProcessSigninContext context)
public ValueTask HandleAsync([NotNull] ProcessSigninContext context)
{
if (context == null)
{
@ -428,107 +189,51 @@ namespace OpenIddict.Server.DataProtection
// If an access token was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.AccessToken))
{
return;
return default;
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.AccessToken,
Purposes.Features.ReferenceTokens,
Purposes.Schemes.Server);
var protector = context.Options.UseReferenceAccessTokens ?
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.AccessToken, Features.ReferenceTokens, Schemes.Server) :
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.AccessToken, Schemes.Server);
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
_options.CurrentValue.Formatter.WriteToken(writer, context.AccessTokenPrincipal);
// Generate a new crypto-secure random identifier that will be substituted to the token.
var data = new byte[256 / 8];
#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
RandomNumberGenerator.Fill(data);
#else
using var generator = RandomNumberGenerator.Create();
generator.GetBytes(data);
#endif
var descriptor = new OpenIddictTokenDescriptor
{
AuthorizationId = context.AccessTokenPrincipal.GetInternalAuthorizationId(),
CreationDate = context.AccessTokenPrincipal.GetCreationDate(),
ExpirationDate = context.AccessTokenPrincipal.GetExpirationDate(),
Payload = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())),
Principal = context.AccessTokenPrincipal,
ReferenceId = Base64UrlEncoder.Encode(data),
Status = Statuses.Valid,
Subject = context.AccessTokenPrincipal.GetClaim(Claims.Subject),
Type = TokenUsages.AccessToken
};
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(context.Request.ClientId))
{
var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application entry cannot be found in the database.");
}
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
}
var token = await _tokenManager.CreateAsync(descriptor);
context.Response.AccessToken = descriptor.ReferenceId;
context.Response.AccessToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Logger.LogTrace("The reference access token '{Identifier}' was successfully created with the " +
"reference identifier '{ReferenceId}' and the following DP payload: {Payload}. " +
context.Logger.LogTrace("The access token '{Identifier}' was successfully created: {Payload}. " +
"The principal used to create the token contained the following claims: {Claims}.",
await _tokenManager.GetIdAsync(token), descriptor.ReferenceId,
descriptor.Payload, context.AccessTokenPrincipal.Claims);
context.AccessTokenPrincipal.GetClaim(Claims.JwtId),
context.Response.AccessToken, context.AccessTokenPrincipal.Claims);
return default;
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the
/// reference Data Protection authorization code returned as part of the response.
/// Note: this handler is not used when the degraded mode is enabled.
/// Contains the logic responsible of generating an authorization code using Data Protection.
/// </summary>
public class AttachReferenceDataProtectionAuthorizationCode : IOpenIddictServerHandler<ProcessSigninContext>
public class GenerateDataProtectionAuthorizationCode : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachReferenceDataProtectionAuthorizationCode() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.Append("Alternatively, you can disable the built-in database-based server features by enabling ")
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString());
public AttachReferenceDataProtectionAuthorizationCode(
[NotNull] IOpenIddictApplicationManager applicationManager,
[NotNull] IOpenIddictTokenManager tokenManager,
[NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
{
_applicationManager = applicationManager;
_tokenManager = tokenManager;
_options = options;
}
public GenerateDataProtectionAuthorizationCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireAuthorizationCodeIncluded>()
.AddFilter<RequireReferenceTokensEnabled>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseScopedHandler<AttachReferenceDataProtectionAuthorizationCode>()
.SetOrder(AttachReferenceAuthorizationCode.Descriptor.Order - 500)
.UseSingletonHandler<GenerateDataProtectionAuthorizationCode>()
.SetOrder(GenerateIdentityModelAuthorizationCode.Descriptor.Order - 500)
.Build();
/// <summary>
@ -538,7 +243,7 @@ namespace OpenIddict.Server.DataProtection
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ProcessSigninContext context)
public ValueTask HandleAsync([NotNull] ProcessSigninContext context)
{
if (context == null)
{
@ -548,195 +253,40 @@ namespace OpenIddict.Server.DataProtection
// If an authorization code was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.Code))
{
return;
return default;
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.AuthorizationCode,
Purposes.Features.ReferenceTokens,
Purposes.Schemes.Server);
var protector = !context.Options.DisableTokenStorage ?
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.AuthorizationCode, Features.ReferenceTokens, Schemes.Server) :
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.AuthorizationCode, Schemes.Server);
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
_options.CurrentValue.Formatter.WriteToken(writer, context.AuthorizationCodePrincipal);
// Generate a new crypto-secure random identifier that will be substituted to the token.
var data = new byte[256 / 8];
#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
RandomNumberGenerator.Fill(data);
#else
using var generator = RandomNumberGenerator.Create();
generator.GetBytes(data);
#endif
var descriptor = new OpenIddictTokenDescriptor
{
AuthorizationId = context.AuthorizationCodePrincipal.GetInternalAuthorizationId(),
CreationDate = context.AuthorizationCodePrincipal.GetCreationDate(),
ExpirationDate = context.AuthorizationCodePrincipal.GetExpirationDate(),
Payload = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())),
Principal = context.AuthorizationCodePrincipal,
ReferenceId = Base64UrlEncoder.Encode(data),
Status = Statuses.Valid,
Subject = context.AuthorizationCodePrincipal.GetClaim(Claims.Subject),
Type = TokenUsages.AuthorizationCode
};
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(context.Request.ClientId))
{
var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application entry cannot be found in the database.");
}
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
}
var token = await _tokenManager.CreateAsync(descriptor);
context.Response.Code = descriptor.ReferenceId;
context.Response.Code = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Logger.LogTrace("The reference authorization code '{Identifier}' was successfully created with the " +
"reference identifier '{ReferenceId}' and the following DP payload: {Payload}. " +
context.Logger.LogTrace("The authorization code '{Identifier}' was successfully created: {Payload}. " +
"The principal used to create the token contained the following claims: {Claims}.",
await _tokenManager.GetIdAsync(token), descriptor.ReferenceId,
descriptor.Payload, context.AuthorizationCodePrincipal.Claims);
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the
/// reference Data Protection refresh token returned as part of the response.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class AttachReferenceDataProtectionRefreshToken : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachReferenceDataProtectionRefreshToken() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.Append("Alternatively, you can disable the built-in database-based server features by enabling ")
.Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.")
.ToString());
public AttachReferenceDataProtectionRefreshToken(
[NotNull] IOpenIddictApplicationManager applicationManager,
[NotNull] IOpenIddictTokenManager tokenManager,
[NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
{
_applicationManager = applicationManager;
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireDegradedModeDisabled>()
.AddFilter<RequireTokenStorageEnabled>()
.AddFilter<RequireRefreshTokenIncluded>()
.AddFilter<RequireReferenceTokensEnabled>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseScopedHandler<AttachReferenceDataProtectionRefreshToken>()
.SetOrder(AttachReferenceRefreshToken.Descriptor.Order - 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ProcessSigninContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If a refresh token was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.RefreshToken))
{
return;
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.RefreshToken,
Purposes.Features.ReferenceTokens,
Purposes.Schemes.Server);
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
_options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal);
// Generate a new crypto-secure random identifier that will be substituted to the token.
var data = new byte[256 / 8];
#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS
RandomNumberGenerator.Fill(data);
#else
using var generator = RandomNumberGenerator.Create();
generator.GetBytes(data);
#endif
var descriptor = new OpenIddictTokenDescriptor
{
AuthorizationId = context.RefreshTokenPrincipal.GetInternalAuthorizationId(),
CreationDate = context.RefreshTokenPrincipal.GetCreationDate(),
ExpirationDate = context.RefreshTokenPrincipal.GetExpirationDate(),
Payload = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray())),
Principal = context.RefreshTokenPrincipal,
ReferenceId = Base64UrlEncoder.Encode(data),
Status = Statuses.Valid,
Subject = context.RefreshTokenPrincipal.GetClaim(Claims.Subject),
Type = TokenUsages.RefreshToken
};
// If the client application is known, associate it with the token.
if (!string.IsNullOrEmpty(context.Request.ClientId))
{
var application = await _applicationManager.FindByClientIdAsync(context.Request.ClientId);
if (application == null)
{
throw new InvalidOperationException("The application entry cannot be found in the database.");
}
descriptor.ApplicationId = await _applicationManager.GetIdAsync(application);
}
var token = await _tokenManager.CreateAsync(descriptor);
context.Response.RefreshToken = descriptor.ReferenceId;
context.AuthorizationCodePrincipal.GetClaim(Claims.JwtId),
context.Response.Code, context.AuthorizationCodePrincipal.Claims);
context.Logger.LogTrace("The reference refresh token '{Identifier}' was successfully created with the " +
"reference identifier '{ReferenceId}' and the following DP payload: {Payload}. " +
"The principal used to create the token contained the following claims: {Claims}.",
await _tokenManager.GetIdAsync(token), descriptor.ReferenceId,
descriptor.Payload, context.RefreshTokenPrincipal.Claims);
return default;
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the self-contained
/// Data Protection access token returned as part of the response.
/// Contains the logic responsible of generating a device code using Data Protection.
/// </summary>
public class AttachSelfContainedDataProtectionAccessToken : IOpenIddictServerHandler<ProcessSigninContext>
public class GenerateDataProtectionDeviceCode : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachSelfContainedDataProtectionAccessToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
public GenerateDataProtectionDeviceCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
@ -744,11 +294,10 @@ namespace OpenIddict.Server.DataProtection
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.AddFilter<RequireAccessTokenIncluded>()
.AddFilter<RequireDeviceCodeIncluded>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseSingletonHandler<AttachSelfContainedDataProtectionAccessToken>()
.SetOrder(AttachSelfContainedAccessToken.Descriptor.Order - 500)
.UseSingletonHandler<GenerateDataProtectionDeviceCode>()
.SetOrder(GenerateIdentityModelDeviceCode.Descriptor.Order - 500)
.Build();
/// <summary>
@ -765,44 +314,43 @@ namespace OpenIddict.Server.DataProtection
throw new ArgumentNullException(nameof(context));
}
// If an access token was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.AccessToken))
// If a device code was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.DeviceCode))
{
return default;
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.AccessToken,
Purposes.Schemes.Server);
var protector = !context.Options.DisableTokenStorage ?
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.DeviceCode, Features.ReferenceTokens, Schemes.Server) :
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.DeviceCode, Schemes.Server);
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
_options.CurrentValue.Formatter.WriteToken(writer, context.AccessTokenPrincipal);
_options.CurrentValue.Formatter.WriteToken(writer, context.DeviceCodePrincipal);
context.Response.AccessToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Response.DeviceCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Logger.LogTrace("The access token '{Identifier}' was successfully created and the " +
"following DP payload was attached to the OpenID Connect response: {Payload}. " +
context.Logger.LogTrace("The device code '{Identifier}' was successfully created: {Payload}. " +
"The principal used to create the token contained the following claims: {Claims}.",
context.AccessTokenPrincipal.GetClaim(Claims.JwtId),
context.Response.AccessToken, context.AccessTokenPrincipal.Claims);
context.DeviceCodePrincipal.GetClaim(Claims.JwtId),
context.Response.DeviceCode, context.DeviceCodePrincipal.Claims);
return default;
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the self-contained
/// Data Protection authorization code returned as part of the response.
/// Contains the logic responsible of generating a refresh token using Data Protection.
/// </summary>
public class AttachSelfContainedDataProtectionAuthorizationCode : IOpenIddictServerHandler<ProcessSigninContext>
public class GenerateDataProtectionRefreshToken : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachSelfContainedDataProtectionAuthorizationCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
public GenerateDataProtectionRefreshToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
@ -810,11 +358,10 @@ namespace OpenIddict.Server.DataProtection
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.AddFilter<RequireAuthorizationCodeIncluded>()
.AddFilter<RequireRefreshTokenIncluded>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseSingletonHandler<AttachSelfContainedDataProtectionAuthorizationCode>()
.SetOrder(AttachSelfContainedAuthorizationCode.Descriptor.Order - 500)
.UseSingletonHandler<GenerateDataProtectionRefreshToken>()
.SetOrder(GenerateIdentityModelRefreshToken.Descriptor.Order - 500)
.Build();
/// <summary>
@ -831,44 +378,43 @@ namespace OpenIddict.Server.DataProtection
throw new ArgumentNullException(nameof(context));
}
// If an authorization code was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.Code))
// If a refresh token was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.RefreshToken))
{
return default;
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.AuthorizationCode,
Purposes.Schemes.Server);
var protector = !context.Options.DisableTokenStorage ?
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.RefreshToken, Features.ReferenceTokens, Schemes.Server) :
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.RefreshToken, Schemes.Server);
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
_options.CurrentValue.Formatter.WriteToken(writer, context.AuthorizationCodePrincipal);
_options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal);
context.Response.Code = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Response.RefreshToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Logger.LogTrace("The authorization code '{Identifier}' was successfully created and the " +
"following JWT payload was attached to the OpenID Connect response: {Payload}. " +
context.Logger.LogTrace("The refresh token '{Identifier}' was successfully created: {Payload}. " +
"The principal used to create the token contained the following claims: {Claims}.",
context.AuthorizationCodePrincipal.GetClaim(Claims.JwtId),
context.Response.Code, context.AuthorizationCodePrincipal.Claims);
context.RefreshTokenPrincipal.GetClaim(Claims.JwtId),
context.Response.RefreshToken, context.RefreshTokenPrincipal.Claims);
return default;
}
}
/// <summary>
/// Contains the logic responsible of generating and attaching the self-contained
/// Data Protection refresh token returned as part of the response.
/// Contains the logic responsible of generating a user code using Data Protection.
/// </summary>
public class AttachSelfContainedDataProtectionRefreshToken : IOpenIddictServerHandler<ProcessSigninContext>
public class GenerateDataProtectionUserCode : IOpenIddictServerHandler<ProcessSigninContext>
{
private readonly IOptionsMonitor<OpenIddictServerDataProtectionOptions> _options;
public AttachSelfContainedDataProtectionRefreshToken([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
public GenerateDataProtectionUserCode([NotNull] IOptionsMonitor<OpenIddictServerDataProtectionOptions> options)
=> _options = options;
/// <summary>
@ -876,11 +422,10 @@ namespace OpenIddict.Server.DataProtection
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessSigninContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.AddFilter<RequireRefreshTokenIncluded>()
.AddFilter<RequireUserCodeIncluded>()
.AddFilter<RequirePreferDataProtectionFormatEnabled>()
.UseSingletonHandler<AttachSelfContainedDataProtectionRefreshToken>()
.SetOrder(AttachSelfContainedRefreshToken.Descriptor.Order - 500)
.UseSingletonHandler<GenerateDataProtectionUserCode>()
.SetOrder(GenerateIdentityModelUserCode.Descriptor.Order - 500)
.Build();
/// <summary>
@ -897,30 +442,30 @@ namespace OpenIddict.Server.DataProtection
throw new ArgumentNullException(nameof(context));
}
// If a refresh token was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.RefreshToken))
// If a user code was already attached by another handler, don't overwrite it.
if (!string.IsNullOrEmpty(context.Response.UserCode))
{
return default;
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.RefreshToken,
Purposes.Schemes.Server);
var protector = !context.Options.DisableTokenStorage ?
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.UserCode, Features.ReferenceTokens, Schemes.Server) :
_options.CurrentValue.DataProtectionProvider.CreateProtector(
Handlers.Server, Formats.UserCode, Schemes.Server);
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
_options.CurrentValue.Formatter.WriteToken(writer, context.RefreshTokenPrincipal);
_options.CurrentValue.Formatter.WriteToken(writer, context.UserCodePrincipal);
context.Response.RefreshToken = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Response.UserCode = Base64UrlEncoder.Encode(protector.Protect(buffer.ToArray()));
context.Logger.LogTrace("The refresh token '{Identifier}' was successfully created and the " +
"following JWT payload was attached to the OpenID Connect response: {Payload}. " +
context.Logger.LogTrace("The user code '{Identifier}' was successfully created: {Payload}. " +
"The principal used to create the token contained the following claims: {Claims}.",
context.RefreshTokenPrincipal.GetClaim(Claims.JwtId),
context.Response.RefreshToken, context.RefreshTokenPrincipal.Claims);
context.UserCodePrincipal.GetClaim(Claims.JwtId),
context.Response.UserCode, context.UserCodePrincipal.Claims);
return default;
}

10
src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs

@ -109,6 +109,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerOwinBuilder EnableUserinfoEndpointPassthrough()
=> Configure(options => options.EnableUserinfoEndpointPassthrough = true);
/// <summary>
/// Enables the pass-through mode for the OpenID Connect user verification endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
/// <returns>The <see cref="OpenIddictServerOwinBuilder"/>.</returns>
public OpenIddictServerOwinBuilder EnableVerificationEndpointPassthrough()
=> Configure(options => options.EnableVerificationEndpointPassthrough = true);
/// <summary>
/// Enables request caching, so that both authorization and logout requests
/// are automatically stored in the distributed cache, which allows flowing

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs

@ -54,6 +54,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireRequestCachingEnabled>();
builder.Services.TryAddSingleton<RequireTokenEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireUserinfoEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireVerificationEndpointPassthroughEnabled>();
// Register the option initializers used by the OpenIddict OWIN server integration services.
// Note: TryAddEnumerable() is used here to ensure the initializers are only registered once.

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

@ -5,18 +5,17 @@
*/
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties;
namespace OpenIddict.Server.Owin
{
@ -25,21 +24,14 @@ namespace OpenIddict.Server.Owin
/// </summary>
public class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddictServerOwinOptions>
{
private readonly ILogger _logger;
private readonly IOpenIddictServerProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerOwinHandler"/> class.
/// </summary>
/// <param name="logger">The logger used by this instance.</param>
/// <param name="provider">The OpenIddict server OWIN provider used by this instance.</param>
public OpenIddictServerOwinHandler(
[NotNull] ILogger logger,
[NotNull] IOpenIddictServerProvider provider)
{
_logger = logger;
_provider = provider;
}
public OpenIddictServerOwinHandler([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
public override async Task<bool> InvokeAsync()
{
@ -104,7 +96,7 @@ namespace OpenIddict.Server.Owin
return false;
}
protected override Task<AuthenticationTicket> AuthenticateCoreAsync()
protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
{
var transaction = Context.Get<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName);
if (transaction == null)
@ -112,14 +104,34 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("An identity cannot be extracted from this request.");
}
if (transaction.Properties.TryGetValue(OpenIddictServerConstants.Properties.AmbientPrincipal, out var principal))
// Note: in many cases, the authentication token was already validated by the time this action is called
// (generally later in the pipeline, when using the pass-through mode). To avoid having to re-validate it,
// the authentication context is resolved from the transaction. If it's not available, a new one is created.
var context = transaction.GetProperty<ProcessAuthenticationContext>(typeof(ProcessAuthenticationContext).FullName);
if (context == null)
{
context = new ProcessAuthenticationContext(transaction);
await _provider.DispatchAsync(context);
}
if (context.IsRequestHandled || context.IsRequestSkipped)
{
return null;
}
else if (context.IsRejected)
{
return Task.FromResult(new AuthenticationTicket(
(ClaimsIdentity) ((ClaimsPrincipal) principal).Identity,
new AuthenticationProperties()));
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerOwinConstants.Properties.Error] = context.Error,
[OpenIddictServerOwinConstants.Properties.ErrorDescription] = context.ErrorDescription,
[OpenIddictServerOwinConstants.Properties.ErrorUri] = context.ErrorUri
});
return new AuthenticationTicket(null, properties);
}
return Task.FromResult<AuthenticationTicket>(null);
return null;
}
protected override async Task TeardownCoreAsync()
@ -145,14 +157,11 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
transaction.Properties[typeof(AuthenticationProperties).FullName] = challenge.Properties ?? new AuthenticationProperties();
var context = new ProcessChallengeContext(transaction)
{
Response = new OpenIddictResponse
{
Error = GetProperty(challenge.Properties, Properties.Error),
ErrorDescription = GetProperty(challenge.Properties, Properties.ErrorDescription),
ErrorUri = GetProperty(challenge.Properties, Properties.ErrorUri)
}
Response = new OpenIddictResponse()
};
await _provider.DispatchAsync(context);
@ -187,9 +196,6 @@ namespace OpenIddict.Server.Owin
.Append("was not registered or was explicitly removed from the handlers list.")
.ToString());
}
static string GetProperty(AuthenticationProperties properties, string name)
=> properties != null && properties.Dictionary.TryGetValue(name, out string value) ? value : null;
}
var signin = Helper.LookupSignIn(Options.AuthenticationType);
@ -201,6 +207,8 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
transaction.Properties[typeof(AuthenticationProperties).FullName] = signin.Properties ?? new AuthenticationProperties();
var context = new ProcessSigninContext(transaction)
{
Principal = signin.Principal,
@ -250,6 +258,8 @@ namespace OpenIddict.Server.Owin
throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.");
}
transaction.Properties[typeof(AuthenticationProperties).FullName] = signout.Properties ?? new AuthenticationProperties();
var context = new ProcessSignoutContext(transaction)
{
Response = new OpenIddictResponse()

22
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs

@ -184,5 +184,27 @@ namespace OpenIddict.Server.Owin
return new ValueTask<bool>(_options.CurrentValue.EnableUserinfoEndpointPassthrough);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if the
/// pass-through mode was not enabled for the verification endpoint.
/// </summary>
public class RequireVerificationEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictServerOwinOptions> _options;
public RequireVerificationEndpointPassthroughEnabled([NotNull] IOptionsMonitor<OpenIddictServerOwinOptions> options)
=> _options = options;
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(_options.CurrentValue.EnableVerificationEndpointPassthrough);
}
}
}
}

34
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
@ -20,7 +21,6 @@ using Microsoft.Owin.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
@ -364,12 +364,14 @@ namespace OpenIddict.Server.Owin
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
var key = _encoder.Encode(parameter.Key);
var value = _encoder.Encode(parameter.Value);
writer.WriteLine($@"<input type=""hidden"" name=""{key}"" value=""{value}"" />");
writer.WriteLine($@"<input type=""hidden"" name=""{_encoder.Encode(key)}"" value=""{_encoder.Encode(value)}"" />");
}
writer.WriteLine(@"<noscript>Click here to finish the authorization process: <input type=""submit"" /></noscript>");
@ -448,9 +450,14 @@ namespace OpenIddict.Server.Owin
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
location = WebUtilities.AddQueryString(location, parameter.Key, parameter.Value);
location = WebUtilities.AddQueryString(location, key, value);
}
response.Redirect(location);
@ -513,12 +520,17 @@ namespace OpenIddict.Server.Owin
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
builder.Append(Contains(builder, '#') ? '&' : '#')
.Append(Uri.EscapeDataString(parameter.Key))
.Append(Uri.EscapeDataString(key))
.Append('=')
.Append(Uri.EscapeDataString(parameter.Value));
.Append(Uri.EscapeDataString(value));
}
response.Redirect(builder.ToString());

47
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs

@ -0,0 +1,47 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters;
using static OpenIddict.Server.OpenIddictServerEvents;
namespace OpenIddict.Server.Owin
{
public static partial class OpenIddictServerOwinHandlers
{
public static class Device
{
public static ImmutableArray<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Device request extraction:
*/
ExtractGetOrPostRequest<ExtractDeviceRequestContext>.Descriptor,
/*
* Device response processing:
*/
ProcessJsonResponse<ApplyDeviceResponseContext>.Descriptor,
/*
* Verification request extraction:
*/
ExtractGetOrPostRequest<ExtractVerificationRequestContext>.Descriptor,
/*
* Verification request handling:
*/
EnablePassthroughMode<HandleVerificationRequestContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor,
/*
* Verification response processing:
*/
ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyVerificationResponseContext, RequireVerificationEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessEmptyResponse<ApplyVerificationResponseContext>.Descriptor);
}
}
}

12
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs

@ -7,6 +7,7 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
@ -19,7 +20,6 @@ using Microsoft.Owin.Infrastructure;
using Newtonsoft.Json;
using Newtonsoft.Json.Bson;
using Newtonsoft.Json.Linq;
using OpenIddict.Abstractions;
using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
@ -50,6 +50,7 @@ namespace OpenIddict.Server.Owin
*/
RemoveCachedRequest.Descriptor,
ProcessQueryResponse.Descriptor,
ProcessHostRedirectionResponse<ApplyVerificationResponseContext>.Descriptor,
ProcessPassthroughErrorResponse<ApplyLogoutResponseContext, RequireLogoutEndpointPassthroughEnabled>.Descriptor,
ProcessLocalErrorResponse<ApplyLogoutResponseContext>.Descriptor,
ProcessEmptyResponse<ApplyLogoutResponseContext>.Descriptor);
@ -344,9 +345,14 @@ namespace OpenIddict.Server.Owin
// Note: while initially not allowed by the core OAuth 2.0 specification, multiple parameters
// with the same name are used by derived drafts like the OAuth 2.0 token exchange specification.
// For consistency, multiple parameters with the same name are also supported by this endpoint.
foreach (var parameter in context.Response.GetFlattenedParameters())
foreach (var (key, value) in
from parameter in context.Response.GetParameters()
let values = (string[]) parameter.Value
where values != null
from value in values
select (parameter.Key, Value: value))
{
location = WebUtilities.AddQueryString(location, parameter.Key, parameter.Value);
location = WebUtilities.AddQueryString(location, key, value);
}
response.Redirect(location);

110
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs

@ -14,12 +14,15 @@ using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Newtonsoft.Json;
using OpenIddict.Abstractions;
using Owin;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlers;
using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters;
using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties;
namespace OpenIddict.Server.Owin
{
@ -32,8 +35,14 @@ namespace OpenIddict.Server.Owin
*/
InferEndpointType.Descriptor,
InferIssuerFromHost.Descriptor,
ValidateTransportSecurityRequirement.Descriptor)
ValidateTransportSecurityRequirement.Descriptor,
/*
* Challenge processing:
*/
AttachHostChallengeError.Descriptor)
.AddRange(Authentication.DefaultHandlers)
.AddRange(Device.DefaultHandlers)
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers)
.AddRange(Introspection.DefaultHandlers)
@ -85,11 +94,13 @@ namespace OpenIddict.Server.Owin
Matches(context.Options.AuthorizationEndpointUris) ? OpenIddictServerEndpointType.Authorization :
Matches(context.Options.ConfigurationEndpointUris) ? OpenIddictServerEndpointType.Configuration :
Matches(context.Options.CryptographyEndpointUris) ? OpenIddictServerEndpointType.Cryptography :
Matches(context.Options.DeviceEndpointUris) ? OpenIddictServerEndpointType.Device :
Matches(context.Options.IntrospectionEndpointUris) ? OpenIddictServerEndpointType.Introspection :
Matches(context.Options.LogoutEndpointUris) ? OpenIddictServerEndpointType.Logout :
Matches(context.Options.RevocationEndpointUris) ? OpenIddictServerEndpointType.Revocation :
Matches(context.Options.TokenEndpointUris) ? OpenIddictServerEndpointType.Token :
Matches(context.Options.UserinfoEndpointUris) ? OpenIddictServerEndpointType.Userinfo :
Matches(context.Options.VerificationEndpointUris) ? OpenIddictServerEndpointType.Verification :
OpenIddictServerEndpointType.Unknown;
return default;
@ -264,6 +275,51 @@ namespace OpenIddict.Server.Owin
}
}
/// <summary>
/// Contains the logic responsible of attaching the error details using the OWIN authentication properties.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class AttachHostChallengeError : IOpenIddictServerHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<AttachHostChallengeError>()
.SetOrder(AttachDefaultChallengeError.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ProcessChallengeContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties)
{
context.Response.Error = GetProperty(properties, Properties.Error);
context.Response.ErrorDescription = GetProperty(properties, Properties.ErrorDescription);
context.Response.ErrorUri = GetProperty(properties, Properties.ErrorUri);
}
return default;
static string GetProperty(AuthenticationProperties properties, string name)
=> properties.Dictionary.TryGetValue(name, out string value) ? value : null;
}
}
/// <summary>
/// Contains the logic responsible of extracting OpenID Connect requests from GET HTTP requests.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
@ -684,6 +740,58 @@ namespace OpenIddict.Server.Owin
}
}
/// <summary>
/// Contains the logic responsible of processing empty OpenID Connect responses that should trigger a host redirection.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
/// </summary>
public class ProcessHostRedirectionResponse<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ProcessHostRedirectionResponse<TContext>>()
.SetOrder(ProcessJsonResponse<TContext>.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetOwinRequest()?.Context.Response;
if (response == null)
{
throw new InvalidOperationException("The OWIN request cannot be resolved.");
}
if (context.Transaction.Properties.TryGetValue(typeof(AuthenticationProperties).FullName, out var property) &&
property is AuthenticationProperties properties && !string.IsNullOrEmpty(properties.RedirectUri))
{
response.Redirect(properties.RedirectUri);
context.Logger.LogInformation("The response was successfully returned as a 302 response.");
context.HandleRequest();
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.

11
src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs

@ -5,7 +5,6 @@
*/
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Owin;
using Microsoft.Owin.Security.Infrastructure;
@ -20,32 +19,26 @@ namespace OpenIddict.Server.Owin
/// </summary>
public class OpenIddictServerOwinMiddleware : AuthenticationMiddleware<OpenIddictServerOwinOptions>
{
private readonly ILogger<OpenIddictServerOwinMiddleware> _logger;
private readonly IOpenIddictServerProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerOwinMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the pipeline, if applicable.</param>
/// <param name="logger">The logger used by this middleware.</param>
/// <param name="options">The OpenIddict server OWIN options.</param>
/// <param name="provider">The OpenIddict server provider.</param>
public OpenIddictServerOwinMiddleware(
[CanBeNull] OwinMiddleware next,
[NotNull] ILogger<OpenIddictServerOwinMiddleware> logger,
[NotNull] IOptionsMonitor<OpenIddictServerOwinOptions> options,
[NotNull] IOpenIddictServerProvider provider)
: base(next, options.CurrentValue)
{
_logger = logger;
_provider = provider;
}
=> _provider = provider;
/// <summary>
/// Creates and returns a new <see cref="OpenIddictServerOwinHandler"/> instance.
/// </summary>
/// <returns>A new instance of the <see cref="OpenIddictServerOwinHandler"/> class.</returns>
protected override AuthenticationHandler<OpenIddictServerOwinOptions> CreateHandler()
=> new OpenIddictServerOwinHandler(_logger, _provider);
=> new OpenIddictServerOwinHandler(_provider);
}
}

1
src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs

@ -64,7 +64,6 @@ namespace OpenIddict.Server.Owin
// To work around this limitation, the server OWIN middleware is manually instantiated and invoked.
var middleware = new OpenIddictServerOwinMiddleware(
next: Next,
logger: GetRequiredService<ILogger<OpenIddictServerOwinMiddleware>>(provider),
options: GetRequiredService<IOptionsMonitor<OpenIddictServerOwinOptions>>(provider),
provider: GetRequiredService<IOpenIddictServerProvider>(provider));

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

@ -70,6 +70,14 @@ namespace OpenIddict.Server.Owin
/// </summary>
public bool EnableUserinfoEndpointPassthrough { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the pass-through mode is enabled for the user verification endpoint.
/// When the pass-through mode is used, OpenID Connect requests are initially handled by OpenIddict.
/// Once validated, the rest of the request processing pipeline is invoked, so that OpenID Connect requests
/// can be handled at a later stage (in a custom middleware or in a MVC controller, for instance).
/// </summary>
public bool EnableVerificationEndpointPassthrough { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether request caching should be enabled.
/// When enabled, both authorization and logout requests are automatically stored

148
src/OpenIddict.Server/OpenIddictServerBuilder.cs

@ -1067,6 +1067,14 @@ namespace Microsoft.Extensions.DependencyInjection
return Configure(options => options.GrantTypes.Add(type));
}
/// <summary>
/// Enables device code flow support. For more information about this
/// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc8628.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder AllowDeviceCodeFlow()
=> Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.DeviceCode));
/// <summary>
/// Enables implicit flow support. For more information
/// about this specific OAuth 2.0/OpenID Connect flow, visit
@ -1249,6 +1257,58 @@ namespace Microsoft.Extensions.DependencyInjection
});
}
/// <summary>
/// Sets the relative or absolute URLs associated to the device endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first address will be returned as part of the discovery document.
/// </summary>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetDeviceEndpointUris([NotNull] params string[] addresses)
{
if (addresses == null)
{
throw new ArgumentNullException(nameof(addresses));
}
return SetDeviceEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray());
}
/// <summary>
/// Sets the relative or absolute URLs associated to the device endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first address will be returned as part of the discovery document.
/// </summary>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetDeviceEndpointUris([NotNull] params Uri[] addresses)
{
if (addresses == null)
{
throw new ArgumentNullException(nameof(addresses));
}
if (addresses.Any(address => !address.IsWellFormedOriginalString()))
{
throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses));
}
if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses));
}
return Configure(options =>
{
options.DeviceEndpointUris.Clear();
foreach (var address in addresses)
{
options.DeviceEndpointUris.Add(address);
}
});
}
/// <summary>
/// Sets the relative or absolute URLs associated to the introspection endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
@ -1509,6 +1569,58 @@ namespace Microsoft.Extensions.DependencyInjection
});
}
/// <summary>
/// Sets the relative or absolute URLs associated to the verification endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first address will be returned by the device endpoint.
/// </summary>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetVerificationEndpointUris([NotNull] params string[] addresses)
{
if (addresses == null)
{
throw new ArgumentNullException(nameof(addresses));
}
return SetVerificationEndpointUris(addresses.Select(address => new Uri(address, UriKind.RelativeOrAbsolute)).ToArray());
}
/// <summary>
/// Sets the relative or absolute URLs associated to the verification endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first address will be returned by the device endpoint.
/// </summary>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetVerificationEndpointUris([NotNull] params Uri[] addresses)
{
if (addresses == null)
{
throw new ArgumentNullException(nameof(addresses));
}
if (addresses.Any(address => !address.IsWellFormedOriginalString()))
{
throw new ArgumentException("One of the specified addresses is not valid.", nameof(addresses));
}
if (addresses.Any(address => !address.IsAbsoluteUri && !address.OriginalString.StartsWith("/", StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException("Relative URLs must start with a '/'.", nameof(addresses));
}
return Configure(options =>
{
options.VerificationEndpointUris.Clear();
foreach (var address in addresses)
{
options.VerificationEndpointUris.Add(address);
}
});
}
/// <summary>
/// Disables authorization storage so that ad-hoc authorizations are
/// not created when an authorization code or refresh token is issued
@ -1644,6 +1756,17 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder SetAuthorizationCodeLifetime([CanBeNull] TimeSpan? lifetime)
=> Configure(options => options.AuthorizationCodeLifetime = lifetime);
/// <summary>
/// Sets the device code lifetime, after which client applications are unable to
/// send a grant_type=urn:ietf:params:oauth:grant-type:device_code token request.
/// Using short-lived device codes is strongly recommended.
/// While discouraged, <c>null</c> can be specified to issue codes that never expire.
/// </summary>
/// <param name="lifetime">The authorization code lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetDeviceCodeLifetime([CanBeNull] TimeSpan? lifetime)
=> Configure(options => options.DeviceCodeLifetime = lifetime);
/// <summary>
/// Sets the identity token lifetime, after which client
/// applications should refuse processing identity tokens.
@ -1666,6 +1789,16 @@ namespace Microsoft.Extensions.DependencyInjection
public OpenIddictServerBuilder SetRefreshTokenLifetime([CanBeNull] TimeSpan? lifetime)
=> Configure(options => options.RefreshTokenLifetime = lifetime);
/// <summary>
/// Sets the user code lifetime, after which they'll no longer be considered valid.
/// Using short-lived device codes is strongly recommended.
/// While discouraged, <c>null</c> can be specified to issue codes that never expire.
/// </summary>
/// <param name="lifetime">The authorization code lifetime.</param>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder SetUserCodeLifetime([CanBeNull] TimeSpan? lifetime)
=> Configure(options => options.UserCodeLifetime = lifetime);
/// <summary>
/// Sets the issuer address, which is used as the base address
/// for the endpoint URIs returned from the discovery endpoint.
@ -1698,17 +1831,14 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// Configures OpenIddict to use reference tokens, so that authorization codes,
/// access tokens and refresh tokens are stored as ciphertext in the database
/// (only an identifier is returned to the client application). Enabling this option
/// is useful to keep track of all the issued tokens, when storing a very large
/// number of claims in the authorization codes, access tokens and refresh tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// Configures OpenIddict to use reference tokens, so that access tokens are stored
/// as ciphertext in the database (only an identifier is returned to the client application).
/// Enabling this option is useful to keep track of all the issued tokens, when storing
/// a very large number of claims in the access tokens or when immediate revocation is desired.
/// </summary>
/// <returns>The <see cref="OpenIddictServerBuilder"/>.</returns>
public OpenIddictServerBuilder UseReferenceTokens()
=> Configure(options => options.UseReferenceTokens = true);
public OpenIddictServerBuilder UseReferenceAccessTokens()
=> Configure(options => options.UseReferenceAccessTokens = true);
/// <summary>
/// Configures OpenIddict to use rolling refresh tokens. When this option is enabled,

102
src/OpenIddict.Server/OpenIddictServerConfiguration.cs

@ -56,34 +56,57 @@ namespace OpenIddict.Server
.ToString());
}
// Ensure the device endpoint has been enabled when the device grant is supported.
if (options.DeviceEndpointUris.Count == 0 && options.GrantTypes.Contains(GrantTypes.DeviceCode))
{
throw new InvalidOperationException("The device endpoint must be enabled to use the device flow.");
}
// Ensure the token endpoint has been enabled when the authorization code,
// client credentials, password or refresh token grants are supported.
// client credentials, device, password or refresh token grants are supported.
if (options.TokenEndpointUris.Count == 0 && (options.GrantTypes.Contains(GrantTypes.AuthorizationCode) ||
options.GrantTypes.Contains(GrantTypes.ClientCredentials) ||
options.GrantTypes.Contains(GrantTypes.DeviceCode) ||
options.GrantTypes.Contains(GrantTypes.Password) ||
options.GrantTypes.Contains(GrantTypes.RefreshToken)))
{
throw new InvalidOperationException(new StringBuilder()
.Append("The token endpoint must be enabled to use the authorization code, ")
.Append("client credentials, password and refresh token flows.")
.Append("client credentials, device, password and refresh token flows.")
.ToString());
}
if (options.RevocationEndpointUris.Count != 0 && options.DisableTokenStorage)
// Ensure the verification endpoint has been enabled when the device grant is supported.
if (options.VerificationEndpointUris.Count == 0 && options.GrantTypes.Contains(GrantTypes.DeviceCode))
{
throw new InvalidOperationException("The revocation endpoint cannot be enabled when token storage is disabled.");
throw new InvalidOperationException("The verification endpoint must be enabled to use the device flow.");
}
if (options.UseReferenceTokens && options.DisableTokenStorage)
if (options.DisableTokenStorage)
{
throw new InvalidOperationException("Reference tokens cannot be used when disabling token storage.");
}
if (options.DeviceEndpointUris.Count != 0 || options.VerificationEndpointUris.Count != 0)
{
throw new InvalidOperationException(new StringBuilder()
.Append("The device and verification endpoints cannot be enabled when token storage is disabled.")
.ToString());
}
if (options.UseSlidingExpiration && options.DisableTokenStorage && !options.UseRollingTokens)
{
throw new InvalidOperationException(new StringBuilder()
.Append("Sliding expiration must be disabled when turning off token storage if rolling tokens are not used.")
.ToString());
if (options.RevocationEndpointUris.Count != 0)
{
throw new InvalidOperationException("The revocation endpoint cannot be enabled when token storage is disabled.");
}
if (options.UseReferenceAccessTokens)
{
throw new InvalidOperationException("Reference tokens cannot be used when disabling token storage.");
}
if (options.UseSlidingExpiration && !options.UseRollingTokens)
{
throw new InvalidOperationException(new StringBuilder()
.Append("Sliding expiration must be disabled when turning off token storage if rolling tokens are not used.")
.ToString());
}
}
if (options.EncryptionCredentials.Count == 0)
@ -106,10 +129,11 @@ namespace OpenIddict.Server
.ToString());
}
// If the degraded mode was enabled, ensure custom validation handlers
// have been registered for the endpoints that require manual validation.
if (options.EnableDegradedMode)
{
// If the degraded mode was enabled, ensure custom validation handlers
// have been registered for the endpoints that require manual validation.
if (options.AuthorizationEndpointUris.Count != 0 && !options.CustomHandlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateAuthorizationRequestContext) &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
@ -121,6 +145,17 @@ namespace OpenIddict.Server
.ToString());
}
if (options.DeviceEndpointUris.Count != 0 && !options.CustomHandlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateDeviceRequestContext) &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(new StringBuilder()
.Append("No custom device request validation handler was found. When enabling the degraded mode, ")
.Append("a custom 'IOpenIddictServerHandler<ValidateDeviceRequestContext>' must be implemented ")
.Append("to validate device requests (e.g to ensure the client_id and client_secret are valid).")
.ToString());
}
if (options.IntrospectionEndpointUris.Count != 0 && !options.CustomHandlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateIntrospectionRequestContext) &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
@ -164,6 +199,45 @@ namespace OpenIddict.Server
.Append("to validate token requests (e.g to ensure the client_id and client_secret are valid).")
.ToString());
}
if (options.VerificationEndpointUris.Count != 0 && !options.CustomHandlers.Any(
descriptor => descriptor.ContextType == typeof(ValidateVerificationRequestContext) &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(new StringBuilder()
.Append("No custom verification request validation handler was found. When enabling the degraded mode, ")
.Append("a custom 'IOpenIddictServerHandler<ValidateVerificationRequestContext>' must be implemented ")
.Append("to validate verification requests (e.g to ensure the user_code is valid).")
.ToString());
}
// If the degraded mode was enabled, ensure custom authentication/sign-in handlers
// have been registered to deal with device/user codes validation and generation.
if (options.GrantTypes.Contains(GrantTypes.DeviceCode))
{
if (!options.CustomHandlers.Any(
descriptor => descriptor.ContextType == typeof(ProcessAuthenticationContext) &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(new StringBuilder()
.Append("No custom verification authentication handler was found. When enabling the degraded mode, ")
.Append("a custom 'IOpenIddictServerHandler<ProcessAuthenticationContext>' must be implemented ")
.Append("to validate device and user codes (e.g by retrieving them from a database).")
.ToString());
}
if (!options.CustomHandlers.Any(
descriptor => descriptor.ContextType == typeof(ProcessSigninContext) &&
descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type))))
{
throw new InvalidOperationException(new StringBuilder()
.Append("No custom verification sign-in handler was found. When enabling the degraded mode, ")
.Append("a custom 'IOpenIddictServerHandler<ProcessSigninContext>' must be implemented ")
.Append("to generate device and user codes (e.g by retrieving them from a database).")
.ToString());
}
}
}
// Automatically add the offline_access scope if the refresh token grant has been enabled.

5
src/OpenIddict.Server/OpenIddictServerConstants.cs

@ -10,10 +10,7 @@ namespace OpenIddict.Server
{
public static class Properties
{
public const string AmbientPrincipal = ".ambient_principal";
public const string OriginalPrincipal = ".original_principal";
public const string ValidatedPostLogoutRedirectUri = ".validated_post_logout_redirect_uri";
public const string ValidatedRedirectUri = ".validated_redirect_uri";
public const string ReferenceTokenIdentifier = ".reference_token_identifier";
}
}
}

12
src/OpenIddict.Server/OpenIddictServerEndpointType.cs

@ -54,6 +54,16 @@ namespace OpenIddict.Server
/// <summary>
/// Revocation endpoint.
/// </summary>
Revocation = 8
Revocation = 8,
/// <summary>
/// Device endpoint.
/// </summary>
Device = 9,
/// <summary>
/// Verification endpoint.
/// </summary>
Verification = 10
}
}

14
src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs

@ -76,13 +76,6 @@ namespace OpenIddict.Server
RedirectUri = address;
}
/// <summary>
/// Gets or sets the security principal extracted from the id_token_hint, if available.
/// Note: the principal may not represent the user currently logged in,
/// so additional validation is strongly encouraged when using this property.
/// </summary>
public ClaimsPrincipal IdentityTokenHintPrincipal { get; set; }
}
/// <summary>
@ -98,13 +91,6 @@ namespace OpenIddict.Server
: base(transaction)
{
}
/// <summary>
/// Gets or sets the security principal extracted from the id_token_hint, if available.
/// Note: the principal may not represent the user currently logged in,
/// so additional validation is strongly encouraged when using this property.
/// </summary>
public ClaimsPrincipal IdentityTokenHintPrincipal { get; set; }
}
/// <summary>

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

@ -0,0 +1,151 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Security.Claims;
using JetBrains.Annotations;
namespace OpenIddict.Server
{
public static partial class OpenIddictServerEvents
{
/// <summary>
/// Represents an event called for each request to the device endpoint to give the user code
/// a chance to manually extract the device request from the ambient HTTP context.
/// </summary>
public class ExtractDeviceRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractDeviceRequestContext"/> class.
/// </summary>
public ExtractDeviceRequestContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each request to the device endpoint
/// to determine if the request is valid and should continue to be processed.
/// </summary>
public class ValidateDeviceRequestContext : BaseValidatingClientContext
{
/// <summary>
/// Creates a new instance of the <see cref="ValidateDeviceRequestContext"/> class.
/// </summary>
public ValidateDeviceRequestContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each validated device request
/// to allow the user code to decide how the request should be handled.
/// </summary>
public class HandleDeviceRequestContext : BaseValidatingTicketContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandleDeviceRequestContext"/> class.
/// </summary>
public HandleDeviceRequestContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called before the device response is returned to the caller.
/// </summary>
public class ApplyDeviceResponseContext : BaseRequestContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyDeviceResponseContext"/> class.
/// </summary>
public ApplyDeviceResponseContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets the error code returned to the client application.
/// When the response indicates a successful response,
/// this property returns <c>null</c>.
/// </summary>
public string Error => Response.Error;
}
/// <summary>
/// Represents an event called for each request to the verification endpoint to give the user code
/// a chance to manually extract the verification request from the ambient HTTP context.
/// </summary>
public class ExtractVerificationRequestContext : BaseValidatingContext
{
/// <summary>
/// Creates a new instance of the <see cref="ExtractVerificationRequestContext"/> class.
/// </summary>
public ExtractVerificationRequestContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called for each request to the verification endpoint
/// to determine if the request is valid and should continue to be processed.
/// </summary>
public class ValidateVerificationRequestContext : BaseValidatingClientContext
{
/// <summary>
/// Creates a new instance of the <see cref="ValidateVerificationRequestContext"/> class.
/// </summary>
public ValidateVerificationRequestContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets or sets the security principal extracted from the user code.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
}
/// <summary>
/// Represents an event called for each validated verification request
/// to allow the user code to decide how the request should be handled.
/// </summary>
public class HandleVerificationRequestContext : BaseValidatingTicketContext
{
/// <summary>
/// Creates a new instance of the <see cref="HandleVerificationRequestContext"/> class.
/// </summary>
public HandleVerificationRequestContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
}
/// <summary>
/// Represents an event called before the verification response is returned to the caller.
/// </summary>
public class ApplyVerificationResponseContext : BaseRequestContext
{
/// <summary>
/// Creates a new instance of the <see cref="ApplyVerificationResponseContext"/> class.
/// </summary>
public ApplyVerificationResponseContext([NotNull] OpenIddictServerTransaction transaction)
: base(transaction)
{
}
/// <summary>
/// Gets the error code returned to the client application.
/// When the response indicates a successful response,
/// this property returns <c>null</c>.
/// </summary>
public string Error => Response.Error;
}
}
}

5
src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs

@ -74,6 +74,11 @@ namespace OpenIddict.Server
/// </summary>
public Uri CryptographyEndpoint { get; set; }
/// <summary>
/// Gets or sets the device endpoint address.
/// </summary>
public Uri DeviceEndpoint { get; set; }
/// <summary>
/// Gets or sets the introspection endpoint address.
/// </summary>

7
src/OpenIddict.Server/OpenIddictServerEvents.Session.cs

@ -69,13 +69,6 @@ namespace OpenIddict.Server
PostLogoutRedirectUri = address;
}
/// <summary>
/// Gets or sets the security principal extracted from the id_token_hint, if available.
/// Note: the principal may not represent the user currently logged in,
/// so additional validation is strongly encouraged when using this property.
/// </summary>
public ClaimsPrincipal IdentityTokenHintPrincipal { get; set; }
}
/// <summary>

46
src/OpenIddict.Server/OpenIddictServerEvents.cs

@ -61,12 +61,6 @@ namespace OpenIddict.Server
/// </summary>
public OpenIddictServerOptions Options => Transaction.Options;
/// <summary>
/// Gets the dictionary containing the properties associated with this event.
/// </summary>
public IDictionary<string, object> Properties { get; }
= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the OpenIddict request or <c>null</c> if it couldn't be extracted.
/// </summary>
@ -311,6 +305,16 @@ namespace OpenIddict.Server
/// Gets or sets the security principal.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
/// <summary>
/// Gets or sets the token to validate.
/// </summary>
public string Token { get; set; }
/// <summary>
/// Gets or sets the expected type of the token.
/// </summary>
public string TokenType { get; set; }
}
/// <summary>
@ -356,6 +360,14 @@ namespace OpenIddict.Server
/// </summary>
public bool IncludeAuthorizationCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a device code
/// should be returned to the client application.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
/// </summary>
public bool IncludeDeviceCode { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether an identity token
/// should be returned to the client application.
@ -372,18 +384,32 @@ namespace OpenIddict.Server
/// </summary>
public bool IncludeRefreshToken { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a user code
/// should be returned to the client application.
/// Note: overriding the value of this property is generally not
/// recommended, except when dealing with non-standard clients.
/// </summary>
public bool IncludeUserCode { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the access token, if applicable.
/// </summary>
public ClaimsPrincipal AccessTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the authorization code, if applicable.
/// </summary>
public ClaimsPrincipal AuthorizationCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the device code, if applicable.
/// </summary>
public ClaimsPrincipal DeviceCodePrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the identity token, if applicable.
@ -395,6 +421,12 @@ namespace OpenIddict.Server
/// will be used to create the refresh token, if applicable.
/// </summary>
public ClaimsPrincipal RefreshTokenPrincipal { get; set; }
/// <summary>
/// Gets or sets the principal containing the claims that
/// will be used to create the user code, if applicable.
/// </summary>
public ClaimsPrincipal UserCodePrincipal { get; set; }
}
/// <summary>

5
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -48,12 +48,12 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireAuthorizationStorageEnabled>();
builder.Services.TryAddSingleton<RequireClientIdParameter>();
builder.Services.TryAddSingleton<RequireDegradedModeDisabled>();
builder.Services.TryAddSingleton<RequireDeviceCodeIncluded>();
builder.Services.TryAddSingleton<RequireEndpointPermissionsEnabled>();
builder.Services.TryAddSingleton<RequireGrantTypePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireIdentityTokenIncluded>();
builder.Services.TryAddSingleton<RequirePostLogoutRedirectUriParameter>();
builder.Services.TryAddSingleton<RequireReferenceTokensDisabled>();
builder.Services.TryAddSingleton<RequireReferenceTokensEnabled>();
builder.Services.TryAddSingleton<RequireReferenceAccessTokensEnabled>();
builder.Services.TryAddSingleton<RequireRefreshTokenIncluded>();
builder.Services.TryAddSingleton<RequireRollingTokensDisabled>();
builder.Services.TryAddSingleton<RequireRollingTokensEnabled>();
@ -61,6 +61,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireScopePermissionsEnabled>();
builder.Services.TryAddSingleton<RequireScopeValidationEnabled>();
builder.Services.TryAddSingleton<RequireTokenStorageEnabled>();
builder.Services.TryAddSingleton<RequireUserCodeIncluded>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<

54
src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs

@ -95,6 +95,22 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no device code is returned.
/// </summary>
public class RequireDeviceCodeIncluded : IOpenIddictServerHandlerFilter<ProcessSigninContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] ProcessSigninContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.IncludeDeviceCode);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if endpoint permissions were disabled.
/// </summary>
@ -160,9 +176,9 @@ namespace OpenIddict.Server
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are enabled.
/// Represents a filter that excludes the associated handlers if reference access tokens are disabled.
/// </summary>
public class RequireReferenceTokensDisabled : IOpenIddictServerHandlerFilter<BaseContext>
public class RequireReferenceAccessTokensEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
@ -171,23 +187,7 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(!context.Options.UseReferenceTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are disabled.
/// </summary>
public class RequireReferenceTokensEnabled : IOpenIddictServerHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.UseReferenceTokens);
return new ValueTask<bool>(context.Options.UseReferenceAccessTokens);
}
}
@ -302,5 +302,21 @@ namespace OpenIddict.Server
return new ValueTask<bool>(!context.Options.DisableTokenStorage);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no user code is returned.
/// </summary>
public class RequireUserCodeIncluded : IOpenIddictServerHandlerFilter<ProcessSigninContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] ProcessSigninContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.IncludeUserCode);
}
}
}
}

128
src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs

@ -49,7 +49,6 @@ namespace OpenIddict.Server
ValidateNonceParameter.Descriptor,
ValidatePromptParameter.Descriptor,
ValidateCodeChallengeParameters.Descriptor,
ValidateIdTokenHint.Descriptor,
ValidateClientId.Descriptor,
ValidateClientType.Descriptor,
ValidateClientRedirectUri.Descriptor,
@ -58,11 +57,6 @@ namespace OpenIddict.Server
ValidateGrantTypePermissions.Descriptor,
ValidateScopePermissions.Descriptor,
/*
* Authorization request handling:
*/
AttachIdentityTokenHintPrincipal.Descriptor,
/*
* Authorization response processing:
*/
@ -186,6 +180,10 @@ namespace OpenIddict.Server
var notification = new ValidateAuthorizationRequestContext(context.Transaction);
await _provider.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the redirect_uri without triggering a new validation process.
context.Transaction.SetProperty(typeof(ValidateAuthorizationRequestContext).FullName, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
@ -212,9 +210,6 @@ namespace OpenIddict.Server
throw new InvalidOperationException("The request cannot be validated because no client_id was specified.");
}
// Store the validated redirect_uri as an environment property.
context.Transaction.Properties[Properties.ValidatedRedirectUri] = notification.RedirectUri;
context.Logger.LogInformation("The authorization request was successfully validated.");
}
}
@ -992,75 +987,6 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Contains the logic responsible of rejecting authorization requests that don't specify a valid id_token_hint.
/// </summary>
public class ValidateIdTokenHint : IOpenIddictServerHandler<ValidateAuthorizationRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateIdTokenHint([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.UseScopedHandler<ValidateIdTokenHint>()
.SetOrder(ValidateCodeChallengeParameters.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ValidateAuthorizationRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request.IdTokenHint))
{
return;
}
var notification = new ProcessAuthenticationContext(context.Transaction);
await _provider.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;
}
// Attach the security principal extracted from the identity token to the
// validation context and store it as an environment property.
context.IdentityTokenHintPrincipal = notification.Principal;
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
}
}
/// <summary>
/// Contains the logic responsible of rejecting authorization requests that use an invalid client_id.
/// Note: this handler is not used when the degraded mode is enabled.
@ -1087,7 +1013,7 @@ namespace OpenIddict.Server
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateAuthorizationRequestContext>()
.AddFilter<RequireDegradedModeDisabled>()
.UseScopedHandler<ValidateClientId>()
.SetOrder(ValidateIdTokenHint.Descriptor.Order + 1_000)
.SetOrder(ValidateCodeChallengeParameters.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -1592,43 +1518,6 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Contains the logic responsible of attaching the principal extracted from the id_token_hint to the event context.
/// </summary>
public class AttachIdentityTokenHintPrincipal : IOpenIddictServerHandler<HandleAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleAuthorizationRequestContext>()
.UseSingletonHandler<AttachIdentityTokenHintPrincipal>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] HandleAuthorizationRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal))
{
context.IdentityTokenHintPrincipal ??= (ClaimsPrincipal) principal;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of inferring the redirect URL
/// used to send the response back to the client application.
@ -1663,11 +1552,14 @@ namespace OpenIddict.Server
return default;
}
var notification = context.Transaction.GetProperty<ValidateAuthorizationRequestContext>(
typeof(ValidateAuthorizationRequestContext).FullName);
// Note: at this stage, the validated redirect URI property may be null (e.g if an error
// is returned from the ExtractAuthorizationRequest/ValidateAuthorizationRequest events).
if (context.Transaction.Properties.TryGetValue(Properties.ValidatedRedirectUri, out var address))
if (!string.IsNullOrEmpty(notification?.RedirectUri))
{
context.RedirectUri = (string) address;
context.RedirectUri = notification.RedirectUri;
}
return default;

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

File diff suppressed because it is too large

12
src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs

@ -278,6 +278,7 @@ namespace OpenIddict.Server
[Metadata.EndSessionEndpoint] = notification.LogoutEndpoint?.AbsoluteUri,
[Metadata.RevocationEndpoint] = notification.RevocationEndpoint?.AbsoluteUri,
[Metadata.UserinfoEndpoint] = notification.UserinfoEndpoint?.AbsoluteUri,
[Metadata.DeviceAuthorizationEndpoint] = notification.DeviceEndpoint?.AbsoluteUri,
[Metadata.JwksUri] = notification.CryptographyEndpoint?.AbsoluteUri,
[Metadata.GrantTypesSupported] = notification.GrantTypes.ToArray(),
[Metadata.ResponseTypesSupported] = notification.ResponseTypes.ToArray(),
@ -394,6 +395,7 @@ namespace OpenIddict.Server
// and OpenID Connect discovery specifications only allow a single address per endpoint.
context.AuthorizationEndpoint ??= context.Options.AuthorizationEndpointUris.FirstOrDefault();
context.CryptographyEndpoint ??= context.Options.CryptographyEndpointUris.FirstOrDefault();
context.DeviceEndpoint ??= context.Options.DeviceEndpointUris.FirstOrDefault();
context.IntrospectionEndpoint ??= context.Options.IntrospectionEndpointUris.FirstOrDefault();
context.LogoutEndpoint ??= context.Options.LogoutEndpointUris.FirstOrDefault();
context.RevocationEndpoint ??= context.Options.RevocationEndpointUris.FirstOrDefault();
@ -423,6 +425,16 @@ namespace OpenIddict.Server
context.CryptographyEndpoint = new Uri(context.Issuer, context.CryptographyEndpoint);
}
if (context.DeviceEndpoint != null && !context.DeviceEndpoint.IsAbsoluteUri)
{
if (context.Issuer == null || !context.Issuer.IsAbsoluteUri)
{
throw new InvalidOperationException("An absolute URL cannot be built for the device endpoint path.");
}
context.DeviceEndpoint = new Uri(context.Issuer, context.DeviceEndpoint);
}
if (context.IntrospectionEndpoint != null && !context.IntrospectionEndpoint.IsAbsoluteUri)
{
if (context.Issuer == null || !context.Issuer.IsAbsoluteUri)

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

@ -7,7 +7,6 @@
using System;
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
@ -18,7 +17,6 @@ using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Server.OpenIddictServerEvents;
using static OpenIddict.Server.OpenIddictServerHandlerFilters;
using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties;
namespace OpenIddict.Server
{
@ -45,6 +43,7 @@ namespace OpenIddict.Server
ValidateClientIdParameter.Descriptor,
ValidateAuthorizationCodeParameter.Descriptor,
ValidateClientCredentialsParameters.Descriptor,
ValidateDeviceCodeParameter.Descriptor,
ValidateRefreshTokenParameter.Descriptor,
ValidatePasswordParameters.Descriptor,
ValidateScopes.Descriptor,
@ -181,6 +180,10 @@ namespace OpenIddict.Server
var notification = new ValidateTokenRequestContext(context.Transaction);
await _provider.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the principal without triggering a new validation process.
context.Transaction.SetProperty(typeof(ValidateTokenRequestContext).FullName, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
@ -202,9 +205,6 @@ namespace OpenIddict.Server
return;
}
// Store the security principal extracted from the authorization code/refresh token as an environment property.
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
context.Logger.LogInformation("The token request was successfully validated.");
}
}
@ -577,6 +577,50 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Contains the logic responsible of rejecting token requests that
/// don't specify a device code for the device code grant type.
/// </summary>
public class ValidateDeviceCodeParameter : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidateDeviceCodeParameter>()
.SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] ValidateTokenRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Reject grant_type=urn:ietf:params:oauth:grant-type:device_code requests missing the device code.
// See https://tools.ietf.org/html/rfc8628#section-3.4 for more information.
if (context.Request.IsDeviceCodeGrantType() && string.IsNullOrEmpty(context.Request.DeviceCode))
{
context.Reject(
error: Errors.InvalidRequest,
description: "The 'device_code' parameter is required when using the device code grant.");
return default;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of rejecting token requests that
/// specify invalid parameters for the refresh token grant type.
@ -589,7 +633,7 @@ namespace OpenIddict.Server
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
.UseSingletonHandler<ValidateRefreshTokenParameter>()
.SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000)
.SetOrder(ValidateDeviceCodeParameter.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -1190,8 +1234,8 @@ namespace OpenIddict.Server
}
/// <summary>
/// Contains the logic responsible of rejecting token requests
/// that don't specify a valid authorization code or refresh token.
/// Contains the logic responsible of rejecting token requests that don't
/// specify a valid authorization code, device code or refresh token.
/// </summary>
public class ValidateToken : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
@ -1223,7 +1267,9 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType())
if (!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsDeviceCodeGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
return;
}
@ -1231,6 +1277,10 @@ namespace OpenIddict.Server
var notification = new ProcessAuthenticationContext(context.Transaction);
await _provider.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the authentication result without triggering a new authentication flow.
context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
@ -1252,17 +1302,14 @@ namespace OpenIddict.Server
return;
}
// Attach the security principal extracted from the token to the
// validation context and store it as an environment property.
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.Principal;
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
context.Transaction.Properties[Properties.OriginalPrincipal] = notification.Principal.Clone(_ => true);
}
}
/// <summary>
/// Contains the logic responsible of rejecting token requests that use an authorization code
/// or a refresh token that was issued for a different client application.
/// Contains the logic responsible of rejecting token requests that use an authorization code,
/// a device code or a refresh token that was issued for a different client application.
/// </summary>
public class ValidatePresenters : IOpenIddictServerHandler<ValidateTokenRequestContext>
{
@ -1289,7 +1336,9 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType())
if (!context.Request.IsAuthorizationCodeGrantType() &&
!context.Request.IsDeviceCodeGrantType() &&
!context.Request.IsRefreshTokenGrantType())
{
return default;
}
@ -1298,46 +1347,57 @@ namespace OpenIddict.Server
if (presenters.Count == 0)
{
// 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 code grant request.
// was issued to a public client but cannot be null for an authorization or device code grant request.
if (context.Request.IsAuthorizationCodeGrantType())
{
throw new InvalidOperationException("The presenters list cannot be extracted from the authorization code.");
}
if (context.Request.IsDeviceCodeGrantType())
{
throw new InvalidOperationException("The presenters list cannot be extracted from the device code.");
}
return default;
}
// If at least one presenter was associated to the authorization code/refresh token,
// 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))
{
context.Logger.LogError("The token request was rejected because the client identifier of the application " +
"was not available and could not be compared to the presenters list stored " +
"in the authorization code or the refresh token.");
"in the authorization code, the device code or the refresh token.");
context.Reject(
error: Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code cannot be used without specifying a client identifier." :
"The specified refresh token cannot be used without specifying a client identifier.");
description:
context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code cannot be used without specifying a client identifier." :
context.Request.IsDeviceCodeGrantType() ?
"The specified device code cannot be used without specifying a client identifier." :
"The specified refresh token cannot be used without specifying a client identifier.");
return default;
}
// Ensure the authorization code/refresh token was issued to the client application making the token request.
// 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.LogError("The token request was rejected because the authorization code " +
context.Logger.LogError("The token request was rejected because the authorization code, the device code " +
"or the refresh token was issued to a different client application.");
context.Reject(
error: Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code cannot be used by this client application." :
"The specified refresh token cannot be used by this client application.");
description:
context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code cannot be used by this client application." :
context.Request.IsDeviceCodeGrantType() ?
"The specified device code cannot be used by this client application." :
"The specified refresh token cannot be used by this client application.");
return default;
}
@ -1647,10 +1707,11 @@ namespace OpenIddict.Server
return default;
}
if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal))
{
context.Principal ??= (ClaimsPrincipal) principal;
}
var notification = context.Transaction.GetProperty<ValidateTokenRequestContext>(
typeof(ValidateTokenRequestContext).FullName) ??
throw new InvalidOperationException("The authentication context cannot be found.");
context.Principal ??= notification.Principal;
return default;
}

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

@ -178,6 +178,10 @@ namespace OpenIddict.Server
var notification = new ValidateIntrospectionRequestContext(context.Transaction);
await _provider.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the principal without triggering a new validation process.
context.Transaction.SetProperty(typeof(ValidateIntrospectionRequestContext).FullName, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
@ -199,9 +203,6 @@ namespace OpenIddict.Server
return;
}
// Store the security principal extracted from the introspected token as an environment property.
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
context.Logger.LogInformation("The introspection request was successfully validated.");
}
}
@ -802,10 +803,8 @@ namespace OpenIddict.Server
return;
}
// Attach the security principal extracted from the token to the
// validation context and store it as an environment property.
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.Principal;
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
}
}
@ -951,10 +950,11 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal))
{
context.Principal ??= (ClaimsPrincipal) principal;
}
var notification = context.Transaction.GetProperty<ValidateIntrospectionRequestContext>(
typeof(ValidateIntrospectionRequestContext).FullName) ??
throw new InvalidOperationException("The authentication context cannot be found.");
context.Principal ??= notification.Principal;
return default;
}

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

@ -172,6 +172,10 @@ namespace OpenIddict.Server
var notification = new ValidateRevocationRequestContext(context.Transaction);
await _provider.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the principal without triggering a new validation process.
context.Transaction.SetProperty(typeof(ValidateRevocationRequestContext).FullName, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
@ -193,9 +197,6 @@ namespace OpenIddict.Server
return;
}
// Store the security principal extracted from the revoked token as an environment property.
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
context.Logger.LogInformation("The revocation request was successfully validated.");
}
}
@ -750,10 +751,8 @@ namespace OpenIddict.Server
return;
}
// Attach the security principal extracted from the token to the
// validation context and store it as an environment property.
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.Principal;
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
}
}
@ -899,10 +898,11 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal))
{
context.Principal ??= (ClaimsPrincipal) principal;
}
var notification = context.Transaction.GetProperty<ValidateRevocationRequestContext>(
typeof(ValidateRevocationRequestContext).FullName) ??
throw new InvalidOperationException("The authentication context cannot be found.");
context.Principal ??= notification.Principal;
return default;
}
@ -965,7 +965,7 @@ namespace OpenIddict.Server
}
// If the received token is an access token, return an error if reference tokens are not enabled.
if (context.Principal.IsAccessToken() && !context.Options.UseReferenceTokens)
if (context.Principal.IsAccessToken() && !context.Options.UseReferenceAccessTokens)
{
context.Logger.LogError("The revocation request was rejected because the access token was not revocable.");
@ -990,10 +990,10 @@ namespace OpenIddict.Server
}
var token = await _tokenManager.FindByIdAsync(identifier);
if (token == null || await _tokenManager.IsRevokedAsync(token))
if (token == null || await _tokenManager.HasStatusAsync(token, Statuses.Revoked))
{
context.Logger.LogInformation("The token '{Identifier}' was not revoked because " +
"it was already marked as invalid.", identifier);
"it was already marked as revoked.", identifier);
context.Reject(
error: Errors.InvalidToken,

129
src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs

@ -38,14 +38,8 @@ namespace OpenIddict.Server
* Logout request validation:
*/
ValidatePostLogoutRedirectUriParameter.Descriptor,
ValidateIdTokenHint.Descriptor,
ValidateClientPostLogoutRedirectUri.Descriptor,
/*
* Logout request handling:
*/
AttachIdentityTokenHintPrincipal.Descriptor,
/*
* Logout response processing:
*/
@ -168,6 +162,10 @@ namespace OpenIddict.Server
var notification = new ValidateLogoutRequestContext(context.Transaction);
await _provider.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the redirect_uri without triggering a new validation process.
context.Transaction.SetProperty(typeof(ValidateLogoutRequestContext).FullName, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
@ -189,12 +187,6 @@ namespace OpenIddict.Server
return;
}
if (!string.IsNullOrEmpty(notification.PostLogoutRedirectUri))
{
// Store the validated post_logout_redirect_uri as an environment property.
context.Transaction.Properties[Properties.ValidatedPostLogoutRedirectUri] = notification.PostLogoutRedirectUri;
}
context.Logger.LogInformation("The logout request was successfully validated.");
}
}
@ -412,75 +404,6 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Contains the logic responsible of rejecting logout requests that don't specify a valid id_token_hint.
/// </summary>
public class ValidateIdTokenHint : IOpenIddictServerHandler<ValidateLogoutRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateIdTokenHint([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateLogoutRequestContext>()
.UseScopedHandler<ValidateIdTokenHint>()
.SetOrder(ValidatePostLogoutRedirectUriParameter.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] ValidateLogoutRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (string.IsNullOrEmpty(context.Request.IdTokenHint))
{
return;
}
var notification = new ProcessAuthenticationContext(context.Transaction);
await _provider.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;
}
// Attach the security principal extracted from the identity token to the
// validation context and store it as an environment property.
context.IdentityTokenHintPrincipal = notification.Principal;
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
}
}
/// <summary>
/// Contains the logic responsible of rejecting logout requests that use an invalid redirect_uri.
/// Note: this handler is not used when the degraded mode is enabled.
@ -566,43 +489,6 @@ namespace OpenIddict.Server
}
}
/// <summary>
/// Contains the logic responsible of attaching the principal extracted from the id_token_hint to the event context.
/// </summary>
public class AttachIdentityTokenHintPrincipal : IOpenIddictServerHandler<HandleLogoutRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleLogoutRequestContext>()
.UseSingletonHandler<AttachIdentityTokenHintPrincipal>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] HandleLogoutRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal))
{
context.IdentityTokenHintPrincipal ??= (ClaimsPrincipal) principal;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of inferring the redirect URL
/// used to send the response back to the client application.
@ -637,11 +523,14 @@ namespace OpenIddict.Server
return default;
}
var notification = context.Transaction.GetProperty<ValidateLogoutRequestContext>(
typeof(ValidateLogoutRequestContext).FullName);
// Note: at this stage, the validated redirect URI property may be null (e.g if
// an error is returned from the ExtractLogoutRequest/ValidateLogoutRequest events).
if (context.Transaction.Properties.TryGetValue(Properties.ValidatedPostLogoutRedirectUri, out var address))
if (!string.IsNullOrEmpty(notification?.PostLogoutRedirectUri))
{
context.PostLogoutRedirectUri = (string) address;
context.PostLogoutRedirectUri = notification.PostLogoutRedirectUri;
}
return default;

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

@ -164,6 +164,10 @@ namespace OpenIddict.Server
var notification = new ValidateUserinfoRequestContext(context.Transaction);
await _provider.DispatchAsync(notification);
// Store the context object in the transaction so it can be later retrieved by handlers
// that want to access the principal without triggering a new validation process.
context.Transaction.SetProperty(typeof(ValidateUserinfoRequestContext).FullName, notification);
if (notification.IsRequestHandled)
{
context.HandleRequest();
@ -185,9 +189,6 @@ namespace OpenIddict.Server
return;
}
// Store the security principal extracted from the authorization code/refresh token as an environment property.
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
context.Logger.LogInformation("The userinfo request was successfully validated.");
}
}
@ -454,10 +455,8 @@ namespace OpenIddict.Server
return;
}
// Attach the security principal extracted from the token to the
// validation context and store it as an environment property.
// Attach the security principal extracted from the token to the validation context.
context.Principal = notification.Principal;
context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal;
}
}
@ -490,10 +489,11 @@ namespace OpenIddict.Server
throw new ArgumentNullException(nameof(context));
}
if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal))
{
context.Principal ??= (ClaimsPrincipal) principal;
}
var notification = context.Transaction.GetProperty<ValidateUserinfoRequestContext>(
typeof(ValidateUserinfoRequestContext).FullName) ??
throw new InvalidOperationException("The authentication context cannot be found.");
context.Principal ??= notification.Principal;
return default;
}

2191
src/OpenIddict.Server/OpenIddictServerHandlers.cs

File diff suppressed because it is too large

80
src/OpenIddict.Server/OpenIddictServerHelpers.cs

@ -0,0 +1,80 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System;
using JetBrains.Annotations;
namespace OpenIddict.Server
{
/// <summary>
/// Exposes extensions simplifying the integration with the OpenIddict server services.
/// </summary>
public static class OpenIddictServerHelpers
{
/// <summary>
/// Retrieves a property value from the server transaction using the specified name.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="transaction">The server transaction.</param>
/// <param name="name">The property name.</param>
/// <returns>The property value or <c>null</c> if it couldn't be found.</returns>
public static TProperty GetProperty<TProperty>(
[NotNull] this OpenIddictServerTransaction transaction, [NotNull] string name) where TProperty : class
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The property name cannot be null or empty.", nameof(name));
}
if (transaction.Properties.TryGetValue(name, out var property) && property is TProperty result)
{
return result;
}
return null;
}
/// <summary>
/// Sets a property in the server transaction using the specified name and value.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="transaction">The server transaction.</param>
/// <param name="name">The property name.</param>
/// <param name="value">The property value.</param>
/// <returns>The server transaction, so that calls can be easily chained.</returns>
public static OpenIddictServerTransaction SetProperty<TProperty>(
[NotNull] this OpenIddictServerTransaction transaction,
[NotNull] string name, [CanBeNull] TProperty value) where TProperty : class
{
if (transaction == null)
{
throw new ArgumentNullException(nameof(transaction));
}
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("The property name cannot be null or empty.", nameof(name));
}
if (value == null)
{
transaction.Properties.Remove(name);
}
else
{
transaction.Properties[name] = value;
}
return transaction;
}
}
}

44
src/OpenIddict.Server/OpenIddictServerOptions.cs

@ -58,6 +58,11 @@ namespace OpenIddict.Server
new Uri("/.well-known/jwks", UriKind.Relative)
};
/// <summary>
/// Gets the absolute and relative URIs associated to the device endpoint.
/// </summary>
public IList<Uri> DeviceEndpointUris { get; } = new List<Uri>();
/// <summary>
/// Gets the absolute and relative URIs associated to the introspection endpoint.
/// </summary>
@ -83,6 +88,11 @@ namespace OpenIddict.Server
/// </summary>
public IList<Uri> UserinfoEndpointUris { get; } = new List<Uri>();
/// <summary>
/// Gets the absolute and relative URIs associated to the verification endpoint.
/// </summary>
public IList<Uri> VerificationEndpointUris { get; } = new List<Uri>();
/// <summary>
/// Gets or sets the JWT handler used to protect and unprotect tokens.
/// </summary>
@ -117,6 +127,14 @@ namespace OpenIddict.Server
/// </summary>
public TimeSpan? AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1);
/// <summary>
/// Gets or sets the period of time device codes remain valid after being issued. The default value is 10 minutes.
/// The client application is expected to start a whole new authentication flow after the device code has expired.
/// While not recommended, this property can be set to <c>null</c> to issue codes that never expire.
/// Note: the same value should be chosen for both <see cref="UserCodeLifetime"/> and this property.
/// </summary>
public TimeSpan? DeviceCodeLifetime { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// Gets or sets the period of time identity tokens remain valid after being issued. The default value is 20 minutes.
/// The client application is expected to refresh or acquire a new identity token after the token has expired.
@ -131,6 +149,14 @@ namespace OpenIddict.Server
/// </summary>
public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14);
/// <summary>
/// Gets or sets the period of time user codes remain valid after being issued. The default value is 10 minutes.
/// The client application is expected to start a whole new authentication flow after the user code has expired.
/// While not recommended, this property can be set to <c>null</c> to issue codes that never expire.
/// Note: the same value should be chosen for both <see cref="DeviceCodeLifetime"/> and this property.
/// </summary>
public TimeSpan? UserCodeLifetime { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// Gets or sets a boolean indicating whether the degraded mode is enabled. When this degraded mode
/// is enabled, all the security checks that depend on the OpenIddict core managers are disabled.
@ -155,8 +181,8 @@ namespace OpenIddict.Server
/// <summary>
/// Gets or sets a boolean indicating whether new refresh tokens should be issued during a refresh token request.
/// Set this property to <c>true</c> to issue a new refresh token, <c>false</c> to prevent the OpenID Connect
/// server middleware from issuing new refresh tokens when receiving a grant_type=refresh_token request.
/// Set this property to <c>true</c> to issue a new refresh token, <c>false</c> to prevent OpenIddict
/// from issuing new refresh tokens when receiving a grant_type=refresh_token request.
/// </summary>
public bool UseSlidingExpiration { get; set; } = true;
@ -250,16 +276,14 @@ namespace OpenIddict.Server
};
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens should be used.
/// When set to <c>true</c>, authorization codes, access tokens and refresh tokens
/// are stored as ciphertext in the database and a crypto-secure random identifier
/// is returned to the client application. Enabling this option is useful
/// to keep track of all the issued tokens, when storing a very large number
/// of claims in the authorization codes, access tokens and refresh tokens
/// Gets or sets a boolean indicating whether reference access tokens should be used.
/// When set to <c>true</c>, access tokens and are stored as ciphertext in the database
/// and a crypto-secure random identifier is returned to the client application.
/// Enabling this option is useful to keep track of all the issued access tokens,
/// when storing a very large number of claims in the access tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// </summary>
public bool UseReferenceTokens { get; set; }
public bool UseReferenceAccessTokens { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether rolling tokens should be used.

131
src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs

@ -9,7 +9,6 @@ using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.AspNetCore.DataProtection;
@ -20,7 +19,6 @@ using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.DataProtection.OpenIddictValidationDataProtectionConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.OpenIddictValidationHandlerFilters;
using static OpenIddict.Validation.OpenIddictValidationHandlers;
namespace OpenIddict.Validation.DataProtection
@ -32,132 +30,16 @@ namespace OpenIddict.Validation.DataProtection
/*
* Authentication processing:
*/
ValidateReferenceDataProtectionToken.Descriptor,
ValidateSelfContainedDataProtectionToken.Descriptor);
ValidateDataProtectionToken.Descriptor);
/// <summary>
/// Contains the logic responsible of rejecting authentication
/// demands that use an invalid reference Data Protection token.
/// Note: this handler is not used when the degraded mode is enabled.
/// Contains the logic responsible of validating tokens generated using Data Protection.
/// </summary>
public class ValidateReferenceDataProtectionToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
public class ValidateDataProtectionToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
private readonly IOptionsMonitor<OpenIddictValidationDataProtectionOptions> _options;
public ValidateReferenceDataProtectionToken() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling the OpenIddict server feature.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.Append("Alternatively, you can disable the built-in database-based server features by enabling ")
.Append("the degraded mode with 'services.AddOpenIddict().AddValidation().EnableDegradedMode()'.")
.ToString());
public ValidateReferenceDataProtectionToken(
[NotNull] IOpenIddictTokenManager tokenManager,
[NotNull] IOptionsMonitor<OpenIddictValidationDataProtectionOptions> options)
{
_tokenManager = tokenManager;
_options = options;
}
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensEnabled>()
.UseScopedHandler<ValidateReferenceDataProtectionToken>()
.SetOrder(ValidateReferenceToken.Descriptor.Order + 500)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal != null)
{
return;
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var identifier = context.Request.AccessToken;
if (string.IsNullOrEmpty(identifier))
{
return;
}
var token = await _tokenManager.FindByReferenceIdAsync(identifier);
if (token == null || !string.Equals(await _tokenManager.GetTypeAsync(token),
TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase))
{
return;
}
var payload = await _tokenManager.GetPayloadAsync(token);
if (string.IsNullOrEmpty(payload))
{
throw new InvalidOperationException(new StringBuilder()
.AppendLine("The payload associated with a reference token cannot be retrieved.")
.Append("This may indicate that the token entry was corrupted.")
.ToString());
}
// Create a Data Protection protector using the provider registered in the options.
var protector = _options.CurrentValue.DataProtectionProvider.CreateProtector(
Purposes.Handlers.Server,
Purposes.Formats.AccessToken,
Purposes.Features.ReferenceTokens,
Purposes.Schemes.Server);
ClaimsPrincipal principal = null;
try
{
using var buffer = new MemoryStream(protector.Unprotect(Base64UrlEncoder.DecodeBytes(payload)));
using var reader = new BinaryReader(buffer);
principal = _options.CurrentValue.Formatter.ReadToken(reader);
}
catch (Exception exception)
{
context.Logger.LogTrace(exception, "An exception occured while deserializing the token '{Token}'.", payload);
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (principal == null)
{
return;
}
// Attach the principal extracted from the authorization code to the parent event context
// and restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal = principal
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetInternalTokenId(await _tokenManager.GetIdAsync(token))
.SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token));
context.Logger.LogTrace("The reference DP token '{Token}' was successfully validated and the following " +
"claims could be extracted: {Claims}.", payload, context.Principal.Claims);
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands
/// that specify an invalid self-contained Data Protection token.
/// </summary>
public class ValidateSelfContainedDataProtectionToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOptionsMonitor<OpenIddictValidationDataProtectionOptions> _options;
public ValidateSelfContainedDataProtectionToken([NotNull] IOptionsMonitor<OpenIddictValidationDataProtectionOptions> options)
public ValidateDataProtectionToken([NotNull] IOptionsMonitor<OpenIddictValidationDataProtectionOptions> options)
=> _options = options;
/// <summary>
@ -165,9 +47,8 @@ namespace OpenIddict.Validation.DataProtection
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensDisabled>()
.UseSingletonHandler<ValidateSelfContainedDataProtectionToken>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order + 500)
.UseSingletonHandler<ValidateDataProtectionToken>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500)
.Build();
/// <summary>

16
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs

@ -9,7 +9,6 @@ using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
@ -25,21 +24,14 @@ namespace OpenIddict.Validation.Owin
/// </summary>
public class OpenIddictValidationOwinHandler : AuthenticationHandler<OpenIddictValidationOwinOptions>
{
private readonly ILogger _logger;
private readonly IOpenIddictValidationProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOwinHandler"/> class.
/// </summary>
/// <param name="logger">The logger used by this instance.</param>
/// <param name="provider">The OpenIddict validation OWIN provider used by this instance.</param>
public OpenIddictValidationOwinHandler(
[NotNull] ILogger logger,
[NotNull] IOpenIddictValidationProvider provider)
{
_logger = logger;
_provider = provider;
}
public OpenIddictValidationOwinHandler([NotNull] IOpenIddictValidationProvider provider)
=> _provider = provider;
public override async Task<bool> InvokeAsync()
{
@ -122,10 +114,6 @@ namespace OpenIddict.Validation.Owin
else if (context.IsRejected)
{
_logger.LogError("An error occurred while authenticating the current request: {Error} ; {Description}",
/* Error: */ context.Error ?? Errors.InvalidToken,
/* Description: */ context.ErrorDescription);
return new AuthenticationTicket(identity: null, new AuthenticationProperties
{
Dictionary =

11
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs

@ -5,7 +5,6 @@
*/
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Owin;
using Microsoft.Owin.Security.Infrastructure;
@ -20,32 +19,26 @@ namespace OpenIddict.Validation.Owin
/// </summary>
public class OpenIddictValidationOwinMiddleware : AuthenticationMiddleware<OpenIddictValidationOwinOptions>
{
private readonly ILogger<OpenIddictValidationOwinMiddleware> _logger;
private readonly IOpenIddictValidationProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOwinMiddleware"/> class.
/// </summary>
/// <param name="next">The next middleware in the pipeline, if applicable.</param>
/// <param name="logger">The logger used by this middleware.</param>
/// <param name="options">The OpenIddict validation OWIN options.</param>
/// <param name="provider">The OpenIddict validation provider.</param>
public OpenIddictValidationOwinMiddleware(
[CanBeNull] OwinMiddleware next,
[NotNull] ILogger<OpenIddictValidationOwinMiddleware> logger,
[NotNull] IOptionsMonitor<OpenIddictValidationOwinOptions> options,
[NotNull] IOpenIddictValidationProvider provider)
: base(next, options.CurrentValue)
{
_logger = logger;
_provider = provider;
}
=> _provider = provider;
/// <summary>
/// Creates and returns a new <see cref="OpenIddictValidationOwinHandler"/> instance.
/// </summary>
/// <returns>A new instance of the <see cref="OpenIddictValidationOwinHandler"/> class.</returns>
protected override AuthenticationHandler<OpenIddictValidationOwinOptions> CreateHandler()
=> new OpenIddictValidationOwinHandler(_logger, _provider);
=> new OpenIddictValidationOwinHandler(_provider);
}
}

1
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddlewareFactory.cs

@ -64,7 +64,6 @@ namespace OpenIddict.Validation.Owin
// To work around this limitation, the validation OWIN middleware is manually instantiated and invoked.
var middleware = new OpenIddictValidationOwinMiddleware(
next: Next,
logger: GetRequiredService<ILogger<OpenIddictValidationOwinMiddleware>>(provider),
options: GetRequiredService<IOptionsMonitor<OpenIddictValidationOwinOptions>>(provider),
provider: GetRequiredService<IOpenIddictValidationProvider>(provider));

2
src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs

@ -57,7 +57,7 @@ namespace OpenIddict.Validation.ServerIntegration
}
}
options.UseReferenceTokens = _options.CurrentValue.UseReferenceTokens;
options.UseReferenceAccessTokens = _options.CurrentValue.UseReferenceAccessTokens;
}
}
}

2
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

@ -60,7 +60,7 @@ namespace OpenIddict.Validation.SystemNetHttp
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<PopulateTokenValidationParameters>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order - 500)
.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)

15
src/OpenIddict.Validation/OpenIddictValidationBuilder.cs

@ -620,17 +620,14 @@ namespace Microsoft.Extensions.DependencyInjection
}
/// <summary>
/// Configures OpenIddict to use reference tokens, so that authorization codes,
/// access tokens and refresh tokens are stored as ciphertext in the database
/// (only an identifier is returned to the client application). Enabling this option
/// is useful to keep track of all the issued tokens, when storing a very large
/// number of claims in the authorization codes, access tokens and refresh tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// Configures OpenIddict to use reference tokens, so that access tokens are stored
/// as ciphertext in the database (only an identifier is returned to the client application).
/// Enabling this option is useful to keep track of all the issued tokens, when storing
/// a very large number of claims in the access tokens or when immediate revocation is desired.
/// </summary>
/// <returns>The <see cref="OpenIddictValidationBuilder"/>.</returns>
public OpenIddictValidationBuilder UseReferenceTokens()
=> Configure(options => options.UseReferenceTokens = true);
public OpenIddictValidationBuilder UseReferenceAccessTokens()
=> Configure(options => options.UseReferenceAccessTokens = true);
/// <summary>
/// Determines whether the specified object is equal to the current object.

16
src/OpenIddict.Validation/OpenIddictValidationConstants.cs

@ -0,0 +1,16 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Validation
{
public static class OpenIddictValidationConstants
{
public static class Properties
{
public const string ReferenceTokenIdentifier = ".reference_token_identifier";
}
}
}

5
src/OpenIddict.Validation/OpenIddictValidationEvents.cs

@ -251,6 +251,11 @@ namespace OpenIddict.Validation
/// Gets or sets the security principal.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
/// <summary>
/// Gets or sets the token to validate.
/// </summary>
public string Token { get; set; }
}
/// <summary>

3
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -44,8 +44,7 @@ namespace Microsoft.Extensions.DependencyInjection
// Register the built-in filters used by the default OpenIddict validation event handlers.
builder.Services.TryAddSingleton<RequireAuthorizationValidationEnabled>();
builder.Services.TryAddSingleton<RequireReferenceTokensDisabled>();
builder.Services.TryAddSingleton<RequireReferenceTokensEnabled>();
builder.Services.TryAddSingleton<RequireReferenceAccessTokensEnabled>();
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<

22
src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs

@ -32,9 +32,9 @@ namespace OpenIddict.Validation
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are enabled.
/// Represents a filter that excludes the associated handlers if reference access tokens are disabled.
/// </summary>
public class RequireReferenceTokensDisabled : IOpenIddictValidationHandlerFilter<BaseContext>
public class RequireReferenceAccessTokensEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
@ -43,23 +43,7 @@ namespace OpenIddict.Validation
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(!context.Options.UseReferenceTokens);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if reference tokens are disabled.
/// </summary>
public class RequireReferenceTokensEnabled : IOpenIddictValidationHandlerFilter<BaseContext>
{
public ValueTask<bool> IsActiveAsync([NotNull] BaseContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
return new ValueTask<bool>(context.Options.UseReferenceTokens);
return new ValueTask<bool>(context.Options.UseReferenceAccessTokens);
}
}
}

155
src/OpenIddict.Validation/OpenIddictValidationHandlers.cs

@ -18,6 +18,7 @@ using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Validation.OpenIddictValidationEvents;
using static OpenIddict.Validation.OpenIddictValidationHandlerFilters;
using Properties = OpenIddict.Validation.OpenIddictValidationConstants.Properties;
namespace OpenIddict.Validation
{
@ -29,8 +30,9 @@ namespace OpenIddict.Validation
* Authentication processing:
*/
ValidateAccessTokenParameter.Descriptor,
ValidateReferenceToken.Descriptor,
ValidateSelfContainedToken.Descriptor,
ValidateReferenceTokenIdentifier.Descriptor,
ValidateIdentityModelToken.Descriptor,
RestoreReferenceTokenProperties.Descriptor,
ValidatePrincipal.Descriptor,
ValidateExpirationDate.Descriptor,
ValidateAudience.Descriptor,
@ -80,25 +82,27 @@ namespace OpenIddict.Validation
return default;
}
context.Token = context.Request.AccessToken;
return default;
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands that use an invalid reference token.
/// Contains the logic responsible of validating reference token identifiers.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class ValidateReferenceToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
public class ValidateReferenceTokenIdentifier : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public ValidateReferenceToken() => throw new InvalidOperationException(new StringBuilder()
public ValidateReferenceTokenIdentifier() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling reference tokens support.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.ToString());
public ValidateReferenceToken([NotNull] IOpenIddictTokenManager tokenManager)
public ValidateReferenceTokenIdentifier([NotNull] IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager;
/// <summary>
@ -106,8 +110,8 @@ namespace OpenIddict.Validation
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceTokensEnabled>()
.UseScopedHandler<ValidateReferenceToken>()
.AddFilter<RequireReferenceAccessTokensEnabled>()
.UseScopedHandler<ValidateReferenceTokenIdentifier>()
.SetOrder(ValidateAccessTokenParameter.Descriptor.Order + 1_000)
.Build();
@ -118,16 +122,15 @@ namespace OpenIddict.Validation
throw new ArgumentNullException(nameof(context));
}
// If a principal was already attached, don't overwrite it.
if (context.Principal != null)
// If the reference token cannot be found, don't return an error to allow another handle to validate it.
var token = await _tokenManager.FindByReferenceIdAsync(context.Token);
if (token == null)
{
return;
}
// If the reference token cannot be found, return a generic error.
var token = await _tokenManager.FindByReferenceIdAsync(context.Request.AccessToken);
if (token == null || !string.Equals(await _tokenManager.GetTypeAsync(token),
TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase))
var type = await _tokenManager.GetTypeAsync(token);
if (!string.Equals(type, TokenUsages.AccessToken, StringComparison.OrdinalIgnoreCase))
{
context.Reject(
error: Errors.InvalidToken,
@ -145,61 +148,27 @@ namespace OpenIddict.Validation
.ToString());
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (!context.Options.JsonWebTokenHandler.CanReadToken(payload))
{
return;
}
// If no issuer signing key was attached, don't return an error to allow another handle to validate it.
var parameters = context.TokenValidationParameters;
if (parameters?.IssuerSigningKeys == null)
{
return;
}
// Clone the token validation parameters before mutating them to ensure the
// shared token validation parameters registered as options are not modified.
parameters = parameters.Clone();
parameters.PropertyBag = new Dictionary<string, object> { [Claims.Private.TokenUsage] = TokenUsages.AccessToken };
parameters.TokenDecryptionKeys = context.Options.EncryptionCredentials.Select(credentials => credentials.Key);
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(payload, parameters);
if (result.ClaimsIdentity == null)
{
context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", payload);
return;
}
// Attach the principal extracted from the authorization code to the parent event context
// and restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity)
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetInternalTokenId(await _tokenManager.GetIdAsync(token))
.SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token));
// Replace the token parameter by the payload resolved from the token entry.
context.Token = payload;
context.Logger.LogTrace("The reference JWT token '{Token}' was successfully validated and the following " +
"claims could be extracted: {Claims}.", payload, context.Principal.Claims);
// Store the identifier of the reference token in the transaction properties
// so it can be later used to restore the properties associated with the token.
context.Transaction.Properties[Properties.ReferenceTokenIdentifier] = await _tokenManager.GetIdAsync(token);
}
}
/// <summary>
/// Contains the logic responsible of rejecting authentication demands that specify an invalid self-contained token.
/// Contains the logic responsible of validating tokens generated using IdentityModel.
/// </summary>
public class ValidateSelfContainedToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
public class ValidateIdentityModelToken : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidateSelfContainedToken>()
.SetOrder(ValidateReferenceToken.Descriptor.Order + 1_000)
.UseSingletonHandler<ValidateIdentityModelToken>()
.SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -223,7 +192,7 @@ namespace OpenIddict.Validation
}
// If the token cannot be validated, don't return an error to allow another handle to validate it.
if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Request.AccessToken))
if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Token))
{
return;
}
@ -242,10 +211,10 @@ namespace OpenIddict.Validation
parameters.ValidIssuer = context.Issuer?.AbsoluteUri;
// If the token cannot be validated, don't return an error to allow another handle to validate it.
var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Request.AccessToken, parameters);
var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(context.Token, parameters);
if (result.ClaimsIdentity == null)
{
context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Request.AccessToken);
context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", context.Token);
return;
}
@ -254,7 +223,67 @@ namespace OpenIddict.Validation
context.Principal = new ClaimsPrincipal(result.ClaimsIdentity);
context.Logger.LogTrace("The self-contained JWT token '{Token}' was successfully validated and the following " +
"claims could be extracted: {Claims}.", context.Request.AccessToken, context.Principal.Claims);
"claims could be extracted: {Claims}.", context.Token, context.Principal.Claims);
}
}
/// <summary>
/// Contains the logic responsible of restoring the properties associated with a reference token entry.
/// Note: this handler is not used when the degraded mode is enabled.
/// </summary>
public class RestoreReferenceTokenProperties : IOpenIddictValidationHandler<ProcessAuthenticationContext>
{
private readonly IOpenIddictTokenManager _tokenManager;
public RestoreReferenceTokenProperties() => throw new InvalidOperationException(new StringBuilder()
.AppendLine("The core services must be registered when enabling reference tokens support.")
.Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ")
.AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.")
.ToString());
public RestoreReferenceTokenProperties([NotNull] IOpenIddictTokenManager tokenManager)
=> _tokenManager = tokenManager;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireReferenceAccessTokensEnabled>()
.UseScopedHandler<RestoreReferenceTokenProperties>()
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.Build();
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Principal == null)
{
return;
}
if (!context.Transaction.Properties.TryGetValue(Properties.ReferenceTokenIdentifier, out var identifier))
{
return;
}
var token = await _tokenManager.FindByIdAsync((string) identifier);
if (token == null)
{
throw new InvalidOperationException("The token entry cannot be found in the database.");
}
// Restore the creation/expiration dates/identifiers from the token entry metadata.
context.Principal = context.Principal
.SetCreationDate(await _tokenManager.GetCreationDateAsync(token))
.SetExpirationDate(await _tokenManager.GetExpirationDateAsync(token))
.SetInternalAuthorizationId(await _tokenManager.GetAuthorizationIdAsync(token))
.SetInternalTokenId(await _tokenManager.GetIdAsync(token))
.SetClaim(Claims.Private.TokenUsage, await _tokenManager.GetTypeAsync(token));
}
}
@ -269,7 +298,7 @@ namespace OpenIddict.Validation
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.UseSingletonHandler<ValidatePrincipal>()
.SetOrder(ValidateSelfContainedToken.Descriptor.Order + 1_000)
.SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000)
.Build();
/// <summary>
@ -449,7 +478,7 @@ namespace OpenIddict.Validation
}
var authorization = await _authorizationManager.FindByIdAsync(identifier);
if (authorization == null || !await _authorizationManager.IsValidAsync(authorization))
if (authorization == null || !await _authorizationManager.HasStatusAsync(authorization, Statuses.Valid))
{
context.Logger.LogError("The authorization '{Identifier}' was no longer valid.", identifier);

14
src/OpenIddict.Validation/OpenIddictValidationOptions.cs

@ -50,16 +50,14 @@ namespace OpenIddict.Validation
public bool EnableAuthorizationValidation { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether reference tokens should be used.
/// When set to <c>true</c>, authorization codes, access tokens and refresh tokens
/// are stored as ciphertext in the database and a crypto-secure random identifier
/// is returned to the client application. Enabling this option is useful
/// to keep track of all the issued tokens, when storing a very large number
/// of claims in the authorization codes, access tokens and refresh tokens
/// Gets or sets a boolean indicating whether reference access tokens should be used.
/// When set to <c>true</c>, access tokens and are stored as ciphertext in the database
/// and a crypto-secure random identifier is returned to the client application.
/// Enabling this option is useful to keep track of all the issued access tokens,
/// when storing a very large number of claims in the access tokens
/// or when immediate revocation of reference access tokens is desired.
/// Note: this option cannot be used when configuring JWT as the access token format.
/// </summary>
public bool UseReferenceTokens { get; set; }
public bool UseReferenceAccessTokens { get; set; }
/// <summary>
/// Gets or sets the absolute URL of the OAuth 2.0/OpenID Connect server.

Loading…
Cancel
Save