diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 4ebe0af9..ff892398 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/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 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 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 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 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 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 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. diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 9daf7098..afbf1ca4 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/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 diff --git a/samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs b/samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs index e708ec48..722d3ade 100644 --- a/samples/Mvc.Server/ViewModels/Authorization/AuthorizeViewModel.cs +++ b/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> Parameters { get; set; } - [Display(Name = "Scope")] public string Scope { get; set; } } diff --git a/samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs b/samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs deleted file mode 100644 index 5777b155..00000000 --- a/samples/Mvc.Server/ViewModels/Authorization/LogoutViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc.ModelBinding; - -namespace Mvc.Server.ViewModels.Authorization -{ - public class LogoutViewModel - { - [BindNever] - public IEnumerable> Parameters { get; set; } - } -} diff --git a/samples/Mvc.Server/ViewModels/Authorization/VerifyViewModel.cs b/samples/Mvc.Server/ViewModels/Authorization/VerifyViewModel.cs new file mode 100644 index 00000000..baedb0d0 --- /dev/null +++ b/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; } + } +} diff --git a/samples/Mvc.Server/Views/Authorization/Authorize.cshtml b/samples/Mvc.Server/Views/Authorization/Authorize.cshtml index 7d07c4c2..549b9a70 100644 --- a/samples/Mvc.Server/Views/Authorization/Authorize.cshtml +++ b/samples/Mvc.Server/Views/Authorization/Authorize.cshtml @@ -1,4 +1,5 @@ -@model AuthorizeViewModel +@using Microsoft.Extensions.Primitives +@model AuthorizeViewModel

Authorization

@@ -6,7 +7,9 @@

Do you want to grant @Model.ApplicationName access to your data? (scopes requested: @Model.Scope)

- @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>) Context.Request.Form : Context.Request.Query) { } diff --git a/samples/Mvc.Server/Views/Authorization/Logout.cshtml b/samples/Mvc.Server/Views/Authorization/Logout.cshtml index 3e994b29..0f892ec1 100644 --- a/samples/Mvc.Server/Views/Authorization/Logout.cshtml +++ b/samples/Mvc.Server/Views/Authorization/Logout.cshtml @@ -1,11 +1,13 @@ -@model LogoutViewModel +@using Microsoft.Extensions.Primitives

Log out

Are you sure you want to sign out?

- @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>) Context.Request.Form : Context.Request.Query) { } diff --git a/samples/Mvc.Server/Views/Authorization/Verify.cshtml b/samples/Mvc.Server/Views/Authorization/Verify.cshtml new file mode 100644 index 00000000..aaacd1d5 --- /dev/null +++ b/samples/Mvc.Server/Views/Authorization/Verify.cshtml @@ -0,0 +1,49 @@ +@using Microsoft.Extensions.Primitives +@model VerifyViewModel + +
+

Authorization

