From 2e45becc5ac45382f38ae8b3d277d8a95589ae80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sun, 1 Sep 2019 19:25:58 +0200 Subject: [PATCH] Bring back userinfo support --- samples/Mvc.Client/Startup.cs | 2 +- .../Controllers/UserinfoController.cs | 17 +- .../OpenIddictServerAspNetCoreExtensions.cs | 1 + .../OpenIddictServerAspNetCoreHandler.cs | 50 +- ...IddictServerAspNetCoreHandlers.Userinfo.cs | 144 ++++ .../OpenIddictServerAspNetCoreHandlers.cs | 59 +- .../OpenIddictServerOwinExtensions.cs | 1 + .../OpenIddictServerOwinHandler.cs | 41 ++ .../OpenIddictServerOwinHandlers.Userinfo.cs | 145 ++++ .../OpenIddictServerOwinHandlers.cs | 59 +- .../OpenIddictServerEvents.Userinfo.cs | 11 + .../OpenIddictServerHandlers.Userinfo.cs | 638 ++++++++++++++++++ .../OpenIddictServerHandlers.cs | 5 +- 13 files changed, 1151 insertions(+), 22 deletions(-) create mode 100644 src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs create mode 100644 src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs create mode 100644 src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 18f0c190..73e225ef 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -32,7 +32,7 @@ namespace Mvc.Client options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654"; options.RequireHttpsMetadata = false; - options.GetClaimsFromUserInfoEndpoint = false; + options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; // Use the authorization code flow. diff --git a/samples/Mvc.Server/Controllers/UserinfoController.cs b/samples/Mvc.Server/Controllers/UserinfoController.cs index b4e0a15b..c6ef839b 100644 --- a/samples/Mvc.Server/Controllers/UserinfoController.cs +++ b/samples/Mvc.Server/Controllers/UserinfoController.cs @@ -1,13 +1,16 @@ -using System.Threading.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Mvc.Server.Models; using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; namespace Mvc.Server.Controllers { - [Route("api")] public class UserinfoController : Controller { private readonly UserManager _userManager; @@ -19,17 +22,17 @@ namespace Mvc.Server.Controllers // // GET: /api/userinfo - //[Authorize(AuthenticationSchemes = OpenIddictValidationDefaults.AuthenticationScheme)] - [HttpGet("userinfo"), Produces("application/json")] + [Authorize(AuthenticationSchemes = OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)] + [HttpGet("~/connect/userinfo"), Produces("application/json")] public async Task Userinfo() { var user = await _userManager.GetUserAsync(User); if (user == null) { - return Challenge(); + return Challenge(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } - var claims = new JObject(); + var claims = new Dictionary(StringComparer.Ordinal); // Note: the "sub" claim is a mandatory claim and must be included in the JSON response. claims[OpenIddictConstants.Claims.Subject] = await _userManager.GetUserIdAsync(user); @@ -54,7 +57,7 @@ namespace Mvc.Server.Controllers // Note: the complete list of standard claims supported by the OpenID Connect specification // can be found here: http://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - return Json(claims); + return Ok(claims); } } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs index 67702538..240347b3 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs @@ -52,6 +52,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 8fb0d596..7a30b64c 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -142,10 +142,7 @@ namespace OpenIddict.Server.AspNetCore if (notification.Principal == null) { - Logger.LogWarning("The identity token extracted from the 'id_token_hint' " + - "parameter was invalid or malformed and was ignored."); - - return AuthenticateResult.NoResult(); + return AuthenticateResult.Fail("The identity token is not valid."); } // Note: tickets are returned even if they are considered invalid (e.g expired). @@ -182,9 +179,7 @@ namespace OpenIddict.Server.AspNetCore if (notification.Principal == null) { - Logger.LogWarning("The authorization code extracted from the token request was invalid and was ignored."); - - return AuthenticateResult.NoResult(); + return AuthenticateResult.Fail("The authorization code is not valid."); } // Note: tickets are returned even if they are considered invalid (e.g expired). @@ -219,12 +214,49 @@ namespace OpenIddict.Server.AspNetCore if (notification.Principal == null) { - Logger.LogWarning("The refresh token extracted from the token request was invalid and was ignored."); + return AuthenticateResult.Fail("The refresh token is not valid."); + } + // Note: tickets are returned even if they are considered invalid (e.g expired). + + return AuthenticateResult.Success(new AuthenticationTicket( + notification.Principal, + OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)); + } + + case OpenIddictServerEndpointType.Userinfo: + { + if (string.IsNullOrEmpty(transaction.Request.AccessToken)) + { return AuthenticateResult.NoResult(); } - // Note: tickets are returned even if they are considered invalid (e.g expired). + var notification = new DeserializeAccessTokenContext(transaction) + { + Token = transaction.Request.AccessToken + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The access token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating access tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + return AuthenticateResult.Fail("The access token is not valid."); + } + + var date = notification.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + return AuthenticateResult.Fail("The access token is no longer valid."); + } return AuthenticateResult.Success(new AuthenticationTicket( notification.Principal, diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs new file mode 100644 index 00000000..ee05144e --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs @@ -0,0 +1,144 @@ +/* + * 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.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore; +using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Userinfo; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Userinfo + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Userinfo request extraction: + */ + ExtractGetRequest.Descriptor, + ExtractAccessToken.Descriptor, + + /* + * Userinfo request handling: + */ + EnablePassthroughMode.Descriptor, + InferIssuerFromHost.Descriptor, + + /* + * Userinfo response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachIssuer.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] HandleUserinfoRequestContext 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 request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + context.SkipRequest(); + + return default; + } + } + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class InferIssuerFromHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachIssuer.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 ValueTask HandleAsync([NotNull] HandleUserinfoRequestContext 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 request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (!request.Host.HasValue) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer.AbsoluteUri; + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 9e013b7b..ed5ed5bf 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -38,7 +38,8 @@ namespace OpenIddict.Server.AspNetCore .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) - .AddRange(Session.DefaultHandlers); + .AddRange(Session.DefaultHandlers) + .AddRange(Userinfo.DefaultHandlers); /// /// Contains the logic responsible of inferring the endpoint type from the request address. @@ -478,7 +479,7 @@ namespace OpenIddict.Server.AspNetCore } /// - /// Contains the logic responsible of rejecting token requests that specify an invalid grant type. + /// Contains the logic responsible of extracting client credentials from the standard HTTP Authorization header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. /// public class ExtractBasicAuthenticationCredentials : IOpenIddictServerHandler @@ -578,6 +579,58 @@ namespace OpenIddict.Server.AspNetCore } } + /// + /// Contains the logic responsible of extracting an access token from the standard HTTP Authorization header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core. + /// + public class ExtractAccessToken : IOpenIddictServerHandler + where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractBasicAuthenticationCredentials.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 request = context.Transaction.GetHttpRequest(); + if (request == null) + { + throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); + } + + string header = request.Headers[HeaderNames.Authorization]; + if (string.IsNullOrEmpty(header) || !header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + return default; + } + + // Attach the access token to the request message. + context.Request.AccessToken = header.Substring("Bearer ".Length); + + 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. @@ -621,6 +674,8 @@ namespace OpenIddict.Server.AspNetCore throw new InvalidOperationException("The ASP.NET Core HTTP request cannot be resolved."); } + context.Logger.LogInformation("The response was successfully returned as a JSON document: {Response}.", context.Response); + using (var buffer = new MemoryStream()) using (var writer = new JsonTextWriter(new StreamWriter(buffer))) { diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs index 50853fa3..dc2df233 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs @@ -53,6 +53,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 3cafea2c..f7fc1b0a 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs @@ -223,6 +223,47 @@ namespace OpenIddict.Server.Owin return new AuthenticationTicket((ClaimsIdentity) notification.Principal.Identity, new AuthenticationProperties()); } + case OpenIddictServerEndpointType.Userinfo: + { + if (string.IsNullOrEmpty(transaction.Request.AccessToken)) + { + return null; + } + + var notification = new DeserializeAccessTokenContext(transaction) + { + Token = transaction.Request.AccessToken + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The access token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating access tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + _logger.LogWarning("The access token extracted from the userinfo request was invalid and was ignored."); + + return null; + } + + var date = notification.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + _logger.LogError("The access token extracted from the userinfo request was expired."); + + return null; + } + + return new AuthenticationTicket((ClaimsIdentity) notification.Principal.Identity, new AuthenticationProperties()); + } + default: throw new InvalidOperationException("An identity cannot be extracted from this request."); } } diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs new file mode 100644 index 00000000..293e7428 --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs @@ -0,0 +1,145 @@ +/* + * 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.Threading.Tasks; +using JetBrains.Annotations; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlerFilters; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers.Userinfo; +using static OpenIddict.Server.Owin.OpenIddictServerOwinHandlers; +using Owin; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Userinfo + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Userinfo request extraction: + */ + ExtractGetRequest.Descriptor, + ExtractAccessToken.Descriptor, + + /* + * Userinfo request handling: + */ + EnablePassthroughMode.Descriptor, + InferIssuerFromHost.Descriptor, + + /* + * Userinfo response processing: + */ + ProcessJsonResponse.Descriptor); + + /// + /// Contains the logic responsible of enabling the pass-through mode for the received request. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class EnablePassthroughMode : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachIssuer.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] HandleUserinfoRequestContext 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 request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + context.SkipRequest(); + + return default; + } + } + + /// + /// Contains the logic responsible of infering the issuer URL from the HTTP request host. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class InferIssuerFromHost : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachIssuer.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 ValueTask HandleAsync([NotNull] HandleUserinfoRequestContext 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 request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + // If the issuer was not populated by another handler (e.g from the server options), + // try to infer it from the request scheme/host/path base (which requires HTTP/1.1). + if (context.Issuer == null) + { + if (string.IsNullOrEmpty(request.Host.Value)) + { + throw new InvalidOperationException("No host was attached to the HTTP request."); + } + + if (!Uri.TryCreate(request.Scheme + "://" + request.Host + request.PathBase, UriKind.Absolute, out Uri issuer)) + { + throw new InvalidOperationException("The issuer address cannot be inferred from the current request."); + } + + context.Issuer = issuer.AbsoluteUri; + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index 4dfd1390..a03b6e73 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -37,7 +37,8 @@ namespace OpenIddict.Server.Owin .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) - .AddRange(Session.DefaultHandlers); + .AddRange(Session.DefaultHandlers) + .AddRange(Userinfo.DefaultHandlers); /// /// Contains the logic responsible of inferring the endpoint type from the request address. @@ -479,7 +480,7 @@ namespace OpenIddict.Server.Owin } /// - /// Contains the logic responsible of rejecting token requests that specify an invalid grant type. + /// Contains the logic responsible of extracting client credentials from the standard HTTP Authorization header. /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. /// public class ExtractBasicAuthenticationCredentials : IOpenIddictServerHandler @@ -579,6 +580,58 @@ namespace OpenIddict.Server.Owin } } + /// + /// Contains the logic responsible of extracting an access token from the standard HTTP Authorization header. + /// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN. + /// + public class ExtractAccessToken : IOpenIddictServerHandler + where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractBasicAuthenticationCredentials.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 request = context.Transaction.GetOwinRequest(); + if (request == null) + { + throw new InvalidOperationException("The OWIN request cannot be resolved."); + } + + var header = request.Headers["Authorization"]; + if (string.IsNullOrEmpty(header) || !header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + { + return default; + } + + // Attach the access token to the request message. + context.Request.AccessToken = header.Substring("Bearer ".Length); + + 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. @@ -622,6 +675,8 @@ namespace OpenIddict.Server.Owin throw new InvalidOperationException("The OWIN request cannot be resolved."); } + context.Logger.LogInformation("The response was successfully returned as a JSON document: {Response}.", context.Response); + using (var buffer = new MemoryStream()) using (var writer = new JsonTextWriter(new StreamWriter(buffer))) { diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs index f2a5788b..5f9b12ff 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; using JetBrains.Annotations; using Newtonsoft.Json.Linq; using OpenIddict.Abstractions; @@ -42,6 +43,11 @@ namespace OpenIddict.Server : base(transaction) { } + + /// + /// Gets or sets the security principal extracted from the access token, if available. + /// + public ClaimsPrincipal Principal { get; set; } } /// @@ -58,6 +64,11 @@ namespace OpenIddict.Server { } + /// + /// Gets or sets the security principal extracted from the access token. + /// + public ClaimsPrincipal Principal { get; set; } + /// /// Gets the additional claims returned to the client application. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs new file mode 100644 index 00000000..c1e6cf5e --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs @@ -0,0 +1,638 @@ +/* + * 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.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Userinfo + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Userinfo request top-level processing: + */ + ExtractUserinfoRequest.Descriptor, + ValidateUserinfoRequest.Descriptor, + HandleUserinfoRequest.Descriptor, + ApplyUserinfoResponse.Descriptor, + ApplyUserinfoResponse.Descriptor, + ApplyUserinfoResponse.Descriptor, + + /* + * Userinfo request validation: + */ + ValidateAccessTokenParameter.Descriptor, + ValidateAccessToken.Descriptor, + + /* + * Userinfo request handling: + */ + AttachIssuer.Descriptor, + AttachPrincipal.Descriptor, + AttachAudiences.Descriptor, + AttachClaims.Descriptor); + + /// + /// Contains the logic responsible of extracting userinfo requests and invoking the corresponding event handlers. + /// + public class ExtractUserinfoRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractUserinfoRequest([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.Userinfo) + { + return; + } + + var notification = new ExtractUserinfoRequestContext(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 userinfo request was not correctly extracted. To extract userinfo requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The userinfo request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating userinfo requests and invoking the corresponding event handlers. + /// + public class ValidateUserinfoRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateUserinfoRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractUserinfoRequest.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.Userinfo) + { + return; + } + + var notification = new ValidateUserinfoRequestContext(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; + } + + // Store the security principal extracted from the authorization code/refresh token as an environment property. + context.Transaction.Properties[Properties.OriginalPrincipal] = notification.Principal; + + context.Logger.LogInformation("The userinfo request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling userinfo requests and invoking the corresponding event handlers. + /// + public class HandleUserinfoRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleUserinfoRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateUserinfoRequest.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.Userinfo) + { + return; + } + + var notification = new HandleUserinfoRequestContext(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 response = new OpenIddictResponse + { + [Claims.Subject] = notification.Subject, + [Claims.Address] = notification.Address, + [Claims.Birthdate] = notification.BirthDate, + [Claims.Email] = notification.Email, + [Claims.EmailVerified] = notification.EmailVerified, + [Claims.FamilyName] = notification.FamilyName, + [Claims.GivenName] = notification.GivenName, + [Claims.Issuer] = notification.Issuer, + [Claims.PhoneNumber] = notification.PhoneNumber, + [Claims.PhoneNumberVerified] = notification.PhoneNumberVerified, + [Claims.PreferredUsername] = notification.PreferredUsername, + [Claims.Profile] = notification.Profile, + [Claims.Website] = notification.Website + }; + + switch (notification.Audiences.Count) + { + case 0: break; + + case 1: + response[Claims.Audience] = notification.Audiences.ElementAt(0); + break; + + default: + response[Claims.Audience] = new JArray(notification.Audiences); + break; + } + + foreach (var claim in notification.Claims) + { + response.SetParameter(claim.Key, claim.Value); + } + + context.Response = response; + } + } + + /// + /// Contains the logic responsible of processing userinfo responses and invoking the corresponding event handlers. + /// + public class ApplyUserinfoResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyUserinfoResponse([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.Userinfo) + { + return; + } + + var notification = new ApplyUserinfoResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting userinfo requests that don't specify an access token. + /// + public class ValidateAccessTokenParameter : 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] ValidateUserinfoRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Request.AccessToken)) + { + context.Logger.LogError("The userinfo request was rejected because the access token was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'access_token' parameter is missing."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting userinfo requests that specify an invalid access token. + /// + public class ValidateAccessToken : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateAccessToken([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .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] ValidateUserinfoRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = new DeserializeAccessTokenContext(context.Transaction) + { + Token = context.Request.AccessToken + }; + + await _provider.DispatchAsync(notification); + + if (!notification.IsHandled) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The access token was not correctly processed. This may indicate ") + .Append("that the event handler responsible of validating access tokens ") + .Append("was not registered or was explicitly removed from the handlers list.") + .ToString()); + } + + if (notification.Principal == null) + { + context.Logger.LogError("The userinfo request was rejected because the access token was invalid."); + + context.Reject( + error: Errors.InvalidToken, + description: "The specified access token is invalid."); + + return; + } + + var date = notification.Principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Logger.LogError("The userinfo request was rejected because the access token was expired."); + + context.Reject( + error: Errors.InvalidToken, + description: "The specified access token is no longer valid."); + + return; + } + + // Attach the principal extracted from the authorization code to the parent event context. + context.Principal = notification.Principal; + } + } + + /// + /// Contains the logic responsible of attaching the principal + /// extracted from the access token to the event context. + /// + public class AttachPrincipal : 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] HandleUserinfoRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Transaction.Properties.TryGetValue(Properties.OriginalPrincipal, out var principal)) + { + context.Principal ??= (ClaimsPrincipal) principal; + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the issuer URL to the userinfo response. + /// + public class AttachIssuer : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachPrincipal.Descriptor.Order + 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] HandleUserinfoRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Options.Issuer != null) + { + context.Issuer = context.Options.Issuer.AbsoluteUri; + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the audiences to the userinfo response. + /// + public class AttachAudiences : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachIssuer.Descriptor.Order + 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] HandleUserinfoRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: when receiving an access token, its audiences list cannot be used for the "aud" claim + // as the client application is not the intented audience but only an authorized presenter. + // See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse + context.Audiences.UnionWith(context.Principal.GetPresenters()); + + return default; + } + } + + /// + /// Contains the logic responsible of attaching well known claims to the userinfo response. + /// + public class AttachClaims : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(AttachAudiences.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] HandleUserinfoRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Subject = context.Principal.GetClaim(Claims.Subject); + + // The following claims are all optional and should be excluded when + // no corresponding value has been found in the authentication principal: + + if (context.Principal.HasScope(Scopes.Profile)) + { + context.FamilyName = context.Principal.GetClaim(Claims.FamilyName); + context.GivenName = context.Principal.GetClaim(Claims.GivenName); + context.BirthDate = context.Principal.GetClaim(Claims.Birthdate); + } + + if (context.Principal.HasScope(Scopes.Email)) + { + context.Email = context.Principal.GetClaim(Claims.Email); + } + + if (context.Principal.HasScope(Scopes.Phone)) + { + context.PhoneNumber = context.Principal.GetClaim(Claims.PhoneNumber); + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index afea9787..262547c5 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -46,7 +46,8 @@ namespace OpenIddict.Server .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) - .AddRange(Session.DefaultHandlers); + .AddRange(Session.DefaultHandlers) + .AddRange(Userinfo.DefaultHandlers); /// /// Contains the logic responsible of ensuring that the challenge response contains an appropriate error. @@ -82,6 +83,7 @@ namespace OpenIddict.Server { OpenIddictServerEndpointType.Authorization => Errors.AccessDenied, OpenIddictServerEndpointType.Token => Errors.InvalidGrant, + OpenIddictServerEndpointType.Userinfo => Errors.InvalidToken, _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") }; @@ -93,6 +95,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.", _ => throw new InvalidOperationException("An OpenID Connect response cannot be returned from this endpoint.") };