Versatile OpenID Connect stack for ASP.NET Core and Microsoft.Owin (compatible with ASP.NET 4.6.1)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

638 lines
28 KiB

/*
* 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;
}
}
}
}
}