Browse Source

Bring back userinfo support

pull/781/head
Kévin Chalet 7 years ago
committed by GitHub
parent
commit
2e45becc5a
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      samples/Mvc.Client/Startup.cs
  2. 17
      samples/Mvc.Server/Controllers/UserinfoController.cs
  3. 1
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreExtensions.cs
  4. 50
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  5. 144
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Userinfo.cs
  6. 59
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs
  7. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
  8. 41
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  9. 145
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Userinfo.cs
  10. 59
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs
  11. 11
      src/OpenIddict.Server/OpenIddictServerEvents.Userinfo.cs
  12. 638
      src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs
  13. 5
      src/OpenIddict.Server/OpenIddictServerHandlers.cs

2
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.

17
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<ApplicationUser> _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<IActionResult> Userinfo()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return Challenge();
return Challenge(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
var claims = new JObject();
var claims = new Dictionary<string, object>(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);
}
}
}

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

@ -52,6 +52,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireRequestCachingEnabled>();
builder.Services.TryAddSingleton<RequireStatusCodePagesIntegrationEnabled>();
builder.Services.TryAddSingleton<RequireTokenEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireUserinfoEndpointPassthroughEnabled>();
// 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.

50
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,

144
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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Userinfo request extraction:
*/
ExtractGetRequest<ExtractUserinfoRequestContext>.Descriptor,
ExtractAccessToken<ExtractUserinfoRequestContext>.Descriptor,
/*
* Userinfo request handling:
*/
EnablePassthroughMode.Descriptor,
InferIssuerFromHost.Descriptor,
/*
* Userinfo response processing:
*/
ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor);
/// <summary>
/// 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.
/// </summary>
public class EnablePassthroughMode : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.AddFilter<RequireUserinfoEndpointPassthroughEnabled>()
.UseSingletonHandler<EnablePassthroughMode>()
.SetOrder(AttachIssuer.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// 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.
/// </summary>
public class InferIssuerFromHost : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<InferIssuerFromHost>()
.SetOrder(AttachIssuer.Descriptor.Order + 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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;
}
}
}
}
}

59
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);
/// <summary>
/// Contains the logic responsible of inferring the endpoint type from the request address.
@ -478,7 +479,7 @@ namespace OpenIddict.Server.AspNetCore
}
/// <summary>
/// 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.
/// </summary>
public class ExtractBasicAuthenticationCredentials<TContext> : IOpenIddictServerHandler<TContext>
@ -578,6 +579,58 @@ namespace OpenIddict.Server.AspNetCore
}
}
/// <summary>
/// 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.
/// </summary>
public class ExtractAccessToken<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpRequest>()
.UseSingletonHandler<ExtractAccessToken<TContext>>()
.SetOrder(ExtractBasicAuthenticationCredentials<TContext>.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to ASP.NET Core requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var 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;
}
}
/// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by ASP.NET Core.
@ -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)))
{

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

@ -53,6 +53,7 @@ namespace Microsoft.Extensions.DependencyInjection
builder.Services.TryAddSingleton<RequireOwinRequest>();
builder.Services.TryAddSingleton<RequireRequestCachingEnabled>();
builder.Services.TryAddSingleton<RequireTokenEndpointPassthroughEnabled>();
builder.Services.TryAddSingleton<RequireUserinfoEndpointPassthroughEnabled>();
// 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.

41
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.");
}
}

145
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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Userinfo request extraction:
*/
ExtractGetRequest<ExtractUserinfoRequestContext>.Descriptor,
ExtractAccessToken<ExtractUserinfoRequestContext>.Descriptor,
/*
* Userinfo request handling:
*/
EnablePassthroughMode.Descriptor,
InferIssuerFromHost.Descriptor,
/*
* Userinfo response processing:
*/
ProcessJsonResponse<ApplyUserinfoResponseContext>.Descriptor);
/// <summary>
/// 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.
/// </summary>
public class EnablePassthroughMode : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.AddFilter<RequireUserinfoEndpointPassthroughEnabled>()
.UseSingletonHandler<EnablePassthroughMode>()
.SetOrder(AttachIssuer.Descriptor.Order - 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// 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.
/// </summary>
public class InferIssuerFromHost : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<InferIssuerFromHost>()
.SetOrder(AttachIssuer.Descriptor.Order + 500)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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;
}
}
}
}
}

59
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);
/// <summary>
/// Contains the logic responsible of inferring the endpoint type from the request address.
@ -479,7 +480,7 @@ namespace OpenIddict.Server.Owin
}
/// <summary>
/// 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.
/// </summary>
public class ExtractBasicAuthenticationCredentials<TContext> : IOpenIddictServerHandler<TContext>
@ -579,6 +580,58 @@ namespace OpenIddict.Server.Owin
}
}
/// <summary>
/// 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.
/// </summary>
public class ExtractAccessToken<TContext> : IOpenIddictServerHandler<TContext>
where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireOwinRequest>()
.UseSingletonHandler<ExtractAccessToken<TContext>>()
.SetOrder(ExtractBasicAuthenticationCredentials<TContext>.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] TContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to OWIN requests. If The OWIN request cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var 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;
}
}
/// <summary>
/// Contains the logic responsible of processing OpenID Connect responses that must be returned as JSON.
/// Note: this handler is not used when the OpenID Connect request is not initially handled by OWIN.
@ -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)))
{

11
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)
{
}
/// <summary>
/// Gets or sets the security principal extracted from the access token, if available.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
}
/// <summary>
@ -58,6 +64,11 @@ namespace OpenIddict.Server
{
}
/// <summary>
/// Gets or sets the security principal extracted from the access token.
/// </summary>
public ClaimsPrincipal Principal { get; set; }
/// <summary>
/// Gets the additional claims returned to the client application.
/// </summary>