+ + @if (string.IsNullOrEmpty(Model.UserCode) || !string.IsNullOrEmpty(Model.Error)) + { + @if (!string.IsNullOrEmpty(Model.Error) && !string.IsNullOrEmpty(Model.ErrorDescription)) + { +

+ An error occurred: +
+ @Model.ErrorDescription (@Model.Error) +

+ } + +

Enter the user code given by the client application:

+ + +
+ +
+ + + + } + else + { +

Do you want to grant @Model.ApplicationName access to your data? (scopes requested: @Model.Scope)

+

+ Make sure that the code displayed on the device is @Model.UserCode. +
+ If the two codes don't match, press "No" to reject the authorization demand. +

+ +
+ @* Flow the request parameters so they can be received by the VerifyAccept/VerifyReject actions: *@ + @foreach (var parameter in Context.Request.HasFormContentType ? + (IEnumerable>) Context.Request.Form : Context.Request.Query) + { + + } + + + + + } +
diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs index eee68c57..9f33d09a 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictAuthorizationManager.cs @@ -270,36 +270,13 @@ namespace OpenIddict.Abstractions ValueTask HasScopesAsync([NotNull] object authorization, ImmutableArray scopes, CancellationToken cancellationToken = default); /// - /// Determines whether a given authorization is ad hoc. + /// Determines whether a given authorization has the specified status. /// /// The authorization. + /// The expected status. /// The that can be used to abort the operation. - /// true if the authorization is ad hoc, false otherwise. - ValueTask IsAdHocAsync([NotNull] object authorization, CancellationToken cancellationToken = default); - - /// - /// Determines whether a given authorization is permanent. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// true if the authorization is permanent, false otherwise. - ValueTask IsPermanentAsync([NotNull] object authorization, CancellationToken cancellationToken = default); - - /// - /// Determines whether a given authorization has been revoked. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// true if the authorization has been revoked, false otherwise. - ValueTask IsRevokedAsync([NotNull] object authorization, CancellationToken cancellationToken = default); - - /// - /// Determines whether a given authorization is valid. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// true if the authorization is valid, false otherwise. - ValueTask IsValidAsync([NotNull] object authorization, CancellationToken cancellationToken = default); + /// true if the authorization has the specified status, false otherwise. + ValueTask HasStatusAsync([NotNull] object authorization, [NotNull] string status, CancellationToken cancellationToken = default); /// /// Executes the specified query and returns all the corresponding elements. diff --git a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs index 5978f846..66439e25 100644 --- a/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs +++ b/src/OpenIddict.Abstractions/Managers/IOpenIddictTokenManager.cs @@ -296,28 +296,13 @@ namespace OpenIddict.Abstractions ValueTask GetTypeAsync([NotNull] object token, CancellationToken cancellationToken = default); /// - /// Determines whether a given token has already been redemeed. + /// Determines whether a given token has the specified status. /// /// The token. + /// The expected status. /// The that can be used to abort the operation. - /// true if the token has already been redemeed, false otherwise. - ValueTask IsRedeemedAsync([NotNull] object token, CancellationToken cancellationToken = default); - - /// - /// Determines whether a given token has been revoked. - /// - /// The token. - /// The that can be used to abort the operation. - /// true if the token has been revoked, false otherwise. - ValueTask IsRevokedAsync([NotNull] object token, CancellationToken cancellationToken = default); - - /// - /// Determines whether a given token is valid. - /// - /// The token. - /// The that can be used to abort the operation. - /// true if the token is valid, false otherwise. - ValueTask IsValidAsync([NotNull] object token, CancellationToken cancellationToken = default); + /// true if the token has the specified status, false otherwise. + ValueTask HasStatusAsync([NotNull] object token, [NotNull] string status, CancellationToken cancellationToken = default); /// /// Executes the specified query and returns all the corresponding elements. @@ -422,6 +407,14 @@ namespace OpenIddict.Abstractions /// true if the token was successfully redemeed, false otherwise. ValueTask TryRedeemAsync([NotNull] object token, CancellationToken cancellationToken = default); + /// + /// Tries to reject a token. + /// + /// The token to reject. + /// The that can be used to abort the operation. + /// true if the token was successfully redemeed, false otherwise. + ValueTask TryRejectAsync([NotNull] object token, CancellationToken cancellationToken = default); + /// /// Tries to revoke a token. /// diff --git a/src/OpenIddict.Abstractions/OpenIddictConstants.cs b/src/OpenIddict.Abstractions/OpenIddictConstants.cs index 9a07bf05..05f4c8e9 100644 --- a/src/OpenIddict.Abstractions/OpenIddictConstants.cs +++ b/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"; } } } diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs index 6d1bcddb..9c5f849e 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs @@ -25,41 +25,6 @@ namespace OpenIddict.Abstractions /// public static class OpenIddictExtensions { - /// - /// 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. - /// - /// The instance. - /// The parameters, as a flattened collection. - public static ImmutableList> GetFlattenedParameters([NotNull] this OpenIddictMessage message) - { - if (message == null) - { - throw new ArgumentNullException(nameof(message)); - } - - var parameters = ImmutableList.CreateBuilder>(); - - foreach (var parameter in message.GetParameters()) - { - var values = (string[]) parameter.Value; - if (values == null) - { - continue; - } - - foreach (var value in values) - { - parameters.Add(new KeyValuePair(parameter.Key, value)); - } - } - - return parameters.ToImmutable(); - } - /// /// Extracts the authentication context class values from an . /// @@ -506,6 +471,22 @@ namespace OpenIddict.Abstractions return string.Equals(request.GrantType, GrantTypes.ClientCredentials, StringComparison.Ordinal); } + /// + /// Determines whether the "grant_type" parameter corresponds to the device code grant. + /// See https://tools.ietf.org/html/rfc8628 for more information. + /// + /// The instance. + /// true if the request is a device code grant request, false otherwise. + 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); + } + /// /// 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; } + /// + /// Gets the device code lifetime associated with the claims principal. + /// + /// The claims principal. + /// The device code lifetime or null if the claim cannot be found. + + 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; + } + /// /// Gets the identity token lifetime associated with the claims principal. /// @@ -1255,6 +1263,33 @@ namespace OpenIddict.Abstractions return null; } + /// + /// Gets the user code lifetime associated with the claims principal. + /// + /// The claims principal. + /// The user code lifetime or null if the claim cannot be found. + + 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; + } + /// /// Gets the internal authorization identifier associated with the claims principal. /// @@ -1717,6 +1752,22 @@ namespace OpenIddict.Abstractions return principal.SetClaim(Claims.Private.AuthorizationCodeLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } + /// + /// Sets the device code lifetime associated with the claims principal. + /// + /// The claims principal. + /// The device code lifetime to store. + /// The claims principal. + 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)); + } + /// /// Sets the identity token lifetime associated with the claims principal. /// @@ -1749,6 +1800,22 @@ namespace OpenIddict.Abstractions return principal.SetClaim(Claims.Private.RefreshTokenLifetime, lifetime?.TotalSeconds.ToString(CultureInfo.InvariantCulture)); } + /// + /// Sets the user code lifetime associated with the claims principal. + /// + /// The claims principal. + /// The user code lifetime to store. + /// The claims principal. + 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)); + } + /// /// Sets the internal authorization identifier associated with the claims principal. /// diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs index d125005a..3badeb5a 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictMessage.cs +++ b/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(); } } diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs index 57c8c456..a953ce6a 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictRequest.cs @@ -192,6 +192,15 @@ namespace OpenIddict.Abstractions set => SetParameter(OpenIddictConstants.Parameters.CodeVerifier, value); } + /// + /// Gets or sets the "device_code" parameter. + /// + public string DeviceCode + { + get => (string) GetParameter(OpenIddictConstants.Parameters.DeviceCode); + set => SetParameter(OpenIddictConstants.Parameters.DeviceCode, value); + } + /// /// Gets or sets the "display" parameter. /// @@ -408,6 +417,15 @@ namespace OpenIddict.Abstractions set => SetParameter(OpenIddictConstants.Parameters.UiLocales, value); } + /// + /// Gets or sets the "user_code" parameter. + /// + public string UserCode + { + get => (string) GetParameter(OpenIddictConstants.Parameters.UserCode); + set => SetParameter(OpenIddictConstants.Parameters.UserCode, value); + } + /// /// Gets or sets the "username" parameter. /// diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs index 3c32a771..d31f5b37 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictResponse.cs @@ -84,6 +84,15 @@ namespace OpenIddict.Abstractions set => SetParameter(OpenIddictConstants.Parameters.Code, value); } + /// + /// Gets or sets the "device_code" parameter. + /// + public string DeviceCode + { + get => (string) GetParameter(OpenIddictConstants.Parameters.DeviceCode); + set => SetParameter(OpenIddictConstants.Parameters.DeviceCode, value); + } + /// /// Gets or sets the "error" parameter. /// @@ -164,5 +173,14 @@ namespace OpenIddict.Abstractions get => (string) GetParameter(OpenIddictConstants.Parameters.TokenType); set => SetParameter(OpenIddictConstants.Parameters.TokenType, value); } + + /// + /// Gets or sets the "user_code" parameter. + /// + public string UserCode + { + get => (string) GetParameter(OpenIddictConstants.Parameters.UserCode); + set => SetParameter(OpenIddictConstants.Parameters.UserCode, value); + } } } diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs index 1ce35c0b..eb6c2c16 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -702,94 +702,26 @@ namespace OpenIddict.Core } /// - /// Determines whether a given authorization is ad hoc. + /// Determines whether a given authorization has the specified status. /// /// The authorization. + /// The expected status. /// The that can be used to abort the operation. - /// true if the authorization is ad hoc, false otherwise. - public async ValueTask 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); - } - - /// - /// Determines whether a given authorization is permanent. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// true if the authorization is permanent, false otherwise. - public async ValueTask 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); - } - - /// - /// Determines whether a given authorization has been revoked. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// true if the authorization has been revoked, false otherwise. - public virtual async ValueTask 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); - } - - /// - /// Determines whether a given authorization is valid. - /// - /// The authorization. - /// The that can be used to abort the operation. - /// true if the authorization is valid, false otherwise. - public virtual async ValueTask IsValidAsync( - [NotNull] TAuthorization authorization, CancellationToken cancellationToken = default) + /// true if the authorization has the specified status, false otherwise. + public virtual async ValueTask 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); } /// @@ -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 IOpenIddictAuthorizationManager.HasScopesAsync(object authorization, ImmutableArray scopes, CancellationToken cancellationToken) => HasScopesAsync((TAuthorization) authorization, scopes, cancellationToken); - ValueTask IOpenIddictAuthorizationManager.IsAdHocAsync(object authorization, CancellationToken cancellationToken) - => IsAdHocAsync((TAuthorization) authorization, cancellationToken); - - ValueTask IOpenIddictAuthorizationManager.IsPermanentAsync(object authorization, CancellationToken cancellationToken) - => IsPermanentAsync((TAuthorization) authorization, cancellationToken); - - ValueTask IOpenIddictAuthorizationManager.IsRevokedAsync(object authorization, CancellationToken cancellationToken) - => IsRevokedAsync((TAuthorization) authorization, cancellationToken); - - ValueTask IOpenIddictAuthorizationManager.IsValidAsync(object authorization, CancellationToken cancellationToken) - => IsValidAsync((TAuthorization) authorization, cancellationToken); + ValueTask IOpenIddictAuthorizationManager.HasStatusAsync(object authorization, string status, CancellationToken cancellationToken) + => HasStatusAsync((TAuthorization) authorization, status, cancellationToken); IAsyncEnumerable IOpenIddictAuthorizationManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => ListAsync(count, offset, cancellationToken).OfType(); diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs index 74d5b331..60217fa5 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -736,69 +736,25 @@ namespace OpenIddict.Core } /// - /// Determines whether a given token has already been redemeed. + /// Determines whether a given token has the specified status. /// /// The token. + /// The expected status. /// The that can be used to abort the operation. - /// true if the token has already been redemeed, false otherwise. - public virtual async ValueTask IsRedeemedAsync([NotNull] TToken token, CancellationToken cancellationToken = default) + /// true if the token has the specified status, false otherwise. + public virtual async ValueTask 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); - } - - /// - /// Determines whether a given token has been revoked. - /// - /// The token. - /// The that can be used to abort the operation. - /// true if the token has been revoked, false otherwise. - public virtual async ValueTask 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); - } - - /// - /// Determines whether a given token is valid. - /// - /// The token. - /// The that can be used to abort the operation. - /// true if the token is valid, false otherwise. - public virtual async ValueTask 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); } /// @@ -1078,6 +1034,54 @@ namespace OpenIddict.Core } } + /// + /// Tries to reject a token. + /// + /// The token to reject. + /// The that can be used to abort the operation. + /// true if the token was successfully redemeed, false otherwise. + public virtual async ValueTask 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; + } + } + /// /// Tries to revoke a token. /// @@ -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 IOpenIddictTokenManager.GetTypeAsync(object token, CancellationToken cancellationToken) => GetTypeAsync((TToken) token, cancellationToken); - ValueTask IOpenIddictTokenManager.IsRedeemedAsync(object token, CancellationToken cancellationToken) - => IsRedeemedAsync((TToken) token, cancellationToken); - - ValueTask IOpenIddictTokenManager.IsRevokedAsync(object token, CancellationToken cancellationToken) - => IsRevokedAsync((TToken) token, cancellationToken); - - ValueTask IOpenIddictTokenManager.IsValidAsync(object token, CancellationToken cancellationToken) - => IsValidAsync((TToken) token, cancellationToken); + ValueTask IOpenIddictTokenManager.HasStatusAsync(object token, string status, CancellationToken cancellationToken) + => HasStatusAsync((TToken) token, status, cancellationToken); IAsyncEnumerable IOpenIddictTokenManager.ListAsync(int? count, int? offset, CancellationToken cancellationToken) => ListAsync(count, offset, cancellationToken).OfType(); @@ -1394,6 +1396,9 @@ namespace OpenIddict.Core ValueTask IOpenIddictTokenManager.TryRedeemAsync(object token, CancellationToken cancellationToken) => TryRedeemAsync((TToken) token, cancellationToken); + ValueTask IOpenIddictTokenManager.TryRejectAsync(object token, CancellationToken cancellationToken) + => TryRejectAsync((TToken) token, cancellationToken); + ValueTask IOpenIddictTokenManager.TryRevokeAsync(object token, CancellationToken cancellationToken) => TryRevokeAsync((TToken) token, cancellationToken); diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs index 06698f2d..39839a38 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictAuthorizationConfiguration.cs +++ b/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) diff --git a/src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs b/src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs index 330c815b..49253bcb 100644 --- a/src/OpenIddict.EntityFramework/Configurations/OpenIddictTokenConfiguration.cs +++ b/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) diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs index f0254283..e341c4d7 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictAuthorizationConfiguration.cs +++ b/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) diff --git a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs b/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs index 6cba70aa..29065994 100644 --- a/src/OpenIddict.EntityFrameworkCore/Configurations/OpenIddictTokenConfiguration.cs +++ b/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) diff --git a/src/OpenIddict.NHibernate/Mappings/OpenIddictAuthorizationMapping.cs b/src/OpenIddict.NHibernate/Mappings/OpenIddictAuthorizationMapping.cs index ddb5dcf6..dc4af94c 100644 --- a/src/OpenIddict.NHibernate/Mappings/OpenIddictAuthorizationMapping.cs +++ b/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); diff --git a/src/OpenIddict.NHibernate/Mappings/OpenIddictTokenMapping.cs b/src/OpenIddict.NHibernate/Mappings/OpenIddictTokenMapping.cs index 6bc9c798..15d158fa 100644 --- a/src/OpenIddict.NHibernate/Mappings/OpenIddictTokenMapping.cs +++ b/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); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs index 22b6cf07..84fb21e8 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreBuilder.cs @@ -109,6 +109,16 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerAspNetCoreBuilder EnableUserinfoEndpointPassthrough() => Configure(options => options.EnableUserinfoEndpointPassthrough = true); + /// + /// 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). + /// + /// The . + public OpenIddictServerAspNetCoreBuilder EnableVerificationEndpointPassthrough() + => Configure(options => options.EnableVerificationEndpointPassthrough = true); + /// /// Enables request caching, so that both authorization and logout requests /// are automatically stored in the distributed cache, which allows flowing diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs index 240347b3..03383cb8 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs @@ -53,6 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // 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. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs index b4e08bca..1f62beaa 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/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 HandleAuthenticateAsync() + protected override async Task HandleAuthenticateAsync() { var transaction = Context.Features.Get()?.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(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 + { + [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) diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs index 6eeaa7d2..51238a3b 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlerFilters.cs @@ -206,5 +206,27 @@ namespace OpenIddict.Server.AspNetCore return new ValueTask(_options.CurrentValue.EnableUserinfoEndpointPassthrough); } } + + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the verification endpoint. + /// + public class RequireVerificationEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireVerificationEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(_options.CurrentValue.EnableVerificationEndpointPassthrough); + } + } } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs index 5c47b52a..4ff9db39 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Authentication.cs +++ b/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($@""); + writer.WriteLine($@""); } writer.WriteLine(@""); @@ -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()); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Device.cs new file mode 100644 index 00000000..22c23d30 --- /dev/null +++ b/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 DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Device request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Device response processing: + */ + ProcessJsonResponse.Descriptor, + + /* + * Verification request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Verification request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Verification response processing: + */ + ProcessHostRedirectionResponse.Descriptor, + ProcessStatusCodePagesErrorResponse.Descriptor, + ProcessPassthroughErrorResponse.Descriptor, + ProcessLocalErrorResponse.Descriptor, + ProcessEmptyResponse.Descriptor); + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs index c24b0979..9d6bb16f 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Session.cs +++ b/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.Descriptor, ProcessStatusCodePagesErrorResponse.Descriptor, ProcessPassthroughErrorResponse.Descriptor, ProcessLocalErrorResponse.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); diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 2c02e9af..2cb3e9a1 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/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 } } + /// + /// 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. + /// + public class AttachHostChallengeError : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachDefaultChallengeError.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + 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; + } + } + /// /// 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 } } + /// + /// 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. + /// + public class ProcessHostRedirectionResponse : IOpenIddictServerHandler + where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + 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; + } + } + /// /// 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. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs index f1d2e21a..25fba02d 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreOptions.cs @@ -63,6 +63,14 @@ namespace OpenIddict.Server.AspNetCore /// public bool EnableUserinfoEndpointPassthrough { get; set; } + /// + /// 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). + /// + public bool EnableVerificationEndpointPassthrough { get; set; } + /// /// Gets or sets a boolean indicating whether request caching should be enabled. /// When enabled, both authorization and logout requests are automatically stored diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs index 13477949..87d2e5b9 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionConstants.cs +++ b/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 diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs index 4752da0f..bca01712 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs +++ b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionFormatter.cs @@ -254,7 +254,7 @@ namespace OpenIddict.Server.DataProtection ClaimsPrincipal principal, IReadOnlyDictionary 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()); diff --git a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs b/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs index c6e303fa..2caa348a 100644 --- a/src/OpenIddict.Server.DataProtection/OpenIddictServerDataProtectionHandlers.cs +++ b/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); - - /// - /// 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. - /// - public class ValidateReferenceDataProtectionToken : IOpenIddictServerHandler - { - private readonly IOpenIddictTokenManager _tokenManager; - private readonly IOptionsMonitor _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 options) - { - _tokenManager = tokenManager; - _options = options; - } - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .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 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); /// - /// 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. /// - public class ValidateSelfContainedDataProtectionToken : IOpenIddictServerHandler + public class ValidateDataProtectionToken : IOpenIddictServerHandler { private readonly IOptionsMonitor _options; - public ValidateSelfContainedDataProtectionToken([NotNull] IOptionsMonitor options) + public ValidateDataProtectionToken([NotNull] IOptionsMonitor options) => _options = options; /// @@ -250,9 +60,8 @@ namespace OpenIddict.Server.DataProtection /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(ValidateSelfContainedToken.Descriptor.Order + 500) + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) .Build(); /// @@ -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 } /// - /// 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. /// - public class AttachReferenceDataProtectionAccessToken : IOpenIddictServerHandler + public class GenerateDataProtectionAccessToken : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; private readonly IOptionsMonitor _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 options) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - _options = options; - } + public GenerateDataProtectionAccessToken([NotNull] IOptionsMonitor options) + => _options = options; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() .AddFilter() - .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(AttachReferenceAccessToken.Descriptor.Order - 500) + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelAccessToken.Descriptor.Order - 500) .Build(); /// @@ -418,7 +179,7 @@ namespace OpenIddict.Server.DataProtection /// /// A that can be used to monitor the asynchronous operation. /// - 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; } } /// - /// 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. /// - public class AttachReferenceDataProtectionAuthorizationCode : IOpenIddictServerHandler + public class GenerateDataProtectionAuthorizationCode : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; private readonly IOptionsMonitor _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 options) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - _options = options; - } + public GenerateDataProtectionAuthorizationCode([NotNull] IOptionsMonitor options) + => _options = options; /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() .AddFilter() - .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(AttachReferenceAuthorizationCode.Descriptor.Order - 500) + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelAuthorizationCode.Descriptor.Order - 500) .Build(); /// @@ -538,7 +243,7 @@ namespace OpenIddict.Server.DataProtection /// /// A that can be used to monitor the asynchronous operation. /// - 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); - } - } - - /// - /// 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. - /// - public class AttachReferenceDataProtectionRefreshToken : IOpenIddictServerHandler - { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; - private readonly IOptionsMonitor _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 options) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - _options = options; - } - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(AttachReferenceRefreshToken.Descriptor.Order - 500) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - 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; } } /// - /// 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. /// - public class AttachSelfContainedDataProtectionAccessToken : IOpenIddictServerHandler + public class GenerateDataProtectionDeviceCode : IOpenIddictServerHandler { private readonly IOptionsMonitor _options; - public AttachSelfContainedDataProtectionAccessToken([NotNull] IOptionsMonitor options) + public GenerateDataProtectionDeviceCode([NotNull] IOptionsMonitor options) => _options = options; /// @@ -744,11 +294,10 @@ namespace OpenIddict.Server.DataProtection /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() + .AddFilter() .AddFilter() - .UseSingletonHandler() - .SetOrder(AttachSelfContainedAccessToken.Descriptor.Order - 500) + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelDeviceCode.Descriptor.Order - 500) .Build(); /// @@ -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; } } /// - /// 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. /// - public class AttachSelfContainedDataProtectionAuthorizationCode : IOpenIddictServerHandler + public class GenerateDataProtectionRefreshToken : IOpenIddictServerHandler { private readonly IOptionsMonitor _options; - public AttachSelfContainedDataProtectionAuthorizationCode([NotNull] IOptionsMonitor options) + public GenerateDataProtectionRefreshToken([NotNull] IOptionsMonitor options) => _options = options; /// @@ -810,11 +358,10 @@ namespace OpenIddict.Server.DataProtection /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() + .AddFilter() .AddFilter() - .UseSingletonHandler() - .SetOrder(AttachSelfContainedAuthorizationCode.Descriptor.Order - 500) + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelRefreshToken.Descriptor.Order - 500) .Build(); /// @@ -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; } } /// - /// 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. /// - public class AttachSelfContainedDataProtectionRefreshToken : IOpenIddictServerHandler + public class GenerateDataProtectionUserCode : IOpenIddictServerHandler { private readonly IOptionsMonitor _options; - public AttachSelfContainedDataProtectionRefreshToken([NotNull] IOptionsMonitor options) + public GenerateDataProtectionUserCode([NotNull] IOptionsMonitor options) => _options = options; /// @@ -876,11 +422,10 @@ namespace OpenIddict.Server.DataProtection /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() + .AddFilter() .AddFilter() - .UseSingletonHandler() - .SetOrder(AttachSelfContainedRefreshToken.Descriptor.Order - 500) + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelUserCode.Descriptor.Order - 500) .Build(); /// @@ -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; } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs index 03cb03f0..d5950bb3 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinBuilder.cs @@ -109,6 +109,16 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerOwinBuilder EnableUserinfoEndpointPassthrough() => Configure(options => options.EnableUserinfoEndpointPassthrough = true); + /// + /// 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). + /// + /// The . + public OpenIddictServerOwinBuilder EnableVerificationEndpointPassthrough() + => Configure(options => options.EnableVerificationEndpointPassthrough = true); + /// /// Enables request caching, so that both authorization and logout requests /// are automatically stored in the distributed cache, which allows flowing diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs index dc2df233..ef7e66b8 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs @@ -54,6 +54,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // 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. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs index 7520d712..4f0219f8 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs +++ b/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 /// public class OpenIddictServerOwinHandler : AuthenticationHandler { - private readonly ILogger _logger; private readonly IOpenIddictServerProvider _provider; /// /// Creates a new instance of the class. /// - /// The logger used by this instance. /// The OpenIddict server OWIN provider used by this instance. - public OpenIddictServerOwinHandler( - [NotNull] ILogger logger, - [NotNull] IOpenIddictServerProvider provider) - { - _logger = logger; - _provider = provider; - } + public OpenIddictServerOwinHandler([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; public override async Task InvokeAsync() { @@ -104,7 +96,7 @@ namespace OpenIddict.Server.Owin return false; } - protected override Task AuthenticateCoreAsync() + protected override async Task AuthenticateCoreAsync() { var transaction = Context.Get(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(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 + { + [OpenIddictServerOwinConstants.Properties.Error] = context.Error, + [OpenIddictServerOwinConstants.Properties.ErrorDescription] = context.ErrorDescription, + [OpenIddictServerOwinConstants.Properties.ErrorUri] = context.ErrorUri + }); + + return new AuthenticationTicket(null, properties); } - return Task.FromResult(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() diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs index 79844258..7883d9b0 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlerFilters.cs @@ -184,5 +184,27 @@ namespace OpenIddict.Server.Owin return new ValueTask(_options.CurrentValue.EnableUserinfoEndpointPassthrough); } } + + /// + /// Represents a filter that excludes the associated handlers if the + /// pass-through mode was not enabled for the verification endpoint. + /// + public class RequireVerificationEndpointPassthroughEnabled : IOpenIddictServerHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireVerificationEndpointPassthroughEnabled([NotNull] IOptionsMonitor options) + => _options = options; + + public ValueTask IsActiveAsync([NotNull] BaseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(_options.CurrentValue.EnableVerificationEndpointPassthrough); + } + } } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs index 65659703..6bd2cc55 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Authentication.cs +++ b/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($@""); + writer.WriteLine($@""); } writer.WriteLine(@""); @@ -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()); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Device.cs new file mode 100644 index 00000000..251ab7ca --- /dev/null +++ b/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 DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Device request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Device response processing: + */ + ProcessJsonResponse.Descriptor, + + /* + * Verification request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Verification request handling: + */ + EnablePassthroughMode.Descriptor, + + /* + * Verification response processing: + */ + ProcessHostRedirectionResponse.Descriptor, + ProcessPassthroughErrorResponse.Descriptor, + ProcessLocalErrorResponse.Descriptor, + ProcessEmptyResponse.Descriptor); + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs index bdbebe39..2d3e2ef1 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Session.cs +++ b/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.Descriptor, ProcessPassthroughErrorResponse.Descriptor, ProcessLocalErrorResponse.Descriptor, ProcessEmptyResponse.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); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 8a3f50d6..511d48ff 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/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 } } + /// + /// 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. + /// + public class AttachHostChallengeError : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachDefaultChallengeError.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + 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; + } + } + /// /// 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 } } + /// + /// 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. + /// + public class ProcessHostRedirectionResponse : IOpenIddictServerHandler + where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ProcessJsonResponse.Descriptor.Order - 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + 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; + } + } + /// /// 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. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs index 1f7531fd..7c7c4871 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs +++ b/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 /// public class OpenIddictServerOwinMiddleware : AuthenticationMiddleware { - private readonly ILogger _logger; private readonly IOpenIddictServerProvider _provider; /// /// Creates a new instance of the class. /// /// The next middleware in the pipeline, if applicable. - /// The logger used by this middleware. /// The OpenIddict server OWIN options. /// The OpenIddict server provider. public OpenIddictServerOwinMiddleware( [CanBeNull] OwinMiddleware next, - [NotNull] ILogger logger, [NotNull] IOptionsMonitor options, [NotNull] IOpenIddictServerProvider provider) : base(next, options.CurrentValue) - { - _logger = logger; - _provider = provider; - } + => _provider = provider; /// /// Creates and returns a new instance. /// /// A new instance of the class. protected override AuthenticationHandler CreateHandler() - => new OpenIddictServerOwinHandler(_logger, _provider); + => new OpenIddictServerOwinHandler(_provider); } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs index 4c7245e7..2c8aa8fa 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddlewareFactory.cs +++ b/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>(provider), options: GetRequiredService>(provider), provider: GetRequiredService(provider)); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs index 77a040ba..a41c7b98 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinOptions.cs @@ -70,6 +70,14 @@ namespace OpenIddict.Server.Owin /// public bool EnableUserinfoEndpointPassthrough { get; set; } + /// + /// 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). + /// + public bool EnableVerificationEndpointPassthrough { get; set; } + /// /// Gets or sets a boolean indicating whether request caching should be enabled. /// When enabled, both authorization and logout requests are automatically stored diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index f3b020da..7d14462e 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -1067,6 +1067,14 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.GrantTypes.Add(type)); } + /// + /// Enables device code flow support. For more information about this + /// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc8628. + /// + /// The . + public OpenIddictServerBuilder AllowDeviceCodeFlow() + => Configure(options => options.GrantTypes.Add(OpenIddictConstants.GrantTypes.DeviceCode)); + /// /// 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 }); } + /// + /// 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. + /// + /// The addresses associated to the endpoint. + /// The . + 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()); + } + + /// + /// 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. + /// + /// The addresses associated to the endpoint. + /// The . + 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); + } + }); + } + /// /// 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 }); } + /// + /// 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. + /// + /// The addresses associated to the endpoint. + /// The . + 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()); + } + + /// + /// 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. + /// + /// The addresses associated to the endpoint. + /// The . + 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); + } + }); + } + /// /// 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); + /// + /// 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, null can be specified to issue codes that never expire. + /// + /// The authorization code lifetime. + /// The . + public OpenIddictServerBuilder SetDeviceCodeLifetime([CanBeNull] TimeSpan? lifetime) + => Configure(options => options.DeviceCodeLifetime = lifetime); + /// /// 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); + /// + /// Sets the user code lifetime, after which they'll no longer be considered valid. + /// Using short-lived device codes is strongly recommended. + /// While discouraged, null can be specified to issue codes that never expire. + /// + /// The authorization code lifetime. + /// The . + public OpenIddictServerBuilder SetUserCodeLifetime([CanBeNull] TimeSpan? lifetime) + => Configure(options => options.UserCodeLifetime = lifetime); + /// /// 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 } /// - /// 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. /// /// The . - public OpenIddictServerBuilder UseReferenceTokens() - => Configure(options => options.UseReferenceTokens = true); + public OpenIddictServerBuilder UseReferenceAccessTokens() + => Configure(options => options.UseReferenceAccessTokens = true); /// /// Configures OpenIddict to use rolling refresh tokens. When this option is enabled, diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index ca3c465e..764f28b8 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/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' 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' 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' 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' 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. diff --git a/src/OpenIddict.Server/OpenIddictServerConstants.cs b/src/OpenIddict.Server/OpenIddictServerConstants.cs index 5cb60d17..2a53578c 100644 --- a/src/OpenIddict.Server/OpenIddictServerConstants.cs +++ b/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"; } } } diff --git a/src/OpenIddict.Server/OpenIddictServerEndpointType.cs b/src/OpenIddict.Server/OpenIddictServerEndpointType.cs index f89db7ed..70094f34 100644 --- a/src/OpenIddict.Server/OpenIddictServerEndpointType.cs +++ b/src/OpenIddict.Server/OpenIddictServerEndpointType.cs @@ -54,6 +54,16 @@ namespace OpenIddict.Server /// /// Revocation endpoint. /// - Revocation = 8 + Revocation = 8, + + /// + /// Device endpoint. + /// + Device = 9, + + /// + /// Verification endpoint. + /// + Verification = 10 } } diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs index 37e7904c..fda22c03 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Authentication.cs @@ -76,13 +76,6 @@ namespace OpenIddict.Server RedirectUri = address; } - - /// - /// 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. - /// - public ClaimsPrincipal IdentityTokenHintPrincipal { get; set; } } /// @@ -98,13 +91,6 @@ namespace OpenIddict.Server : base(transaction) { } - - /// - /// 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. - /// - public ClaimsPrincipal IdentityTokenHintPrincipal { get; set; } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs new file mode 100644 index 00000000..41eea842 --- /dev/null +++ b/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 + { + /// + /// 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. + /// + public class ExtractDeviceRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractDeviceRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the device endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateDeviceRequestContext : BaseValidatingClientContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateDeviceRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each validated device request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleDeviceRequestContext : BaseValidatingTicketContext + { + /// + /// Creates a new instance of the class. + /// + public HandleDeviceRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called before the device response is returned to the caller. + /// + public class ApplyDeviceResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyDeviceResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + + /// + /// 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. + /// + public class ExtractVerificationRequestContext : BaseValidatingContext + { + /// + /// Creates a new instance of the class. + /// + public ExtractVerificationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called for each request to the verification endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public class ValidateVerificationRequestContext : BaseValidatingClientContext + { + /// + /// Creates a new instance of the class. + /// + public ValidateVerificationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets or sets the security principal extracted from the user code. + /// + public ClaimsPrincipal Principal { get; set; } + } + + /// + /// Represents an event called for each validated verification request + /// to allow the user code to decide how the request should be handled. + /// + public class HandleVerificationRequestContext : BaseValidatingTicketContext + { + /// + /// Creates a new instance of the class. + /// + public HandleVerificationRequestContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + } + + /// + /// Represents an event called before the verification response is returned to the caller. + /// + public class ApplyVerificationResponseContext : BaseRequestContext + { + /// + /// Creates a new instance of the class. + /// + public ApplyVerificationResponseContext([NotNull] OpenIddictServerTransaction transaction) + : base(transaction) + { + } + + /// + /// Gets the error code returned to the client application. + /// When the response indicates a successful response, + /// this property returns null. + /// + public string Error => Response.Error; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs index f3474f09..e13b6c5e 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Discovery.cs @@ -74,6 +74,11 @@ namespace OpenIddict.Server /// public Uri CryptographyEndpoint { get; set; } + /// + /// Gets or sets the device endpoint address. + /// + public Uri DeviceEndpoint { get; set; } + /// /// Gets or sets the introspection endpoint address. /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs index bcb96db8..4bebd336 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Session.cs @@ -69,13 +69,6 @@ namespace OpenIddict.Server PostLogoutRedirectUri = address; } - - /// - /// 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. - /// - public ClaimsPrincipal IdentityTokenHintPrincipal { get; set; } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index bdaab68e..56aa463c 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -61,12 +61,6 @@ namespace OpenIddict.Server /// public OpenIddictServerOptions Options => Transaction.Options; - /// - /// Gets the dictionary containing the properties associated with this event. - /// - public IDictionary Properties { get; } - = new Dictionary(StringComparer.OrdinalIgnoreCase); - /// /// Gets or sets the OpenIddict request or null if it couldn't be extracted. /// @@ -311,6 +305,16 @@ namespace OpenIddict.Server /// Gets or sets the security principal. /// public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets the token to validate. + /// + public string Token { get; set; } + + /// + /// Gets or sets the expected type of the token. + /// + public string TokenType { get; set; } } /// @@ -356,6 +360,14 @@ namespace OpenIddict.Server /// public bool IncludeAuthorizationCode { get; set; } + /// + /// 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. + /// + public bool IncludeDeviceCode { get; set; } + /// /// Gets or sets a boolean indicating whether an identity token /// should be returned to the client application. @@ -372,18 +384,32 @@ namespace OpenIddict.Server /// public bool IncludeRefreshToken { get; set; } + /// + /// 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. + /// + public bool IncludeUserCode { get; set; } + /// /// Gets or sets the principal containing the claims that /// will be used to create the access token, if applicable. /// public ClaimsPrincipal AccessTokenPrincipal { get; set; } - + /// /// Gets or sets the principal containing the claims that /// will be used to create the authorization code, if applicable. /// public ClaimsPrincipal AuthorizationCodePrincipal { get; set; } + /// + /// Gets or sets the principal containing the claims that + /// will be used to create the device code, if applicable. + /// + public ClaimsPrincipal DeviceCodePrincipal { get; set; } + /// /// 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. /// public ClaimsPrincipal RefreshTokenPrincipal { get; set; } + + /// + /// Gets or sets the principal containing the claims that + /// will be used to create the user code, if applicable. + /// + public ClaimsPrincipal UserCodePrincipal { get; set; } } /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index f1d3663e..202a6f53 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -48,12 +48,12 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); @@ -61,6 +61,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< diff --git a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs index 957d44fc..cde9e180 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlerFilters.cs @@ -95,6 +95,22 @@ namespace OpenIddict.Server } } + /// + /// Represents a filter that excludes the associated handlers if no device code is returned. + /// + public class RequireDeviceCodeIncluded : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.IncludeDeviceCode); + } + } + /// /// Represents a filter that excludes the associated handlers if endpoint permissions were disabled. /// @@ -160,9 +176,9 @@ namespace OpenIddict.Server } /// - /// 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. /// - public class RequireReferenceTokensDisabled : IOpenIddictServerHandlerFilter + public class RequireReferenceAccessTokensEnabled : IOpenIddictServerHandlerFilter { public ValueTask IsActiveAsync([NotNull] BaseContext context) { @@ -171,23 +187,7 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - return new ValueTask(!context.Options.UseReferenceTokens); - } - } - - /// - /// Represents a filter that excludes the associated handlers if reference tokens are disabled. - /// - public class RequireReferenceTokensEnabled : IOpenIddictServerHandlerFilter - { - public ValueTask IsActiveAsync([NotNull] BaseContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(context.Options.UseReferenceTokens); + return new ValueTask(context.Options.UseReferenceAccessTokens); } } @@ -302,5 +302,21 @@ namespace OpenIddict.Server return new ValueTask(!context.Options.DisableTokenStorage); } } + + /// + /// Represents a filter that excludes the associated handlers if no user code is returned. + /// + public class RequireUserCodeIncluded : IOpenIddictServerHandlerFilter + { + public ValueTask IsActiveAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new ValueTask(context.IncludeUserCode); + } + } } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index d42ed2cd..df12d1de 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/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 } } - /// - /// Contains the logic responsible of rejecting authorization requests that don't specify a valid id_token_hint. - /// - public class ValidateIdTokenHint : IOpenIddictServerHandler - { - private readonly IOpenIddictServerProvider _provider; - - public ValidateIdTokenHint([NotNull] IOpenIddictServerProvider provider) - => _provider = provider; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidateCodeChallengeParameters.Descriptor.Order + 1_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - 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; - } - } - /// /// 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() .AddFilter() .UseScopedHandler() - .SetOrder(ValidateIdTokenHint.Descriptor.Order + 1_000) + .SetOrder(ValidateCodeChallengeParameters.Descriptor.Order + 1_000) .Build(); /// @@ -1592,43 +1518,6 @@ namespace OpenIddict.Server } } - /// - /// Contains the logic responsible of attaching the principal extracted from the id_token_hint to the event context. - /// - public class AttachIdentityTokenHintPrincipal : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - 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; - } - } - /// /// 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( + 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; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs new file mode 100644 index 00000000..20fedf5a --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -0,0 +1,1205 @@ +/* + * 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 System.Collections.Immutable; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +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 +{ + public static partial class OpenIddictServerHandlers + { + public static class Device + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Device request top-level processing: + */ + ExtractDeviceRequest.Descriptor, + ValidateDeviceRequest.Descriptor, + HandleDeviceRequest.Descriptor, + ApplyDeviceResponse.Descriptor, + ApplyDeviceResponse.Descriptor, + ApplyDeviceResponse.Descriptor, + ApplyDeviceResponse.Descriptor, + + /* + * Device request validation: + */ + ValidateClientIdParameter.Descriptor, + ValidateScopes.Descriptor, + ValidateClientId.Descriptor, + ValidateClientType.Descriptor, + ValidateClientSecret.Descriptor, + ValidateEndpointPermissions.Descriptor, + ValidateScopePermissions.Descriptor, + + /* + * Verification request top-level processing: + */ + ExtractVerificationRequest.Descriptor, + ValidateVerificationRequest.Descriptor, + HandleVerificationRequest.Descriptor, + ApplyVerificationResponse.Descriptor, + ApplyVerificationResponse.Descriptor, + ApplyVerificationResponse.Descriptor, + ApplyVerificationResponse.Descriptor, + + /* + * Verification request handling: + */ + AttachUserCodePrincipal.Descriptor); + + /// + /// Contains the logic responsible of extracting device requests and invoking the corresponding event handlers. + /// + public class ExtractDeviceRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractDeviceRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Device) + { + return; + } + + var notification = new ExtractDeviceRequestContext(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; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The device request was not correctly extracted. To extract device requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The device request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating device requests and invoking the corresponding event handlers. + /// + public class ValidateDeviceRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateDeviceRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractDeviceRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Device) + { + return; + } + + var notification = new ValidateDeviceRequestContext(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; + } + + context.Logger.LogInformation("The device request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling device requests and invoking the corresponding event handlers. + /// + public class HandleDeviceRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleDeviceRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateDeviceRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Device) + { + return; + } + + var notification = new HandleDeviceRequestContext(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; + } + + var @event = new ProcessSigninContext(context.Transaction) + { + Principal = notification.Principal, + Response = new OpenIddictResponse() + }; + + if (@event.Principal == null) + { + // Note: no authentication type is deliberately specified to represent an unauthenticated identity. + var principal = new ClaimsPrincipal(new ClaimsIdentity()); + principal.SetScopes(context.Request.GetScopes()); + + @event.Principal = principal; + } + + await _provider.DispatchAsync(@event); + + if (@event.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (@event.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The device request was not handled. To handle device requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .Append("Alternatively, enable the pass-through mode to handle them at a later stage.") + .ToString()); + } + } + + /// + /// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers. + /// + public class ApplyDeviceResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyDeviceResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Device) + { + return; + } + + var notification = new ApplyDeviceResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The device response was not correctly applied. To apply device responses, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + } + + /// + /// Contains the logic responsible of rejecting device requests that don't specify a client identifier. + /// + public class ValidateClientIdParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ValidateDeviceRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // client_id is a required parameter and MUST cause an error when missing. + // See https://tools.ietf.org/html/rfc8628#section-3.1 for more information. + if (string.IsNullOrEmpty(context.ClientId)) + { + context.Logger.LogError("The device request was rejected because the mandatory 'client_id' was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'client_id' parameter is missing."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting authorization requests that use unregistered scopes. + /// Note: this handler is not used when the degraded mode is enabled or when scope validation is disabled. + /// + public class ValidateScopes : IOpenIddictServerHandler + { + private readonly IOpenIddictScopeManager _scopeManager; + + public ValidateScopes() => 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 ValidateScopes([NotNull] IOpenIddictScopeManager scopeManager) + => _scopeManager = scopeManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateDeviceRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If all the specified scopes are registered in the options, avoid making a database lookup. + var scopes = context.Request.GetScopes().Except(context.Options.Scopes); + if (scopes.Count != 0) + { + await foreach (var scope in _scopeManager.FindByNamesAsync(scopes.ToImmutableArray())) + { + scopes = scopes.Remove(await _scopeManager.GetNameAsync(scope)); + } + } + + // If at least one scope was not recognized, return an error. + if (scopes.Count != 0) + { + context.Logger.LogError("The device request was rejected because " + + "invalid scopes were specified: {Scopes}.", scopes); + + context.Reject( + error: Errors.InvalidScope, + description: "The specified 'scope' parameter is not valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting device requests that use an invalid client_id. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientId : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientId() => 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 ValidateClientId([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateScopes.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateDeviceRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Retrieve the application details corresponding to the requested client_id. + // If no entity can be found, this likely indicates that the client_id is invalid. + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + context.Logger.LogError("The device request was rejected because the client " + + "application was not found: '{ClientId}'.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified 'client_id' parameter is invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting device requests made by applications + /// whose client type is not compatible with the requested grant type. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientType : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientType() => 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 ValidateClientType([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateDeviceRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + if (await _applicationManager.IsPublicAsync(application)) + { + // Reject device requests containing a client_secret when the client is a public application. + if (!string.IsNullOrEmpty(context.ClientSecret)) + { + context.Logger.LogError("The device request was rejected because the public application '{ClientId}' " + + "was not allowed to send a client secret.", context.ClientId); + + context.Reject( + error: Errors.InvalidRequest, + description: "The 'client_secret' parameter is not valid for this client application."); + + return; + } + + return; + } + + // Confidential and hybrid applications MUST authenticate to protect them from impersonation attacks. + if (string.IsNullOrEmpty(context.ClientSecret)) + { + context.Logger.LogError("The device request was rejected because the confidential or hybrid application " + + "'{ClientId}' didn't specify a client secret.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The 'client_secret' parameter required for this client application is missing."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting device requests specifying an invalid client secret. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientSecret : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientSecret() => 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 ValidateClientSecret([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientType.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateDeviceRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // If the application is not a public client, validate the client secret. + if (!await _applicationManager.IsPublicAsync(application) && + !await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + { + context.Logger.LogError("The device request was rejected because the confidential or hybrid application " + + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified client credentials are invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting device requests made by + /// applications that haven't been granted the device endpoint permission. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateEndpointPermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateEndpointPermissions() => 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 ValidateEndpointPermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateDeviceRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Reject the request if the application is not allowed to use the device endpoint. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Device)) + { + context.Logger.LogError("The device request was rejected because the application '{ClientId}' " + + "was not allowed to use the device endpoint.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "This client application is not allowed to use the device endpoint."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting device requests made by applications + /// that haven't been granted the appropriate grant type permission. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateScopePermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateScopePermissions() => 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 ValidateScopePermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateDeviceRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + foreach (var scope in context.Request.GetScopes()) + { + // Avoid validating the "openid" and "offline_access" scopes as they represent protocol scopes. + if (string.Equals(scope, Scopes.OfflineAccess, StringComparison.Ordinal) || + string.Equals(scope, Scopes.OpenId, StringComparison.Ordinal)) + { + continue; + } + + // Reject the request if the application is not allowed to use the iterated scope. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Prefixes.Scope + scope)) + { + context.Logger.LogError("The device request was rejected because the application '{ClientId}' " + + "was not allowed to use the scope {Scope}.", context.ClientId, scope); + + context.Reject( + error: Errors.InvalidRequest, + description: "This client application is not allowed to use the specified scope."); + + return; + } + } + } + } + + /// + /// Contains the logic responsible of extracting verification requests and invoking the corresponding event handlers. + /// + public class ExtractVerificationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractVerificationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Verification) + { + return; + } + + var notification = new ExtractVerificationRequestContext(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; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The verification request was not correctly extracted. To extract verification requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The verification request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating verification requests and invoking the corresponding event handlers. + /// + public class ValidateVerificationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateVerificationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractVerificationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Verification) + { + return; + } + + var notification = new ValidateVerificationRequestContext(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; + } + + context.Logger.LogInformation("The verification request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling verification requests and invoking the corresponding event handlers. + /// + public class HandleVerificationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleVerificationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateVerificationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Verification) + { + return; + } + + var notification = new HandleVerificationRequestContext(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; + } + + if (notification.Principal != null) + { + var @event = new ProcessSigninContext(context.Transaction) + { + Principal = notification.Principal, + Response = new OpenIddictResponse() + }; + + await _provider.DispatchAsync(@event); + + if (@event.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (@event.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The verification request was not handled. To handle verification requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .Append("Alternatively, enable the pass-through mode to handle them at a later stage.") + .ToString()); + } + } + + /// + /// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers. + /// + public class ApplyVerificationResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyVerificationResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Verification) + { + return; + } + + var notification = new ApplyVerificationResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + throw new InvalidOperationException(new StringBuilder() + .Append("The verification response was not correctly applied. To apply verification responses, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + } + + /// + /// Contains the logic responsible of attaching the claims principal resolved from the user code. + /// + public class AttachUserCodePrincipal : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public AttachUserCodePrincipal([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] HandleVerificationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the user_code may not be present (e.g when the user typed + // the verification_uri manually without the user code appended). + // In this case, ignore the missing token so that a view can be + // rendered by the application to ask the user to enter the code. + if (string.IsNullOrEmpty(context.Request.UserCode)) + { + return; + } + + 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(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + // Note: authentication errors are deliberately not flowed up to the parent context. + return; + } + + // Attach the security principal extracted from the token to the validation context. + context.Principal = notification.Principal; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs index 232c5313..1f578fbe 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs +++ b/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) diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 09def67c..a07702b8 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/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 } } + /// + /// Contains the logic responsible of rejecting token requests that + /// don't specify a device code for the device code grant type. + /// + public class ValidateDeviceCodeParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + 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; + } + } + /// /// 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() .UseSingletonHandler() - .SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000) + .SetOrder(ValidateDeviceCodeParameter.Descriptor.Order + 1_000) .Build(); /// @@ -1190,8 +1234,8 @@ namespace OpenIddict.Server } /// - /// 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. /// public class ValidateToken : IOpenIddictServerHandler { @@ -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); } } /// - /// 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. /// public class ValidatePresenters : IOpenIddictServerHandler { @@ -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( + typeof(ValidateTokenRequestContext).FullName) ?? + throw new InvalidOperationException("The authentication context cannot be found."); + + context.Principal ??= notification.Principal; return default; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index 7995aad8..2d2aa55d 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/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( + typeof(ValidateIntrospectionRequestContext).FullName) ?? + throw new InvalidOperationException("The authentication context cannot be found."); + + context.Principal ??= notification.Principal; return default; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index 94b674b9..f07a8a53 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/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( + 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, diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs index 5861852b..7db501ef 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs +++ b/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 } } - /// - /// Contains the logic responsible of rejecting logout requests that don't specify a valid id_token_hint. - /// - public class ValidateIdTokenHint : IOpenIddictServerHandler - { - private readonly IOpenIddictServerProvider _provider; - - public ValidateIdTokenHint([NotNull] IOpenIddictServerProvider provider) - => _provider = provider; - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() - .SetOrder(ValidatePostLogoutRedirectUriParameter.Descriptor.Order + 1_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - 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; - } - } - /// /// 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 } } - /// - /// Contains the logic responsible of attaching the principal extracted from the id_token_hint to the event context. - /// - public class AttachIdentityTokenHintPrincipal : IOpenIddictServerHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) - .Build(); - - /// - /// Processes the event. - /// - /// The context associated with the event to process. - /// - /// A that can be used to monitor the asynchronous operation. - /// - 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; - } - } - /// /// 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( + 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; diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs index 320c6cad..23745267 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs +++ b/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( + typeof(ValidateUserinfoRequestContext).FullName) ?? + throw new InvalidOperationException("The authentication context cannot be found."); + + context.Principal ??= notification.Principal; return default; } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index b672fa07..0f456114 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Security.Claims; using System.Security.Cryptography; @@ -33,8 +34,11 @@ namespace OpenIddict.Server * Authentication processing: */ ValidateAuthenticationDemand.Descriptor, - ValidateReferenceToken.Descriptor, - ValidateSelfContainedToken.Descriptor, + ValidateTokenParameter.Descriptor, + NormalizeUserCode.Descriptor, + ValidateReferenceTokenIdentifier.Descriptor, + ValidateIdentityModelToken.Descriptor, + RestoreReferenceTokenProperties.Descriptor, ValidatePrincipal.Descriptor, ValidateTokenEntry.Descriptor, ValidateAuthorizationEntry.Descriptor, @@ -43,12 +47,15 @@ namespace OpenIddict.Server /* * Challenge processing: */ + ValidateChallengeDemand.Descriptor, AttachDefaultChallengeError.Descriptor, + RejectDeviceCodeEntry.Descriptor, + RejectUserCodeEntry.Descriptor, /* * Sign-in processing: */ - ValidateSigninResponse.Descriptor, + ValidateSigninDemand.Descriptor, RestoreInternalClaims.Descriptor, AttachDefaultScopes.Descriptor, AttachDefaultPresenters.Descriptor, @@ -58,30 +65,43 @@ namespace OpenIddict.Server PrepareAccessTokenPrincipal.Descriptor, PrepareAuthorizationCodePrincipal.Descriptor, + PrepareDeviceCodePrincipal.Descriptor, PrepareRefreshTokenPrincipal.Descriptor, PrepareIdentityTokenPrincipal.Descriptor, + PrepareUserCodePrincipal.Descriptor, RedeemTokenEntry.Descriptor, - RevokeRollingTokenEntries.Descriptor, + RedeemDeviceCodeEntry.Descriptor, + RedeemUserCodeEntry.Descriptor, + RevokeExistingTokenEntries.Descriptor, ExtendRefreshTokenEntry.Descriptor, - AttachReferenceAccessToken.Descriptor, - AttachReferenceAuthorizationCode.Descriptor, - AttachReferenceRefreshToken.Descriptor, + GenerateIdentityModelAccessToken.Descriptor, + CreateReferenceAccessTokenEntry.Descriptor, - CreateSelfContainedAuthorizationCodeEntry.Descriptor, - CreateSelfContainedRefreshTokenEntry.Descriptor, + GenerateIdentityModelAuthorizationCode.Descriptor, + CreateReferenceAuthorizationCodeEntry.Descriptor, - AttachSelfContainedAccessToken.Descriptor, - AttachSelfContainedAuthorizationCode.Descriptor, - AttachSelfContainedRefreshToken.Descriptor, + GenerateIdentityModelDeviceCode.Descriptor, + CreateReferenceDeviceCodeEntry.Descriptor, + UpdateReferenceDeviceCodeEntry.Descriptor, + + GenerateIdentityModelRefreshToken.Descriptor, + CreateReferenceRefreshTokenEntry.Descriptor, + + AttachDeviceCodeIdentifier.Descriptor, + GenerateIdentityModelUserCode.Descriptor, + CreateReferenceUserCodeEntry.Descriptor, AttachTokenDigests.Descriptor, - AttachSelfContainedIdentityToken.Descriptor, - - AttachAdditionalProperties.Descriptor) + GenerateIdentityModelIdentityToken.Descriptor, + + BeautifyUserCode.Descriptor, + AttachAccessTokenProperties.Descriptor, + AttachDeviceCodeProperties.Descriptor) .AddRange(Authentication.DefaultHandlers) + .AddRange(Device.DefaultHandlers) .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Introspection.DefaultHandlers) @@ -124,8 +144,10 @@ namespace OpenIddict.Server case OpenIddictServerEndpointType.Logout: case OpenIddictServerEndpointType.Revocation: case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType(): + case OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType(): case OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType(): case OpenIddictServerEndpointType.Userinfo: + case OpenIddictServerEndpointType.Verification: return default; default: throw new InvalidOperationException("No identity cannot be extracted from this request."); @@ -134,14 +156,149 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of rejecting authentication demands that use an invalid reference token. + /// Contains the logic responsible of resolving the token from the incoming request. + /// + public class ValidateTokenParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var (token, type) = context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => (context.Request.IdTokenHint, TokenUsages.IdToken), + OpenIddictServerEndpointType.Logout => (context.Request.IdTokenHint, TokenUsages.IdToken), + + OpenIddictServerEndpointType.Introspection => (context.Request.Token, null), + OpenIddictServerEndpointType.Revocation => (context.Request.Token, null), + + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => (context.Request.Code, TokenUsages.AuthorizationCode), + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => (context.Request.DeviceCode, TokenUsages.DeviceCode), + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => (context.Request.RefreshToken, TokenUsages.RefreshToken), + + OpenIddictServerEndpointType.Userinfo => (context.Request.AccessToken, TokenUsages.AccessToken), + + OpenIddictServerEndpointType.Verification => (context.Request.UserCode, TokenUsages.UserCode), + + _ => (null, null) + }; + + if (string.IsNullOrEmpty(token)) + { + context.Reject( + error: Errors.InvalidRequest, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The authorization code is missing.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The specified device code is missing.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token is missing.", + + _ => "The security token is missing." + }); + + return default; + } + + context.Token = token; + context.TokenType = type; + + return default; + } + } + + /// + /// Contains the logic responsible of normalizing user codes. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class NormalizeUserCode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + // Technically, this handler doesn't require that the degraded mode be disabled + // but the default CreateReferenceUserCodeEntry that creates the user code + // reference identifiers only works when the degraded mode is disabled. + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (!string.Equals(context.TokenType, TokenUsages.UserCode, StringComparison.OrdinalIgnoreCase)) + { + return default; + } + + // Note: unlike other tokens, user codes may be potentially entered manually by users in a web form. + // To make that easier, user codes are generally "beautified" by adding intermediate dashes to + // make them easier to read and type. Since these additional characters are not part of the original + // user codes, non-digit characters are automatically filtered from the reference identifier. + + var builder = new StringBuilder(context.Token); + for (var index = builder.Length - 1; index >= 0; index--) + { + var character = builder[index]; + if (character < '0' || character > '9') + { + builder.Remove(index, 1); + } + } + + context.Token = builder.ToString(); + + return default; + } + } + + /// + /// Contains the logic responsible of validating reference token identifiers. /// Note: this handler is not used when the degraded mode is enabled. /// - public class ValidateReferenceToken : IOpenIddictServerHandler + public class ValidateReferenceTokenIdentifier : IOpenIddictServerHandler { 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 the OpenIddict server feature.") .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") @@ -149,7 +306,7 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public ValidateReferenceToken([NotNull] IOpenIddictTokenManager tokenManager) + public ValidateReferenceTokenIdentifier([NotNull] IOpenIddictTokenManager tokenManager) => _tokenManager = tokenManager; /// @@ -159,9 +316,8 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(NormalizeUserCode.Descriptor.Order + 1_000) .Build(); public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) @@ -171,36 +327,15 @@ namespace OpenIddict.Server 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)) + // 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(identifier); - if (token == null || !await IsTokenTypeValidAsync(token)) + if (!string.IsNullOrEmpty(context.TokenType) && + !string.Equals(context.TokenType, await _tokenManager.GetTypeAsync(token))) { context.Reject( error: context.EndpointType switch @@ -212,6 +347,8 @@ namespace OpenIddict.Server { OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => "The specified authorization code is not valid.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The specified device code is not valid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => "The specified refresh token is not valid.", @@ -230,143 +367,27 @@ namespace OpenIddict.Server .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; - } - - var result = context.EndpointType switch - { - OpenIddictServerEndpointType.Introspection => await ValidateAnyTokenAsync(payload), - OpenIddictServerEndpointType.Revocation => await ValidateAnyTokenAsync(payload), - - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => await ValidateTokenAsync(payload, TokenUsages.AuthorizationCode), - - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => await ValidateTokenAsync(payload, TokenUsages.RefreshToken), - - OpenIddictServerEndpointType.Userinfo => await ValidateTokenAsync(payload, TokenUsages.AccessToken), - - _ => new TokenValidationResult { IsValid = false } - }; - - // If the token cannot be validated, don't return an error to allow another handle to validate it. - if (result.ClaimsIdentity == 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 = 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)); - - context.Logger.LogTrace("The reference JWT token '{Token}' was successfully validated and the following " + - "claims could be extracted: {Claims}.", payload, context.Principal.Claims); - - async ValueTask ValidateTokenAsync(string token, string type) - { - var parameters = context.Options.TokenValidationParameters.Clone(); - parameters.PropertyBag = new Dictionary { [Claims.Private.TokenUsage] = type }; - parameters.ValidIssuer = context.Issuer?.AbsoluteUri; - - parameters.IssuerSigningKeys = type switch - { - TokenUsages.AccessToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), - TokenUsages.AuthorizationCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), - TokenUsages.RefreshToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), - - _ => Array.Empty() - }; - - parameters.TokenDecryptionKeys = type switch - { - TokenUsages.AuthorizationCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), - TokenUsages.RefreshToken => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), - - TokenUsages.AccessToken => context.Options.EncryptionCredentials - .Select(credentials => credentials.Key) - .Where(key => key is SymmetricSecurityKey), - - _ => Array.Empty() - }; - - var result = await context.Options.JsonWebTokenHandler.ValidateTokenStringAsync(token, parameters); - if (!result.IsValid) - { - context.Logger.LogTrace(result.Exception, "An error occurred while validating the token '{Token}'.", token); - } - - return result; - } - - async ValueTask ValidateAnyTokenAsync(string token) - { - var result = await ValidateTokenAsync(token, TokenUsages.AccessToken); - if (result.IsValid) - { - return result; - } - - result = await ValidateTokenAsync(token, TokenUsages.RefreshToken); - if (result.IsValid) - { - return result; - } - - result = await ValidateTokenAsync(token, TokenUsages.AuthorizationCode); - if (result.IsValid) - { - return result; - } - - return new TokenValidationResult { IsValid = false }; - } - - async ValueTask 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 - }, + // Replace the token parameter by the payload resolved from the token entry. + context.Token = payload; - _ => false - }; + // 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); } } /// - /// 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. /// - public class ValidateSelfContainedToken : IOpenIddictServerHandler + public class ValidateIdentityModelToken : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateReferenceToken.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) .Build(); /// @@ -389,62 +410,16 @@ namespace OpenIddict.Server return; } - var token = context.EndpointType switch - { - OpenIddictServerEndpointType.Authorization => context.Request.IdTokenHint, - OpenIddictServerEndpointType.Logout => context.Request.IdTokenHint, - - 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) || !context.Options.JsonWebTokenHandler.CanReadToken(token)) + if (!context.Options.JsonWebTokenHandler.CanReadToken(context.Token)) { return; } - var result = context.EndpointType switch - { - OpenIddictServerEndpointType.Authorization => await ValidateTokenAsync(token, TokenUsages.IdToken), - OpenIddictServerEndpointType.Logout => await ValidateTokenAsync(token, TokenUsages.IdToken), - - // When reference tokens are enabled, this handler can only validate id_tokens. - OpenIddictServerEndpointType.Introspection when context.Options.UseReferenceTokens - => await ValidateTokenAsync(token, TokenUsages.IdToken), - - OpenIddictServerEndpointType.Revocation when context.Options.UseReferenceTokens - => await ValidateTokenAsync(token, TokenUsages.IdToken), - - _ when context.Options.UseReferenceTokens => new TokenValidationResult { IsValid = false }, - - OpenIddictServerEndpointType.Introspection => await ValidateAnyTokenAsync(token), - OpenIddictServerEndpointType.Revocation => await ValidateAnyTokenAsync(token), - - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => await ValidateTokenAsync(token, TokenUsages.AuthorizationCode), - - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => await ValidateTokenAsync(token, TokenUsages.RefreshToken), - - OpenIddictServerEndpointType.Userinfo => await ValidateTokenAsync(token, TokenUsages.AccessToken), - - _ => new TokenValidationResult { IsValid = false } - }; - // If the token cannot be validated, don't return an error to allow another handle to validate it. + var result = !string.IsNullOrEmpty(context.TokenType) ? + await ValidateTokenAsync(context.Token, context.TokenType) : + await ValidateAnyTokenAsync(context.Token); if (result.ClaimsIdentity == null) { return; @@ -453,8 +428,8 @@ namespace OpenIddict.Server // Attach the principal extracted from the token to the parent event context. 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}.", token, context.Principal.Claims); + context.Logger.LogTrace("The token '{Token}' was successfully validated and the following claims " + + "could be extracted: {Claims}.", context.Token, context.Principal.Claims); async ValueTask ValidateTokenAsync(string token, string type) { @@ -466,7 +441,9 @@ namespace OpenIddict.Server { TokenUsages.AccessToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), TokenUsages.AuthorizationCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), + TokenUsages.DeviceCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), TokenUsages.RefreshToken => context.Options.SigningCredentials.Select(credentials => credentials.Key), + TokenUsages.UserCode => context.Options.SigningCredentials.Select(credentials => credentials.Key), TokenUsages.IdToken => context.Options.SigningCredentials .Select(credentials => credentials.Key) @@ -478,7 +455,9 @@ namespace OpenIddict.Server parameters.TokenDecryptionKeys = type switch { TokenUsages.AuthorizationCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), + TokenUsages.DeviceCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), TokenUsages.RefreshToken => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), + TokenUsages.UserCode => context.Options.EncryptionCredentials.Select(credentials => credentials.Key), TokenUsages.AccessToken => context.Options.EncryptionCredentials .Select(credentials => credentials.Key) @@ -527,6 +506,69 @@ namespace OpenIddict.Server } } + /// + /// 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. + /// + public class RestoreReferenceTokenProperties : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RestoreReferenceTokenProperties() => 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 RestoreReferenceTokenProperties([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .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)); + } + } + /// /// Contains the logic responsible of rejecting authentication demands for which no valid principal was resolved. /// @@ -538,7 +580,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateSelfContainedToken.Descriptor.Order + 1_000) + .SetOrder(RestoreReferenceTokenProperties.Descriptor.Order + 1_000) .Build(); /// @@ -570,6 +612,8 @@ namespace OpenIddict.Server OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => "The specified authorization code is not valid.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The specified device code is not valid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => "The specified refresh token is not valid.", @@ -651,6 +695,8 @@ namespace OpenIddict.Server { OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => "The specified authorization code is not valid.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The specified device code is not valid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => "The specified refresh token is not valid.", @@ -660,37 +706,70 @@ namespace OpenIddict.Server return; } - // If the authorization code/refresh token is already marked as redeemed, this may indicate that - // it was compromised. In this case, revoke the authorization and all the associated tokens. - // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. if (context.EndpointType == OpenIddictServerEndpointType.Token && - (context.Request.IsAuthorizationCodeGrantType() || context.Request.IsRefreshTokenGrantType()) && - await _tokenManager.IsRedeemedAsync(token)) - { - await TryRevokeAuthorizationChainAsync(token); + (context.Request.IsAuthorizationCodeGrantType() || + context.Request.IsDeviceCodeGrantType() || + context.Request.IsRefreshTokenGrantType())) + { + // If the authorization code/device code/refresh token is already marked as redeemed, this may indicate + // that it was compromised. In this case, revoke the authorization and all the associated tokens. + // See https://tools.ietf.org/html/rfc6749#section-10.5 for more information. + if (await _tokenManager.HasStatusAsync(token, Statuses.Redeemed)) + { + await TryRevokeAuthorizationChainAsync(token); - context.Logger.LogError("The token '{Identifier}' has already been redeemed.", identifier); + context.Logger.LogError("The token '{Identifier}' has already been redeemed.", identifier); - context.Reject( - error: context.EndpointType switch + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code has already been redeemed.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The specified device code has already been redeemed.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token has already been redeemed.", + + _ => "The specified token has already been redeemed." + }); + + return; + } + + if (context.Request.IsDeviceCodeGrantType()) + { + // If the device code is not marked as valid yet, return an authorization_pending error. + if (await _tokenManager.HasStatusAsync(token, Statuses.Inactive)) { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: context.EndpointType switch + context.Logger.LogError("The token '{Identifier}' is not active yet.", identifier); + + context.Reject( + error: Errors.AuthorizationPending, + description: "The authorization has not been granted yet by the end user."); + + return; + } + + // If the device code is marked as rejected, return an authorization_pending error. + if (await _tokenManager.HasStatusAsync(token, Statuses.Rejected)) { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => "The specified authorization code has already been redeemed.", - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => "The specified refresh token has already been redeemed.", + context.Logger.LogError("The token '{Identifier}' was marked as rejected.", identifier); - _ => "The specified token has already been redeemed." - }); + context.Reject( + error: Errors.AccessDenied, + description: "The authorization demand has been rejected by the end user."); - return; + return; + } + } } - if (!await _tokenManager.IsValidAsync(token)) + if (!await _tokenManager.HasStatusAsync(token, Statuses.Valid)) { context.Logger.LogError("The token '{Identifier}' was no longer valid.", identifier); @@ -704,6 +783,8 @@ namespace OpenIddict.Server { OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => "The specified authorization code is no longer valid.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The specified device code is no longer valid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => "The specified refresh token is no longer valid.", @@ -800,7 +881,7 @@ namespace OpenIddict.Server } 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); @@ -814,6 +895,8 @@ namespace OpenIddict.Server { OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() => "The authorization associated with the authorization code is no longer valid.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The authorization associated with the device code is no longer valid.", OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() => "The authorization associated with the refresh token is no longer valid.", @@ -846,44 +929,87 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + public ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Don't validate the lifetime of id_tokens used as id_token_hints. + switch (context.EndpointType) + { + case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Logout: + return default; + } + + var date = context.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Reject( + error: context.EndpointType switch + { + OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + _ => Errors.InvalidToken + }, + description: context.EndpointType switch + { + OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() + => "The specified authorization code is no longer valid.", + OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() + => "The specified device code is no longer valid.", + OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() + => "The specified refresh token is no longer valid.", + + _ => "The specified token is no longer valid." + }); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting challenge demands made from unsupported endpoints. + /// + public class ValidateChallengeDemand : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessChallengeContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - // Don't validate the lifetime of id_tokens used as id_token_hints. switch (context.EndpointType) { case OpenIddictServerEndpointType.Authorization: - case OpenIddictServerEndpointType.Logout: + case OpenIddictServerEndpointType.Token: + case OpenIddictServerEndpointType.Userinfo: + case OpenIddictServerEndpointType.Verification: return default; - } - - var date = context.Principal.GetExpirationDate(); - if (date.HasValue && date.Value < DateTimeOffset.UtcNow) - { - context.Reject( - error: context.EndpointType switch - { - OpenIddictServerEndpointType.Token => Errors.InvalidGrant, - _ => Errors.InvalidToken - }, - description: context.EndpointType switch - { - OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => "The specified authorization code is no longer valid.", - OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => "The specified refresh token is no longer valid.", - - _ => "The specified token is no longer valid." - }); - return default; + default: throw new InvalidOperationException("No challenge can be triggered from this endpoint."); } - - return default; } } @@ -898,7 +1024,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(int.MinValue + 100_000) + .SetOrder(ValidateChallengeDemand.Descriptor.Order + 1_000) .Build(); /// @@ -922,6 +1048,7 @@ namespace OpenIddict.Server OpenIddictServerEndpointType.Authorization => Errors.AccessDenied, OpenIddictServerEndpointType.Token => Errors.InvalidGrant, OpenIddictServerEndpointType.Userinfo => Errors.InvalidToken, + OpenIddictServerEndpointType.Verification => Errors.AccessDenied, _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") }; @@ -934,6 +1061,7 @@ namespace OpenIddict.Server OpenIddictServerEndpointType.Authorization => "The authorization was denied by the resource owner.", OpenIddictServerEndpointType.Token => "The token request was rejected by the authorization server.", OpenIddictServerEndpointType.Userinfo => "The access token is not valid or cannot be used to retrieve user information.", + OpenIddictServerEndpointType.Verification => "The authorization was denied by the resource owner.", _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") }; @@ -944,17 +1072,153 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of ensuring that the sign-in response + /// Contains the logic responsible of rejecting the device code entry associated with the user code. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RejectDeviceCodeEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RejectDeviceCodeEntry() => 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 RejectDeviceCodeEntry([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(AttachDefaultChallengeError.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessChallengeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Verification) + { + return; + } + + var notification = context.Transaction.GetProperty( + typeof(ProcessAuthenticationContext).FullName) ?? + throw new InvalidOperationException("The authentication context cannot be found."); + + // Extract the device code identifier from the authentication principal. + var identifier = notification.Principal.GetClaim(Claims.Private.DeviceCodeId); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException("The device code identifier cannot be extracted from the principal."); + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token != null) + { + await _tokenManager.TryRejectAsync(token); + } + } + } + + /// + /// Contains the logic responsible of rejecting the user code entry, if applicable. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RejectUserCodeEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RejectUserCodeEntry() => 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 RejectUserCodeEntry([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(RejectDeviceCodeEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessChallengeContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Verification) + { + return; + } + + var notification = context.Transaction.GetProperty( + typeof(ProcessAuthenticationContext).FullName) ?? + throw new InvalidOperationException("The authentication context cannot be found."); + + // Extract the device code identifier from the authentication principal. + var identifier = notification.Principal.GetInternalTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException("The token identifier cannot be extracted from the principal."); + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token != null) + { + await _tokenManager.TryRejectAsync(token); + } + } + } + + /// + /// Contains the logic responsible of ensuring that the sign-in demand /// is compatible with the type of the endpoint that handled the request. /// - public class ValidateSigninResponse : IOpenIddictServerHandler + public class ValidateSigninDemand : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .Build(); @@ -975,25 +1239,61 @@ namespace OpenIddict.Server switch (context.EndpointType) { case OpenIddictServerEndpointType.Authorization: + case OpenIddictServerEndpointType.Device: case OpenIddictServerEndpointType.Token: + case OpenIddictServerEndpointType.Verification: break; default: throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint."); } - if (context.Principal.Identity == null || !context.Principal.Identity.IsAuthenticated) + if (context.Principal.Identity == null) + { + throw new InvalidOperationException(new StringBuilder() + .AppendLine("The specified principal doesn't contain any claims-based identity.") + .Append("Make sure that both 'ClaimsPrincipal.Identity' is not null.") + .ToString()); + } + + // Note: sign-in operations triggered from the device endpoint can't be associated to specific users + // as users' identity is not known until they reach the verification endpoint and validate the user code. + // As such, the principal used in this case cannot contain an authenticated identity or a subject claim. + if (context.EndpointType == OpenIddictServerEndpointType.Device) + { + if (context.Principal.Identity.IsAuthenticated) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The specified principal contains an authenticated identity, which is not valid ") + .AppendLine("when the sign-in operation is triggered from the device authorization endpoint.") + .Append("Make sure that 'ClaimsPrincipal.Identity.AuthenticationType' is null ") + .Append("and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'false'.") + .ToString()); + } + + if (!string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject))) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The specified principal contains a subject claim, which is not valid ") + .Append("when the sign-in operation is triggered from the device authorization endpoint.") + .ToString()); + } + + return default; + } + + if (!context.Principal.Identity.IsAuthenticated) { throw new InvalidOperationException(new StringBuilder() - .AppendLine("The specified principal doesn't contain a valid or authenticated identity.") - .Append("Make sure that both 'ClaimsPrincipal.Identity' and 'ClaimsPrincipal.Identity.AuthenticationType' ") - .Append("are not null and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") + .AppendLine("The specified principal doesn't contain a valid/authenticated identity.") + .Append("Make sure that 'ClaimsPrincipal.Identity.AuthenticationType' is not null ") + .Append("and that 'ClaimsPrincipal.Identity.IsAuthenticated' returns 'true'.") .ToString()); } if (string.IsNullOrEmpty(context.Principal.GetClaim(Claims.Subject))) { throw new InvalidOperationException(new StringBuilder() - .AppendLine("The security principal was rejected because the mandatory subject claim was missing.") + .AppendLine("The specified principal was rejected because the mandatory subject claim was missing.") .ToString()); } @@ -1012,7 +1312,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateSigninResponse.Descriptor.Order + 1_000) + .SetOrder(ValidateSigninDemand.Descriptor.Order + 1_000) .Build(); /// @@ -1029,23 +1329,26 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (context.EndpointType != OpenIddictServerEndpointType.Token) + switch (context.EndpointType) { - return default; - } + case OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType(): + case OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType(): + case OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType(): + case OpenIddictServerEndpointType.Verification: + break; - if (!context.Request.IsAuthorizationCodeGrantType() && !context.Request.IsRefreshTokenGrantType()) - { - return default; + default: + return default; } - if (!context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var principal)) - { - throw new InvalidOperationException("The original principal cannot be resolved from the transaction."); - } + var identity = (ClaimsIdentity) context.Principal.Identity; + + var notification = context.Transaction.GetProperty( + typeof(ProcessAuthenticationContext).FullName) ?? + throw new InvalidOperationException("The authentication context cannot be found."); // Restore the internal claims resolved from the authorization code/refresh token. - foreach (var claims in ((ClaimsPrincipal) principal).Claims + foreach (var claims in notification.Principal.Claims .Where(claim => claim.Type.StartsWith(Claims.Prefixes.Private)) .GroupBy(claim => claim.Type)) { @@ -1055,7 +1358,14 @@ namespace OpenIddict.Server continue; } - ((ClaimsIdentity) context.Principal.Identity).AddClaims(claims); + // When the request is a verification request, don't flow the copy from the user code. + if (context.EndpointType == OpenIddictServerEndpointType.Verification && + string.Equals(claims.Key, Claims.Private.Scopes, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + identity.AddClaims(claims); } return default; @@ -1226,20 +1536,25 @@ namespace OpenIddict.Server // For authorization requests, return an authorization code if a response type containing code was specified. OpenIddictServerEndpointType.Authorization => context.Request.HasResponseType(ResponseTypes.Code), - // For token requests, prevent an authorization code from being returned as this type of token - // cannot be issued from the token endpoint in the standard OAuth 2.0/OpenID Connect flows. - OpenIddictServerEndpointType.Token => false, + _ => false + }; + + context.IncludeDeviceCode = context.EndpointType switch + { + // For device requests, always return a device code. + OpenIddictServerEndpointType.Device => true, + + // Note: a device code is not directly returned by the verification endpoint (that generally + // returns an empty response or redirects the user agent to another page), but a device code + // must be generated to replace the payload of the device code initially returned to the client. + // In this case, the new device code is not returned as part of the response but persisted in the DB. + OpenIddictServerEndpointType.Verification => true, _ => false }; context.IncludeRefreshToken = context.EndpointType switch { - // For authorization requests, prevent a refresh token from being returned as OAuth 2.0 - // explicitly disallows returning a refresh token from the authorization endpoint. - // See https://tools.ietf.org/html/rfc6749#section-4.2.2 for more information. - OpenIddictServerEndpointType.Authorization => false, - // For token requests, never return a refresh token if the offline_access scope was not granted. OpenIddictServerEndpointType.Token when !context.Principal.HasScope(Scopes.OfflineAccess) => false, @@ -1265,6 +1580,14 @@ namespace OpenIddict.Server _ => false }; + context.IncludeUserCode = context.EndpointType switch + { + // Only return a user code if the request is a device authorization request. + OpenIddictServerEndpointType.Device => true, + + _ => false + }; + return default; } } @@ -1319,8 +1642,8 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // If no authorization code or refresh token is returned, don't create an authorization. - if (!context.IncludeAuthorizationCode && !context.IncludeRefreshToken) + // If no authorization code, device code or refresh token is returned, don't create an authorization. + if (!context.IncludeAuthorizationCode && !context.IncludeDeviceCode && !context.IncludeRefreshToken) { return; } @@ -1489,16 +1812,103 @@ namespace OpenIddict.Server context.Request.IsRefreshTokenGrantType() && !string.IsNullOrEmpty(context.Request.Scope)) { var scopes = context.Request.GetScopes(); - if (scopes.Count != 0) + principal.SetClaim(Claims.Scope, string.Join(" ", scopes.Intersect(context.Principal.GetScopes()))); + + context.Logger.LogDebug("The access token scopes will be limited to the scopes " + + "requested by the client application: {Scopes}.", scopes); + } + + context.AccessTokenPrincipal = principal; + + return default; + } + } + + /// + /// Contains the logic responsible of preparing and attaching the claims principal + /// used to generate the authorization code, if one is going to be returned. + /// + public class PrepareAuthorizationCodePrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(PrepareAccessTokenPrincipal.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Create a new principal containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never include the public or internal token identifiers to ensure the identifiers + // that are automatically inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase)) { - context.Logger.LogDebug("The access token scopes will be limited to the scopes " + - "requested by the client application: {Scopes}.", scopes); + return false; + } - principal.SetClaim(Claims.Scope, string.Join(" ", scopes.Intersect(context.Principal.GetScopes()))); + // Never include the creation and expiration dates that are automatically + // inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase)) + { + return false; } + + // Other claims are always included in the authorization code, even private claims. + return true; + }); + + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()); + principal.SetCreationDate(DateTimeOffset.UtcNow); + + var lifetime = context.Principal.GetAuthorizationCodeLifetime() ?? context.Options.AuthorizationCodeLifetime; + if (lifetime.HasValue) + { + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); + } + + // Attach the redirect_uri to allow for later comparison when + // receiving a grant_type=authorization_code token request. + principal.SetClaim(Claims.Private.RedirectUri, context.Request.RedirectUri); + + // Attach the code challenge and the code challenge methods to allow the ValidateCodeVerifier + // handler to validate the code verifier sent by the client as part of the token request. + if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) + { + principal.SetClaim(Claims.Private.CodeChallenge, context.Request.CodeChallenge); + + // Default to S256 if no explicit code challenge method was specified. + principal.SetClaim(Claims.Private.CodeChallengeMethod, + !string.IsNullOrEmpty(context.Request.CodeChallengeMethod) ? + context.Request.CodeChallengeMethod : CodeChallengeMethods.Sha256); } - context.AccessTokenPrincipal = principal; + // Attach the nonce so that it can be later returned by + // the token endpoint as part of the JWT identity token. + principal.SetClaim(Claims.Private.Nonce, context.Request.Nonce); + + context.AuthorizationCodePrincipal = principal; return default; } @@ -1506,18 +1916,17 @@ namespace OpenIddict.Server /// /// Contains the logic responsible of preparing and attaching the claims principal - /// used to generate the authorization code, if one is going to be returned. + /// used to generate the device code, if one is going to be returned. /// - public class PrepareAuthorizationCodePrincipal : IOpenIddictServerHandler + public class PrepareDeviceCodePrincipal : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(PrepareAccessTokenPrincipal.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(PrepareAuthorizationCodePrincipal.Descriptor.Order + 1_000) .Build(); /// @@ -1534,6 +1943,13 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } + // Note: a device code principal is produced when a device code is included in the response or when a + // device code entry is replaced when processing a sign-in response sent to the verification endpoint. + if (context.EndpointType != OpenIddictServerEndpointType.Verification && !context.IncludeDeviceCode) + { + return default; + } + // Create a new principal containing only the filtered claims. // Actors identities are also filtered (delegation scenarios). var principal = context.Principal.Clone(claim => @@ -1555,46 +1971,20 @@ namespace OpenIddict.Server return false; } - // Other claims are always included in the authorization code, even private claims. + // Other claims are always included in the device code, even private claims. return true; }); principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()); principal.SetCreationDate(DateTimeOffset.UtcNow); - var lifetime = context.Principal.GetAuthorizationCodeLifetime() ?? context.Options.AuthorizationCodeLifetime; + var lifetime = context.Principal.GetDeviceCodeLifetime() ?? context.Options.DeviceCodeLifetime; if (lifetime.HasValue) { principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); } - // Attach the redirect_uri to allow for later comparison when - // receiving a grant_type=authorization_code token request. - if (!string.IsNullOrEmpty(context.Request.RedirectUri)) - { - principal.SetClaim(Claims.Private.RedirectUri, context.Request.RedirectUri); - } - - // Attach the code challenge and the code challenge methods to allow the ValidateCodeVerifier - // handler to validate the code verifier sent by the client as part of the token request. - if (!string.IsNullOrEmpty(context.Request.CodeChallenge)) - { - principal.SetClaim(Claims.Private.CodeChallenge, context.Request.CodeChallenge); - - // Default to S256 if no explicit code challenge method was specified. - principal.SetClaim(Claims.Private.CodeChallengeMethod, - !string.IsNullOrEmpty(context.Request.CodeChallengeMethod) ? - context.Request.CodeChallengeMethod : CodeChallengeMethods.Sha256); - } - - // Attach the nonce so that it can be later returned by - // the token endpoint as part of the JWT identity token. - if (!string.IsNullOrEmpty(context.Request.Nonce)) - { - principal.SetClaim(Claims.Private.Nonce, context.Request.Nonce); - } - - context.AuthorizationCodePrincipal = principal; + context.DeviceCodePrincipal = principal; return default; } @@ -1613,7 +2003,7 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(PrepareAuthorizationCodePrincipal.Descriptor.Order + 1_000) + .SetOrder(PrepareDeviceCodePrincipal.Descriptor.Order + 1_000) .Build(); /// @@ -1663,12 +2053,11 @@ namespace OpenIddict.Server if (context.EndpointType == OpenIddictServerEndpointType.Token && context.Request.IsRefreshTokenGrantType() && !context.Options.UseSlidingExpiration) { - if (!context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var property)) - { - throw new InvalidOperationException("The original principal cannot be resolved from the transaction."); - } + var notification = context.Transaction.GetProperty( + typeof(ProcessAuthenticationContext).FullName) ?? + throw new InvalidOperationException("The authentication context cannot be found."); - principal.SetExpirationDate(((ClaimsPrincipal) property).GetExpirationDate()); + principal.SetExpirationDate(notification.Principal.GetExpirationDate()); } else @@ -1720,9 +2109,8 @@ namespace OpenIddict.Server // Actors identities are also filtered (delegation scenarios). var principal = context.Principal.Clone(claim => { - // Never exclude the subject and authorization identifier claims. - if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase) || - string.Equals(claim.Type, Claims.Private.AuthorizationId, StringComparison.OrdinalIgnoreCase)) + // Never exclude the subject claim. + if (string.Equals(claim.Type, Claims.Subject, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -1785,18 +2173,87 @@ namespace OpenIddict.Server // If a nonce was present in the authorization request, it MUST be included in the id_token generated // by the token endpoint. For that, OpenIddict simply flows the nonce as an authorization code claim. // See http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation for more information. + principal.SetClaim(Claims.Nonce, context.EndpointType switch + { + OpenIddictServerEndpointType.Authorization => context.Request.Nonce, + OpenIddictServerEndpointType.Token => context.Principal.GetClaim(Claims.Private.Nonce), + _ => null + }); + + context.IdentityTokenPrincipal = principal; + + return default; + } + } + + /// + /// Contains the logic responsible of preparing and attaching the claims principal + /// used to generate the user code, if one is going to be returned. + /// + public class PrepareUserCodePrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(PrepareIdentityTokenPrincipal.Descriptor.Order + 1_000) + .Build(); - if (context.EndpointType == OpenIddictServerEndpointType.Authorization && !string.IsNullOrEmpty(context.Request.Nonce)) + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) { - principal.SetClaim(Claims.Nonce, context.Request.Nonce); + throw new ArgumentNullException(nameof(context)); } - else if (context.EndpointType == OpenIddictServerEndpointType.Token) + // Create a new principal containing only the filtered claims. + // Actors identities are also filtered (delegation scenarios). + var principal = context.Principal.Clone(claim => + { + // Never include the public or internal token identifiers to ensure the identifiers + // that are automatically inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.JwtId, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.Private.TokenId, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Never include the creation and expiration dates that are automatically + // inherited from the parent token are not reused for the new token. + if (string.Equals(claim.Type, Claims.ExpiresAt, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.IssuedAt, StringComparison.OrdinalIgnoreCase) || + string.Equals(claim.Type, Claims.NotBefore, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Other claims are always included in the authorization code, even private claims. + return true; + }); + + principal.SetClaim(Claims.JwtId, Guid.NewGuid().ToString()); + principal.SetCreationDate(DateTimeOffset.UtcNow); + + var lifetime = context.Principal.GetUserCodeLifetime() ?? context.Options.UserCodeLifetime; + if (lifetime.HasValue) { - principal.SetClaim(Claims.Nonce, context.Principal.GetClaim(Claims.Private.Nonce)); + principal.SetExpirationDate(principal.GetCreationDate() + lifetime.Value); } - context.IdentityTokenPrincipal = principal; + // Store the client_id as a public client_id claim. + principal.SetClaim(Claims.ClientId, context.Request.ClientId); + + context.UserCodePrincipal = principal; return default; } @@ -1807,11 +2264,179 @@ namespace OpenIddict.Server /// corresponding to the received authorization code or refresh token. /// Note: this handler is not used when the degraded mode is enabled. /// - public class RedeemTokenEntry : IOpenIddictServerHandler + public class RedeemTokenEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RedeemTokenEntry() => 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 RedeemTokenEntry([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return; + } + + if (!context.Request.IsAuthorizationCodeGrantType() && + !context.Request.IsDeviceCodeGrantType() && + !context.Request.IsRefreshTokenGrantType()) + { + return; + } + + if (context.Request.IsRefreshTokenGrantType() && !context.Options.UseRollingTokens) + { + return; + } + + // Extract the token identifier from the authentication principal. + // If no token identifier can be found, this indicates that the token has no backing database entry. + var identifier = context.Principal.GetInternalTokenId(); + if (string.IsNullOrEmpty(identifier)) + { + return; + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null) + { + throw new InvalidOperationException("The token details cannot be found in the database."); + } + + // If rolling tokens are enabled or if the request is an authorization_code or device_code request, + // mark the authorization/device code or the refresh token as redeemed to prevent future reuses. + // If the operation fails, return an error indicating the code/token is no longer valid. + // See https://tools.ietf.org/html/rfc6749#section-6 for more information. + if (!await _tokenManager.TryRedeemAsync(token)) + { + context.Reject( + error: Errors.InvalidGrant, + description: + context.Request.IsAuthorizationCodeGrantType() ? + "The specified authorization code is no longer valid." : + context.Request.IsDeviceCodeGrantType() ? + "The specified device code is no longer valid." : + "The specified refresh token is no longer valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of redeeming the device code entry associated with the user code. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RedeemDeviceCodeEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictTokenManager _tokenManager; + + public RedeemDeviceCodeEntry() => 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 RedeemDeviceCodeEntry([NotNull] IOpenIddictTokenManager tokenManager) + => _tokenManager = tokenManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Token) + { + return; + } + + if (!context.Request.IsDeviceCodeGrantType()) + { + return; + } + + // Extract the device code identifier from the authentication principal. + var identifier = context.Principal.GetClaim(Claims.Private.DeviceCodeId); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException("The device code identifier cannot be extracted from the principal."); + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null || !await _tokenManager.TryRedeemAsync(token)) + { + context.Reject( + error: Errors.InvalidGrant, + description: "The specified device code is no longer valid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of redeeming the user code entry, if applicable. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class RedeemUserCodeEntry : IOpenIddictServerHandler { private readonly IOpenIddictTokenManager _tokenManager; - public RedeemTokenEntry() => throw new InvalidOperationException(new StringBuilder() + public RedeemUserCodeEntry() => 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'.") @@ -1819,7 +2444,7 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public RedeemTokenEntry([NotNull] IOpenIddictTokenManager tokenManager) + public RedeemUserCodeEntry([NotNull] IOpenIddictTokenManager tokenManager) => _tokenManager = tokenManager; /// @@ -1829,8 +2454,8 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(100_000) + .UseScopedHandler() + .SetOrder(RedeemDeviceCodeEntry.Descriptor.Order + 1_000) .Build(); /// @@ -1847,13 +2472,12 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (context.EndpointType != OpenIddictServerEndpointType.Token) + if (context.EndpointType != OpenIddictServerEndpointType.Verification) { return; } - // Extract the token identifier from the authentication principal. - // If no token identifier can be found, this indicates that the token has no backing database entry. + // Extract the device code identifier from the authentication principal. var identifier = context.Principal.GetInternalTokenId(); if (string.IsNullOrEmpty(identifier)) { @@ -1861,27 +2485,11 @@ namespace OpenIddict.Server } var token = await _tokenManager.FindByIdAsync(identifier); - if (token == null) - { - throw new InvalidOperationException("The token details cannot be found in the database."); - } - - if (!context.Options.UseRollingTokens && !context.Request.IsAuthorizationCodeGrantType()) - { - return; - } - - // If rolling tokens are enabled or if the request is a grant_type=authorization_code request, - // mark the authorization code or the refresh token as redeemed to prevent future reuses. - // If the operation fails, return an error indicating the code/token is no longer valid. - // See https://tools.ietf.org/html/rfc6749#section-6 for more information. - if (!await _tokenManager.TryRedeemAsync(token)) + if (token == null || !await _tokenManager.TryRedeemAsync(token)) { context.Reject( error: Errors.InvalidGrant, - description: context.Request.IsAuthorizationCodeGrantType() ? - "The specified authorization code is no longer valid." : - "The specified refresh token is no longer valid."); + description: "The specified user code is no longer valid."); return; } @@ -1889,15 +2497,14 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of redeeming the token entry - /// corresponding to the received authorization code or refresh token. + /// Contains the logic responsible of revoking all the tokens that were previously issued. /// Note: this handler is not used when the degraded mode is enabled. /// - public class RevokeRollingTokenEntries : IOpenIddictServerHandler + public class RevokeExistingTokenEntries : IOpenIddictServerHandler { private readonly IOpenIddictTokenManager _tokenManager; - public RevokeRollingTokenEntries() => throw new InvalidOperationException(new StringBuilder() + public RevokeExistingTokenEntries() => 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'.") @@ -1905,7 +2512,7 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public RevokeRollingTokenEntries([NotNull] IOpenIddictTokenManager tokenManager) + public RevokeExistingTokenEntries([NotNull] IOpenIddictTokenManager tokenManager) => _tokenManager = tokenManager; /// @@ -1916,8 +2523,8 @@ namespace OpenIddict.Server .AddFilter() .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(RedeemTokenEntry.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(RedeemUserCodeEntry.Descriptor.Order + 1_000) .Build(); /// @@ -1993,7 +2600,7 @@ namespace OpenIddict.Server .AddFilter() .AddFilter() .UseScopedHandler() - .SetOrder(RevokeRollingTokenEntries.Descriptor.Order + 1_000) + .SetOrder(RevokeExistingTokenEntries.Descriptor.Order + 1_000) .Build(); /// @@ -2044,16 +2651,69 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of generating and attaching - /// the reference access token returned as part of the response. + /// Contains the logic responsible of generating an access token using IdentityModel. + /// + public class GenerateIdentityModelAccessToken : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ExtendRefreshTokenEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + 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)) + { + return; + } + + context.Response.AccessToken = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( + new SecurityTokenDescriptor + { + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, + EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( + credentials => credentials.Key is SymmetricSecurityKey), + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity + }); + + context.Logger.LogTrace("The access token '{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); + } + } + + /// + /// Contains the logic responsible of creating a reference access token entry. /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachReferenceAccessToken : IOpenIddictServerHandler + public class CreateReferenceAccessTokenEntry : IOpenIddictServerHandler { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictTokenManager _tokenManager; - public AttachReferenceAccessToken() => throw new InvalidOperationException(new StringBuilder() + public CreateReferenceAccessTokenEntry() => 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'.") @@ -2061,7 +2721,7 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public AttachReferenceAccessToken( + public CreateReferenceAccessTokenEntry( [NotNull] IOpenIddictApplicationManager applicationManager, [NotNull] IOpenIddictTokenManager tokenManager) { @@ -2076,10 +2736,10 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .AddFilter() + .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(ExtendRefreshTokenEntry.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(GenerateIdentityModelAccessToken.Descriptor.Order + 1_000) .Build(); /// @@ -2096,8 +2756,7 @@ namespace OpenIddict.Server 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 (string.IsNullOrEmpty(context.Response.AccessToken)) { return; } @@ -2115,6 +2774,7 @@ namespace OpenIddict.Server AuthorizationId = context.AccessTokenPrincipal.GetInternalAuthorizationId(), CreationDate = context.AccessTokenPrincipal.GetCreationDate(), ExpirationDate = context.AccessTokenPrincipal.GetExpirationDate(), + Payload = context.Response.AccessToken, Principal = context.AccessTokenPrincipal, ReferenceId = Base64UrlEncoder.Encode(data), Status = Statuses.Valid, @@ -2134,41 +2794,81 @@ namespace OpenIddict.Server descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - descriptor.Payload = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( + var token = await _tokenManager.CreateAsync(descriptor); + + context.AccessTokenPrincipal.SetInternalTokenId(await _tokenManager.GetIdAsync(token)); + context.Response.AccessToken = descriptor.ReferenceId; + + context.Logger.LogTrace("The reference token entry for access token '{Identifier}' was successfully " + + "created with the reference identifier '{ReferenceId}'.", + await _tokenManager.GetIdAsync(token), descriptor.ReferenceId); + } + } + + /// + /// Contains the logic responsible of generating an authorization code using IdentityModel. + /// + public class GenerateIdentityModelAuthorizationCode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(CreateReferenceAccessTokenEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + 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)) + { + return; + } + + context.Response.Code = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { - Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey), Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity + Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity }); - var token = await _tokenManager.CreateAsync(descriptor); - - context.Response.AccessToken = descriptor.ReferenceId; - - context.Logger.LogTrace("The reference access token '{Identifier}' was successfully created with the " + - "reference identifier '{ReferenceId}' and the following JWT 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.AccessTokenPrincipal.Claims); + context.AuthorizationCodePrincipal.GetClaim(Claims.JwtId), + context.Response.Code, context.AuthorizationCodePrincipal.Claims); } } /// - /// Contains the logic responsible of generating and attaching - /// the reference authorization code returned as part of the response. + /// Contains the logic responsible of creating a reference authorization code entry. /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachReferenceAuthorizationCode : IOpenIddictServerHandler + public class CreateReferenceAuthorizationCodeEntry : IOpenIddictServerHandler { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictTokenManager _tokenManager; - public AttachReferenceAuthorizationCode() => throw new InvalidOperationException(new StringBuilder() + public CreateReferenceAuthorizationCodeEntry() => 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'.") @@ -2176,7 +2876,7 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public AttachReferenceAuthorizationCode( + public CreateReferenceAuthorizationCodeEntry( [NotNull] IOpenIddictApplicationManager applicationManager, [NotNull] IOpenIddictTokenManager tokenManager) { @@ -2191,10 +2891,9 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(AttachReferenceAccessToken.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(GenerateIdentityModelAuthorizationCode.Descriptor.Order + 1_000) .Build(); /// @@ -2211,8 +2910,7 @@ namespace OpenIddict.Server 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 (string.IsNullOrEmpty(context.Response.Code)) { return; } @@ -2230,6 +2928,7 @@ namespace OpenIddict.Server AuthorizationId = context.AuthorizationCodePrincipal.GetInternalAuthorizationId(), CreationDate = context.AuthorizationCodePrincipal.GetCreationDate(), ExpirationDate = context.AuthorizationCodePrincipal.GetExpirationDate(), + Payload = context.Response.Code, Principal = context.AuthorizationCodePrincipal, ReferenceId = Base64UrlEncoder.Encode(data), Status = Statuses.Valid, @@ -2249,41 +2948,81 @@ namespace OpenIddict.Server descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - descriptor.Payload = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( + var token = await _tokenManager.CreateAsync(descriptor); + + context.AuthorizationCodePrincipal.SetInternalTokenId(await _tokenManager.GetIdAsync(token)); + context.Response.Code = descriptor.ReferenceId; + + context.Logger.LogTrace("The reference token entry for authorization code '{Identifier}' was successfully " + + "created with the reference identifier '{ReferenceId}'.", + await _tokenManager.GetIdAsync(token), descriptor.ReferenceId); + } + } + + /// + /// Contains the logic responsible of generating a device code using IdentityModel. + /// + public class GenerateIdentityModelDeviceCode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(CreateReferenceAuthorizationCodeEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If a device code was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.DeviceCode)) + { + return; + } + + context.Response.DeviceCode = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { - Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.DeviceCode }, EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey), Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity + Subject = (ClaimsIdentity) context.DeviceCodePrincipal.Identity }); - var token = await _tokenManager.CreateAsync(descriptor); - - context.Response.Code = descriptor.ReferenceId; - - context.Logger.LogTrace("The reference authorization code '{Identifier}' was successfully created with the " + - "reference identifier '{ReferenceId}' and the following payload: {Payload}. " + + context.Logger.LogTrace("The device 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); + context.DeviceCodePrincipal.GetClaim(Claims.JwtId), + context.Response.DeviceCode, context.DeviceCodePrincipal.Claims); } } /// - /// Contains the logic responsible of generating and attaching - /// the reference refresh token returned as part of the response. + /// Contains the logic responsible of creating a reference device code entry. /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachReferenceRefreshToken : IOpenIddictServerHandler + public class CreateReferenceDeviceCodeEntry : IOpenIddictServerHandler { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictTokenManager _tokenManager; - public AttachReferenceRefreshToken() => throw new InvalidOperationException(new StringBuilder() + public CreateReferenceDeviceCodeEntry() => 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'.") @@ -2291,7 +3030,7 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public AttachReferenceRefreshToken( + public CreateReferenceDeviceCodeEntry( [NotNull] IOpenIddictApplicationManager applicationManager, [NotNull] IOpenIddictTokenManager tokenManager) { @@ -2306,10 +3045,9 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(AttachReferenceAuthorizationCode.Descriptor.Order + 1_000) + .AddFilter() + .UseScopedHandler() + .SetOrder(GenerateIdentityModelDeviceCode.Descriptor.Order + 1_000) .Build(); /// @@ -2326,8 +3064,12 @@ namespace OpenIddict.Server 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 (string.IsNullOrEmpty(context.Response.DeviceCode)) + { + return; + } + + if (context.EndpointType == OpenIddictServerEndpointType.Verification) { return; } @@ -2342,14 +3084,15 @@ namespace OpenIddict.Server #endif var descriptor = new OpenIddictTokenDescriptor { - AuthorizationId = context.RefreshTokenPrincipal.GetInternalAuthorizationId(), - CreationDate = context.RefreshTokenPrincipal.GetCreationDate(), - ExpirationDate = context.RefreshTokenPrincipal.GetExpirationDate(), - Principal = context.RefreshTokenPrincipal, + AuthorizationId = context.DeviceCodePrincipal.GetInternalAuthorizationId(), + CreationDate = context.DeviceCodePrincipal.GetCreationDate(), + ExpirationDate = context.DeviceCodePrincipal.GetExpirationDate(), + Payload = context.Response.DeviceCode, + Principal = context.DeviceCodePrincipal, ReferenceId = Base64UrlEncoder.Encode(data), - Status = Statuses.Valid, - Subject = context.RefreshTokenPrincipal.GetClaim(Claims.Subject), - Type = TokenUsages.RefreshToken + Status = Statuses.Inactive, + Subject = null, // Device codes are not bound to a user, which is not known until the user code is populated. + Type = TokenUsages.DeviceCode }; // If the client application is known, associate it with the token. @@ -2364,65 +3107,126 @@ namespace OpenIddict.Server descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); } - descriptor.Payload = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( - new SecurityTokenDescriptor - { - Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, - EncryptingCredentials = context.Options.EncryptionCredentials[0], - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity - }); + var token = await _tokenManager.CreateAsync(descriptor); + + context.DeviceCodePrincipal.SetInternalTokenId(await _tokenManager.GetIdAsync(token)); + context.Response.DeviceCode = descriptor.ReferenceId; + + context.Logger.LogTrace("The reference token entry for device code '{Identifier}' was successfully " + + "created with the reference identifier '{ReferenceId}'.", + await _tokenManager.GetIdAsync(token), descriptor.ReferenceId); + } + } + + /// + /// Contains the logic responsible of updating the existing reference device code entry. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class UpdateReferenceDeviceCodeEntry : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public UpdateReferenceDeviceCodeEntry() => 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 UpdateReferenceDeviceCodeEntry( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(CreateReferenceDeviceCodeEntry.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Response.DeviceCode)) + { + return; + } + + if (context.EndpointType != OpenIddictServerEndpointType.Verification) + { + return; + } + + // Extract the token identifier from the authentication principal. + var identifier = context.Principal.GetClaim(Claims.Private.DeviceCodeId); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException("The device code identifier cannot be extracted from the principal."); + } + + var token = await _tokenManager.FindByIdAsync(identifier); + if (token == null) + { + throw new InvalidOperationException("The token details cannot be found in the database."); + } - var token = await _tokenManager.CreateAsync(descriptor); + // Replace the device code details by the payload derived from the new device code principal, + // that includes all the user claims populated by the application after authenticating the user. + var descriptor = new OpenIddictTokenDescriptor(); + await _tokenManager.PopulateAsync(descriptor, token); - context.Response.RefreshToken = descriptor.ReferenceId; + // Note: the lifetime is deliberately extended to give more time to the client to redeem the code. + descriptor.ExpirationDate = context.DeviceCodePrincipal.GetExpirationDate(); + descriptor.Payload = context.Response.DeviceCode; + descriptor.Status = Statuses.Valid; + descriptor.Subject = context.DeviceCodePrincipal.GetClaim(Claims.Subject); - context.Logger.LogTrace("The reference refresh token '{Identifier}' was successfully created with the " + - "reference identifier '{ReferenceId}' and the following 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); + await _tokenManager.PopulateAsync(token, descriptor); + await _tokenManager.UpdateAsync(token); + + // Don't return the prepared device code directly from the verification endpoint. + context.Response.DeviceCode = null; + + context.Logger.LogTrace("The reference token entry for device code '{Identifier}' was successfully updated'.", + await _tokenManager.GetIdAsync(token)); } } /// - /// Contains the logic responsible of creating a token entry in the database for the authorization code. - /// Note: this handler is not used when the degraded mode is enabled. + /// Contains the logic responsible of generating a refresh token using IdentityModel. /// - public class CreateSelfContainedAuthorizationCodeEntry : IOpenIddictServerHandler + public class GenerateIdentityModelRefreshToken : IOpenIddictServerHandler { - private readonly IOpenIddictApplicationManager _applicationManager; - private readonly IOpenIddictTokenManager _tokenManager; - - public CreateSelfContainedAuthorizationCodeEntry() => 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 CreateSelfContainedAuthorizationCodeEntry( - [NotNull] IOpenIddictApplicationManager applicationManager, - [NotNull] IOpenIddictTokenManager tokenManager) - { - _applicationManager = applicationManager; - _tokenManager = tokenManager; - } - /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .AddFilter() - .AddFilter() - .UseScopedHandler() - .SetOrder(ExtendRefreshTokenEntry.Descriptor.Order + 1_000) + .AddFilter() + .UseSingletonHandler() + .SetOrder(UpdateReferenceDeviceCodeEntry.Descriptor.Order + 1_000) .Build(); /// @@ -2439,55 +3243,40 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // If a token identifier was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.AuthorizationCodePrincipal.GetInternalTokenId())) + // If a refresh token was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.RefreshToken)) { return; } - var descriptor = new OpenIddictTokenDescriptor - { - AuthorizationId = context.AuthorizationCodePrincipal.GetInternalAuthorizationId(), - CreationDate = context.AuthorizationCodePrincipal.GetCreationDate(), - ExpirationDate = context.AuthorizationCodePrincipal.GetExpirationDate(), - Principal = context.AuthorizationCodePrincipal, - 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) + context.Response.RefreshToken = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( + new SecurityTokenDescriptor { - throw new InvalidOperationException("The application entry cannot be found in the database."); - } - - descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); - } - - var token = await _tokenManager.CreateAsync(descriptor); - var identifier = await _tokenManager.GetIdAsync(token); - - // Set the internal token identifier so that it can be added to the serialized code. - context.AuthorizationCodePrincipal.SetInternalTokenId(identifier); + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, + EncryptingCredentials = context.Options.EncryptionCredentials[0], + Issuer = context.Issuer?.AbsoluteUri, + SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => + credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), + Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity + }); - context.Logger.LogTrace("The entry for authorization code '{Identifier}' was successfully created.", identifier); + context.Logger.LogTrace("The refresh token '{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); } } /// - /// Contains the logic responsible of creating a token entry in the database for the refresh token. + /// Contains the logic responsible of creating a reference refresh token entry. /// Note: this handler is not used when the degraded mode is enabled. /// - public class CreateSelfContainedRefreshTokenEntry : IOpenIddictServerHandler + public class CreateReferenceRefreshTokenEntry : IOpenIddictServerHandler { private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictTokenManager _tokenManager; - public CreateSelfContainedRefreshTokenEntry() => throw new InvalidOperationException(new StringBuilder() + public CreateReferenceRefreshTokenEntry() => 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'.") @@ -2495,7 +3284,7 @@ namespace OpenIddict.Server .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") .ToString()); - public CreateSelfContainedRefreshTokenEntry( + public CreateReferenceRefreshTokenEntry( [NotNull] IOpenIddictApplicationManager applicationManager, [NotNull] IOpenIddictTokenManager tokenManager) { @@ -2510,10 +3299,9 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .AddFilter() .AddFilter() - .UseScopedHandler() - .SetOrder(CreateSelfContainedAuthorizationCodeEntry.Descriptor.Order + 1_000) + .UseScopedHandler() + .SetOrder(GenerateIdentityModelRefreshToken.Descriptor.Order + 1_000) .Build(); /// @@ -2530,18 +3318,27 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - // If a token identifier was already attached by another handler, don't overwrite it. - if (!string.IsNullOrEmpty(context.RefreshTokenPrincipal.GetInternalTokenId())) + if (string.IsNullOrEmpty(context.Response.RefreshToken)) { return; } + // 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 = context.Response.RefreshToken, Principal = context.RefreshTokenPrincipal, + ReferenceId = Base64UrlEncoder.Encode(data), Status = Statuses.Valid, Subject = context.RefreshTokenPrincipal.GetClaim(Claims.Subject), Type = TokenUsages.RefreshToken @@ -2560,30 +3357,30 @@ namespace OpenIddict.Server } var token = await _tokenManager.CreateAsync(descriptor); - var identifier = await _tokenManager.GetIdAsync(token); - // Set the internal token identifier so that it can be added to the serialized token. - context.RefreshTokenPrincipal.SetInternalTokenId(identifier); + context.RefreshTokenPrincipal.SetInternalTokenId(await _tokenManager.GetIdAsync(token)); + context.Response.RefreshToken = descriptor.ReferenceId; - context.Logger.LogTrace("The entry for refresh token '{Identifier}' was successfully created.", identifier); + context.Logger.LogTrace("The reference token entry for refresh token '{Identifier}' was successfully " + + "created with the reference identifier '{ReferenceId}'.", + await _tokenManager.GetIdAsync(token), descriptor.ReferenceId); } } /// - /// Contains the logic responsible of generating and attaching - /// the self-contained access token returned as part of the response. + /// Contains the logic responsible of generating and attaching the device code identifier to the user code principal. /// - public class AttachSelfContainedAccessToken : IOpenIddictServerHandler + public class AttachDeviceCodeIdentifier : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(CreateSelfContainedRefreshTokenEntry.Descriptor.Order + 1_000) + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(CreateReferenceRefreshTokenEntry.Descriptor.Order + 1_000) .Build(); /// @@ -2593,54 +3390,36 @@ namespace OpenIddict.Server /// /// A that can be used to monitor the asynchronous operation. /// - public async ValueTask HandleAsync([NotNull] ProcessSigninContext context) + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) { if (context == null) { 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)) + var identifier = context.DeviceCodePrincipal.GetInternalTokenId(); + if (!string.IsNullOrEmpty(identifier)) { - return; + context.UserCodePrincipal.SetClaim(Claims.Private.DeviceCodeId, identifier); } - context.Response.AccessToken = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( - new SecurityTokenDescriptor - { - Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AccessToken }, - EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( - credentials => credentials.Key is SymmetricSecurityKey), - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.AccessTokenPrincipal.Identity - }); - - context.Logger.LogTrace("The access token '{Identifier}' was successfully created and the " + - "following JWT payload was attached to the OpenID Connect response: {Payload}. " + - "The principal used to create the token contained the following claims: {Claims}.", - context.AccessTokenPrincipal.GetClaim(Claims.JwtId), - context.Response.AccessToken, context.AccessTokenPrincipal.Claims); + return default; } } /// - /// Contains the logic responsible of generating and attaching - /// the self-contained authorization code returned as part of the response. + /// Contains the logic responsible of generating a user code using IdentityModel. /// - public class AttachSelfContainedAuthorizationCode : IOpenIddictServerHandler + public class GenerateIdentityModelUserCode : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(AttachSelfContainedAccessToken.Descriptor.Order + 1_000) + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachDeviceCodeIdentifier.Descriptor.Order + 1_000) .Build(); /// @@ -2657,47 +3436,66 @@ namespace OpenIddict.Server 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 user code was already attached by another handler, don't overwrite it. + if (!string.IsNullOrEmpty(context.Response.UserCode)) { return; } - context.Response.Code = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( + context.Response.UserCode = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( new SecurityTokenDescriptor { - Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.AuthorizationCode }, + Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.UserCode }, EncryptingCredentials = context.Options.EncryptionCredentials.FirstOrDefault( credentials => credentials.Key is SymmetricSecurityKey), Issuer = context.Issuer?.AbsoluteUri, SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.AuthorizationCodePrincipal.Identity + Subject = (ClaimsIdentity) context.UserCodePrincipal.Identity }); - 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 user code '{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.UserCodePrincipal.GetClaim(Claims.JwtId), + context.Response.UserCode, context.UserCodePrincipal.Claims); } } /// - /// Contains the logic responsible of generating and attaching - /// the self-contained refresh token returned as part of the response. + /// Contains the logic responsible of creating a reference user code entry. + /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachSelfContainedRefreshToken : IOpenIddictServerHandler + public class CreateReferenceUserCodeEntry : IOpenIddictServerHandler { + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictTokenManager _tokenManager; + + public CreateReferenceUserCodeEntry() => 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 CreateReferenceUserCodeEntry( + [NotNull] IOpenIddictApplicationManager applicationManager, + [NotNull] IOpenIddictTokenManager tokenManager) + { + _applicationManager = applicationManager; + _tokenManager = tokenManager; + } + /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .AddFilter() - .UseSingletonHandler() - .SetOrder(AttachSelfContainedAuthorizationCode.Descriptor.Order + 1_000) + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(GenerateIdentityModelUserCode.Descriptor.Order + 1_000) .Build(); /// @@ -2714,28 +3512,64 @@ namespace OpenIddict.Server 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 (string.IsNullOrEmpty(context.Response.UserCode)) { return; } - context.Response.RefreshToken = await context.Options.JsonWebTokenHandler.CreateTokenFromDescriptorAsync( - new SecurityTokenDescriptor + // Note: unlike other reference tokens, user codes are meant to be used by humans, + // who may have to enter it in a web form. To ensure it remains easy enough to type + // even by users with non-Latin keyboards, user codes generated by OpenIddict are + // only compound of 12 digits, generated using a crypto-secure random number generator. + // In this case, the resulting user code is estimated to have at most ~40 bits of entropy. + + var data = new byte[12]; +#if SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + RandomNumberGenerator.Fill(data); +#else + using var generator = RandomNumberGenerator.Create(); + generator.GetBytes(data); +#endif + var builder = new StringBuilder(data.Length); + + for (var index = 0; index < data.Length; index += 4) + { + builder.AppendFormat(CultureInfo.InvariantCulture, "{0:D4}", BitConverter.ToUInt32(data, index) % 10000); + } + + var descriptor = new OpenIddictTokenDescriptor + { + AuthorizationId = context.UserCodePrincipal.GetInternalAuthorizationId(), + CreationDate = context.UserCodePrincipal.GetCreationDate(), + ExpirationDate = context.UserCodePrincipal.GetExpirationDate(), + Payload = context.Response.UserCode, + Principal = context.UserCodePrincipal, + ReferenceId = builder.ToString(), + Status = Statuses.Valid, + Subject = null, // User codes are not bound to a user until authorization is granted. + Type = TokenUsages.UserCode + }; + + // 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) { - Claims = new Dictionary { [Claims.Private.TokenUsage] = TokenUsages.RefreshToken }, - EncryptingCredentials = context.Options.EncryptionCredentials[0], - Issuer = context.Issuer?.AbsoluteUri, - SigningCredentials = context.Options.SigningCredentials.FirstOrDefault(credentials => - credentials.Key is SymmetricSecurityKey) ?? context.Options.SigningCredentials.First(), - Subject = (ClaimsIdentity) context.RefreshTokenPrincipal.Identity - }); + throw new InvalidOperationException("The application entry cannot be found in the database."); + } - context.Logger.LogTrace("The refresh token '{Identifier}' was successfully created and the " + - "following JWT payload was attached to the OpenID Connect response: {Payload}. " + - "The principal used to create the token contained the following claims: {Claims}.", - context.RefreshTokenPrincipal.GetClaim(Claims.JwtId), - context.Response.RefreshToken, context.RefreshTokenPrincipal.Claims); + descriptor.ApplicationId = await _applicationManager.GetIdAsync(application); + } + + var token = await _tokenManager.CreateAsync(descriptor); + + context.UserCodePrincipal.SetInternalTokenId(await _tokenManager.GetIdAsync(token)); + context.Response.UserCode = builder.ToString(); + + context.Logger.LogTrace("The reference token entry for user code '{Identifier}' was successfully " + + "created with the reference identifier '{ReferenceId}'.", + await _tokenManager.GetIdAsync(token), descriptor.ReferenceId); } } @@ -2752,7 +3586,7 @@ namespace OpenIddict.Server = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(AttachSelfContainedRefreshToken.Descriptor.Order + 1_000) + .SetOrder(CreateReferenceUserCodeEntry.Descriptor.Order + 1_000) .Build(); /// @@ -2872,10 +3706,9 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of generating and attaching - /// the self-contained identity token returned as part of the response. + /// Contains the logic responsible of generating an identity token using IdentityModel. /// - public class AttachSelfContainedIdentityToken : IOpenIddictServerHandler + public class GenerateIdentityModelIdentityToken : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. @@ -2883,7 +3716,7 @@ namespace OpenIddict.Server public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(AttachTokenDigests.Descriptor.Order + 1_000) .Build(); @@ -2917,8 +3750,7 @@ namespace OpenIddict.Server Subject = (ClaimsIdentity) context.IdentityTokenPrincipal.Identity }); - context.Logger.LogTrace("The identity token '{Identifier}' was successfully created and the " + - "following JWT payload was attached to the OpenID Connect response: {Payload}. " + + context.Logger.LogTrace("The identity token '{Identifier}' was successfully created: {Payload}. " + "The principal used to create the token contained the following claims: {Claims}.", context.IdentityTokenPrincipal.GetClaim(Claims.JwtId), context.Response.IdToken, context.IdentityTokenPrincipal.Claims); @@ -2926,18 +3758,23 @@ namespace OpenIddict.Server } /// - /// Contains the logic responsible of attaching additional properties to the sign-in response. + /// Contains the logic responsible of beautifying the user code returned to the client. + /// Note: this handler is not used when the degraded mode is enabled. /// - public class AttachAdditionalProperties : IOpenIddictServerHandler + public class BeautifyUserCode : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(AttachSelfContainedIdentityToken.Descriptor.Order + 1_000) + // Technically, this handler doesn't require that the degraded mode be disabled + // but the default CreateReferenceUserCodeEntry that creates the user code + // reference identifiers only works when the degraded mode is disabled. + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(GenerateIdentityModelIdentityToken.Descriptor.Order + 1_000) .Build(); /// @@ -2954,9 +3791,57 @@ namespace OpenIddict.Server throw new ArgumentNullException(nameof(context)); } - if (context.AccessTokenPrincipal == null) + // To make user codes easier to read and type by humans, a dash is automatically + // appended before each new block of 4 integers. These dashes are expected to be + // stripped from the user codes when receiving them at the verification endpoint. + + var builder = new StringBuilder(context.Response.UserCode); + if (builder.Length % 4 != 0) + { + return default; + } + + for (var index = builder.Length; index >= 0; index -= 4) + { + if (index != 0 && index != builder.Length) + { + builder.Insert(index, Separators.Dash[0]); + } + } + + context.Response.UserCode = builder.ToString(); + + return default; + } + } + + /// + /// Contains the logic responsible of attaching additional access token properties to the sign-in response. + /// + public class AttachAccessTokenProperties : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(BeautifyUserCode.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) { - throw new InvalidOperationException("The access token principal couldn't be found."); + throw new ArgumentNullException(nameof(context)); } context.Response.TokenType = TokenTypes.Bearer; @@ -2971,7 +3856,7 @@ namespace OpenIddict.Server // If the granted access token scopes differ from the requested scopes, return the granted scopes // list as a parameter to inform the client application of the fact the scopes set will be reduced. if ((context.EndpointType == OpenIddictServerEndpointType.Token && context.Request.IsAuthorizationCodeGrantType()) || - !context.AccessTokenPrincipal.GetScopes().SetEquals(context.Request.GetScopes())) + !context.AccessTokenPrincipal.GetScopes().SetEquals(context.Request.GetScopes())) { context.Response.Scope = string.Join(" ", context.AccessTokenPrincipal.GetScopes()); } @@ -2979,5 +3864,63 @@ namespace OpenIddict.Server return default; } } + + /// + /// Contains the logic responsible of attaching additional device code properties to the sign-in response. + /// + public class AttachDeviceCodeProperties : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachAccessTokenProperties.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ProcessSigninContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var endpoint = context.Options.VerificationEndpointUris.FirstOrDefault(); + if (endpoint != null) + { + if (context.Issuer == null || !context.Issuer.IsAbsoluteUri) + { + throw new InvalidOperationException("An absolute URL cannot be built for the device endpoint path."); + } + + var address = new Uri(context.Issuer, endpoint); + var builder = new UriBuilder(address) + { + Query = string.Concat(Parameters.UserCode, "=", context.Response.UserCode) + }; + + context.Response[Parameters.VerificationUri] = address.AbsoluteUri; + context.Response[Parameters.VerificationUriComplete] = builder.Uri.AbsoluteUri; + } + + // If an expiration date was set on the device code principal, return it to the client application. + var date = context.DeviceCodePrincipal.GetExpirationDate(); + if (date.HasValue && date.Value > DateTimeOffset.UtcNow) + { + context.Response.ExpiresIn = (long) ((date.Value - DateTimeOffset.UtcNow).TotalSeconds + .5); + } + + return default; + } + } } } diff --git a/src/OpenIddict.Server/OpenIddictServerHelpers.cs b/src/OpenIddict.Server/OpenIddictServerHelpers.cs new file mode 100644 index 00000000..357e9251 --- /dev/null +++ b/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 +{ + /// + /// Exposes extensions simplifying the integration with the OpenIddict server services. + /// + public static class OpenIddictServerHelpers + { + /// + /// Retrieves a property value from the server transaction using the specified name. + /// + /// The type of the property. + /// The server transaction. + /// The property name. + /// The property value or null if it couldn't be found. + public static TProperty GetProperty( + [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; + } + + /// + /// Sets a property in the server transaction using the specified name and value. + /// + /// The type of the property. + /// The server transaction. + /// The property name. + /// The property value. + /// The server transaction, so that calls can be easily chained. + public static OpenIddictServerTransaction SetProperty( + [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; + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index 9126eabc..c884f19d 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -58,6 +58,11 @@ namespace OpenIddict.Server new Uri("/.well-known/jwks", UriKind.Relative) }; + /// + /// Gets the absolute and relative URIs associated to the device endpoint. + /// + public IList DeviceEndpointUris { get; } = new List(); + /// /// Gets the absolute and relative URIs associated to the introspection endpoint. /// @@ -83,6 +88,11 @@ namespace OpenIddict.Server /// public IList UserinfoEndpointUris { get; } = new List(); + /// + /// Gets the absolute and relative URIs associated to the verification endpoint. + /// + public IList VerificationEndpointUris { get; } = new List(); + /// /// Gets or sets the JWT handler used to protect and unprotect tokens. /// @@ -117,6 +127,14 @@ namespace OpenIddict.Server /// public TimeSpan? AccessTokenLifetime { get; set; } = TimeSpan.FromHours(1); + /// + /// 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 null to issue codes that never expire. + /// Note: the same value should be chosen for both and this property. + /// + public TimeSpan? DeviceCodeLifetime { get; set; } = TimeSpan.FromMinutes(10); + /// /// 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 /// public TimeSpan? RefreshTokenLifetime { get; set; } = TimeSpan.FromDays(14); + /// + /// 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 null to issue codes that never expire. + /// Note: the same value should be chosen for both and this property. + /// + public TimeSpan? UserCodeLifetime { get; set; } = TimeSpan.FromMinutes(10); + /// /// 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 /// /// Gets or sets a boolean indicating whether new refresh tokens should be issued during a refresh token request. - /// Set this property to true to issue a new refresh token, false to prevent the OpenID Connect - /// server middleware from issuing new refresh tokens when receiving a grant_type=refresh_token request. + /// Set this property to true to issue a new refresh token, false to prevent OpenIddict + /// from issuing new refresh tokens when receiving a grant_type=refresh_token request. /// public bool UseSlidingExpiration { get; set; } = true; @@ -250,16 +276,14 @@ namespace OpenIddict.Server }; /// - /// Gets or sets a boolean indicating whether reference tokens should be used. - /// When set to true, 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 true, 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. /// - public bool UseReferenceTokens { get; set; } + public bool UseReferenceAccessTokens { get; set; } /// /// Gets or sets a boolean indicating whether rolling tokens should be used. diff --git a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs b/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs index fe2a234b..1149fd1a 100644 --- a/src/OpenIddict.Validation.DataProtection/OpenIddictValidationDataProtectionHandlers.cs +++ b/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); /// - /// 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. /// - public class ValidateReferenceDataProtectionToken : IOpenIddictValidationHandler + public class ValidateDataProtectionToken : IOpenIddictValidationHandler { - private readonly IOpenIddictTokenManager _tokenManager; private readonly IOptionsMonitor _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 options) - { - _tokenManager = tokenManager; - _options = options; - } - - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseScopedHandler() - .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); - } - } - - /// - /// Contains the logic responsible of rejecting authentication demands - /// that specify an invalid self-contained Data Protection token. - /// - public class ValidateSelfContainedDataProtectionToken : IOpenIddictValidationHandler - { - private readonly IOptionsMonitor _options; - - public ValidateSelfContainedDataProtectionToken([NotNull] IOptionsMonitor options) + public ValidateDataProtectionToken([NotNull] IOptionsMonitor options) => _options = options; /// @@ -165,9 +47,8 @@ namespace OpenIddict.Validation.DataProtection /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(ValidateSelfContainedToken.Descriptor.Order + 500) + .UseSingletonHandler() + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 500) .Build(); /// diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs index 4d0affb5..a7846b08 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs +++ b/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 /// public class OpenIddictValidationOwinHandler : AuthenticationHandler { - private readonly ILogger _logger; private readonly IOpenIddictValidationProvider _provider; /// /// Creates a new instance of the class. /// - /// The logger used by this instance. /// The OpenIddict validation OWIN provider used by this instance. - public OpenIddictValidationOwinHandler( - [NotNull] ILogger logger, - [NotNull] IOpenIddictValidationProvider provider) - { - _logger = logger; - _provider = provider; - } + public OpenIddictValidationOwinHandler([NotNull] IOpenIddictValidationProvider provider) + => _provider = provider; public override async Task 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 = diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs index 3f1d372c..69e4dd4f 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs +++ b/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 /// public class OpenIddictValidationOwinMiddleware : AuthenticationMiddleware { - private readonly ILogger _logger; private readonly IOpenIddictValidationProvider _provider; /// /// Creates a new instance of the class. /// /// The next middleware in the pipeline, if applicable. - /// The logger used by this middleware. /// The OpenIddict validation OWIN options. /// The OpenIddict validation provider. public OpenIddictValidationOwinMiddleware( [CanBeNull] OwinMiddleware next, - [NotNull] ILogger logger, [NotNull] IOptionsMonitor options, [NotNull] IOpenIddictValidationProvider provider) : base(next, options.CurrentValue) - { - _logger = logger; - _provider = provider; - } + => _provider = provider; /// /// Creates and returns a new instance. /// /// A new instance of the class. protected override AuthenticationHandler CreateHandler() - => new OpenIddictValidationOwinHandler(_logger, _provider); + => new OpenIddictValidationOwinHandler(_provider); } } diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddlewareFactory.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddlewareFactory.cs index e9e6a6e8..06351a61 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddlewareFactory.cs +++ b/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>(provider), options: GetRequiredService>(provider), provider: GetRequiredService(provider)); diff --git a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs index 4f91b2e7..9bb1db51 100644 --- a/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs +++ b/src/OpenIddict.Validation.ServerIntegration/OpenIddictValidationServerIntegrationConfiguration.cs @@ -57,7 +57,7 @@ namespace OpenIddict.Validation.ServerIntegration } } - options.UseReferenceTokens = _options.CurrentValue.UseReferenceTokens; + options.UseReferenceAccessTokens = _options.CurrentValue.UseReferenceAccessTokens; } } } diff --git a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs index c5fd8721..bce643a5 100644 --- a/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs +++ b/src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs @@ -60,7 +60,7 @@ namespace OpenIddict.Validation.SystemNetHttp = OpenIddictValidationHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(ValidateSelfContainedToken.Descriptor.Order - 500) + .SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500) .Build(); public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context) diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index f6d9cf98..1fb8ff37 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -620,17 +620,14 @@ namespace Microsoft.Extensions.DependencyInjection } /// - /// 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. /// /// The . - public OpenIddictValidationBuilder UseReferenceTokens() - => Configure(options => options.UseReferenceTokens = true); + public OpenIddictValidationBuilder UseReferenceAccessTokens() + => Configure(options => options.UseReferenceAccessTokens = true); /// /// Determines whether the specified object is equal to the current object. diff --git a/src/OpenIddict.Validation/OpenIddictValidationConstants.cs b/src/OpenIddict.Validation/OpenIddictValidationConstants.cs new file mode 100644 index 00000000..5d3ef499 --- /dev/null +++ b/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"; + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index eebaff5d..0d5dab00 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -251,6 +251,11 @@ namespace OpenIddict.Validation /// Gets or sets the security principal. /// public ClaimsPrincipal Principal { get; set; } + + /// + /// Gets or sets the token to validate. + /// + public string Token { get; set; } } /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index 408eb93c..f834eb63 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/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(); - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Note: TryAddEnumerable() is used here to ensure the initializer is registered only once. builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton< diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs index 7f1f6c1d..8ad59c18 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlerFilters.cs @@ -32,9 +32,9 @@ namespace OpenIddict.Validation } /// - /// 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. /// - public class RequireReferenceTokensDisabled : IOpenIddictValidationHandlerFilter + public class RequireReferenceAccessTokensEnabled : IOpenIddictValidationHandlerFilter { public ValueTask IsActiveAsync([NotNull] BaseContext context) { @@ -43,23 +43,7 @@ namespace OpenIddict.Validation throw new ArgumentNullException(nameof(context)); } - return new ValueTask(!context.Options.UseReferenceTokens); - } - } - - /// - /// Represents a filter that excludes the associated handlers if reference tokens are disabled. - /// - public class RequireReferenceTokensEnabled : IOpenIddictValidationHandlerFilter - { - public ValueTask IsActiveAsync([NotNull] BaseContext context) - { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new ValueTask(context.Options.UseReferenceTokens); + return new ValueTask(context.Options.UseReferenceAccessTokens); } } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index a69963d5..a6f851bb 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/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; } } /// - /// 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. /// - public class ValidateReferenceToken : IOpenIddictValidationHandler + public class ValidateReferenceTokenIdentifier : IOpenIddictValidationHandler { 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; /// @@ -106,8 +110,8 @@ namespace OpenIddict.Validation /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseScopedHandler() + .AddFilter() + .UseScopedHandler() .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 { [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); } } /// - /// 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. /// - public class ValidateSelfContainedToken : IOpenIddictValidationHandler + public class ValidateIdentityModelToken : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(ValidateReferenceToken.Descriptor.Order + 1_000) + .UseSingletonHandler() + .SetOrder(ValidateReferenceTokenIdentifier.Descriptor.Order + 1_000) .Build(); /// @@ -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); + } + } + + /// + /// 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. + /// + public class RestoreReferenceTokenProperties : IOpenIddictValidationHandler + { + 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; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseScopedHandler() + .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() .UseSingletonHandler() - .SetOrder(ValidateSelfContainedToken.Descriptor.Order + 1_000) + .SetOrder(ValidateIdentityModelToken.Descriptor.Order + 1_000) .Build(); /// @@ -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); diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 468d054b..f629d26f 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -50,16 +50,14 @@ namespace OpenIddict.Validation public bool EnableAuthorizationValidation { get; set; } /// - /// Gets or sets a boolean indicating whether reference tokens should be used. - /// When set to true, 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 true, 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. /// - public bool UseReferenceTokens { get; set; } + public bool UseReferenceAccessTokens { get; set; } /// /// Gets or sets the absolute URL of the OAuth 2.0/OpenID Connect server.