638
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<OpenIddictServerHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Userinfo request top-level processing:
*/
ExtractUserinfoRequest.Descriptor,
ValidateUserinfoRequest.Descriptor,
HandleUserinfoRequest.Descriptor,
ApplyUserinfoResponse<ProcessChallengeResponseContext>.Descriptor,
ApplyUserinfoResponse<ProcessErrorResponseContext>.Descriptor,
ApplyUserinfoResponse<ProcessRequestContext>.Descriptor,
/*
* Userinfo request validation:
*/
ValidateAccessTokenParameter.Descriptor,
ValidateAccessToken.Descriptor,
/*
* Userinfo request handling:
*/
AttachIssuer.Descriptor,
AttachPrincipal.Descriptor,
AttachAudiences.Descriptor,
AttachClaims.Descriptor);
/// <summary>
/// Contains the logic responsible of extracting userinfo requests and invoking the corresponding event handlers.
/// </summary>
public class ExtractUserinfoRequest : IOpenIddictServerHandler<ProcessRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ExtractUserinfoRequest([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseScopedHandler<ExtractUserinfoRequest>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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<ExtractUserinfoRequestContext>' ")
.AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.")
.ToString());
}
context.Logger.LogInformation("The userinfo request was successfully extracted: {Request}.", notification.Request);
}
}
/// <summary>
/// Contains the logic responsible of validating userinfo requests and invoking the corresponding event handlers.
/// </summary>
public class ValidateUserinfoRequest : IOpenIddictServerHandler<ProcessRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateUserinfoRequest([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseScopedHandler<ValidateUserinfoRequest>()
.SetOrder(ExtractUserinfoRequest.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] 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.");
}
}
/// <summary>
/// Contains the logic responsible of handling userinfo requests and invoking the corresponding event handlers.
/// </summary>
public class HandleUserinfoRequest : IOpenIddictServerHandler<ProcessRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public HandleUserinfoRequest([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.UseScopedHandler<HandleUserinfoRequest>()
.SetOrder(ValidateUserinfoRequest.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public async ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of processing userinfo responses and invoking the corresponding event handlers.
/// </summary>
public class ApplyUserinfoResponse<TContext> : IOpenIddictServerHandler<TContext> where TContext : BaseRequestContext
{
private readonly IOpenIddictServerProvider _provider;
public ApplyUserinfoResponse([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<TContext>()
.UseScopedHandler<ApplyUserinfoResponse<TContext>>()
.SetOrder(int.MaxValue - 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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;
}
}
}
/// <summary>
/// Contains the logic responsible of rejecting userinfo requests that don't specify an access token.
/// </summary>
public class ValidateAccessTokenParameter : IOpenIddictServerHandler<ValidateUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateUserinfoRequestContext>()
.UseSingletonHandler<ValidateAccessTokenParameter>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of rejecting userinfo requests that specify an invalid access token.
/// </summary>
public class ValidateAccessToken : IOpenIddictServerHandler<ValidateUserinfoRequestContext>
{
private readonly IOpenIddictServerProvider _provider;
public ValidateAccessToken([NotNull] IOpenIddictServerProvider provider)
=> _provider = provider;
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateUserinfoRequestContext>()
.UseScopedHandler<ValidateAccessToken>()
.SetOrder(100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public 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;
}
}
/// <summary>
/// Contains the logic responsible of attaching the principal
/// extracted from the access token to the event context.
/// </summary>
public class AttachPrincipal : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.UseSingletonHandler<AttachPrincipal>()
.SetOrder(int.MinValue + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of attaching the issuer URL to the userinfo response.
/// </summary>
public class AttachIssuer : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.UseSingletonHandler<AttachIssuer>()
.SetOrder(AttachPrincipal.Descriptor.Order + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] HandleUserinfoRequestContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Options.Issuer != null)
{
context.Issuer = context.Options.Issuer.AbsoluteUri;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible of attaching the audiences to the userinfo response.
/// </summary>
public class AttachAudiences : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.UseSingletonHandler<AttachAudiences>()
.SetOrder(AttachIssuer.Descriptor.Order + 100_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
/// <summary>
/// Contains the logic responsible of attaching well known claims to the userinfo response.
/// </summary>
public class AttachClaims : IOpenIddictServerHandler<HandleUserinfoRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictServerHandlerDescriptor Descriptor { get; }
= OpenIddictServerHandlerDescriptor.CreateBuilder<HandleUserinfoRequestContext>()
.UseSingletonHandler<AttachClaims>()
.SetOrder(AttachAudiences.Descriptor.Order + 1_000)
.Build();
/// <summary>
/// Processes the event.
/// </summary>
/// <param name="context">The context associated with the event to process.</param>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous operation.
/// </returns>
public ValueTask HandleAsync([NotNull] 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;
}
}
}
}
}

5
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);
/// <summary>
/// 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.")
};

Loading…
Cancel
Save