24 changed files with 1820 additions and 367 deletions
@ -0,0 +1,18 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
namespace OpenIddict.Client.SystemNetHttp; |
|||
|
|||
/// <summary>
|
|||
/// Exposes common constants used by the OpenIddict System.Net.Http integration.
|
|||
/// </summary>
|
|||
public static class OpenIddictClientSystemNetHttpConstants |
|||
{ |
|||
public static class ContentTypes |
|||
{ |
|||
public const string JsonWebToken = "application/jwt"; |
|||
} |
|||
} |
|||
@ -0,0 +1,136 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.Diagnostics; |
|||
using System.Net.Http.Headers; |
|||
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpConstants; |
|||
|
|||
namespace OpenIddict.Client.SystemNetHttp; |
|||
|
|||
public static partial class OpenIddictClientSystemNetHttpHandlers |
|||
{ |
|||
public static class Userinfo |
|||
{ |
|||
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( |
|||
/* |
|||
* Userinfo request processing: |
|||
*/ |
|||
PrepareGetHttpRequest<PrepareUserinfoRequestContext>.Descriptor, |
|||
AttachBearerAccessToken.Descriptor, |
|||
AttachFormParameters<PrepareUserinfoRequestContext>.Descriptor, |
|||
SendHttpRequest<ApplyUserinfoRequestContext>.Descriptor, |
|||
DisposeHttpRequest<ApplyUserinfoRequestContext>.Descriptor, |
|||
|
|||
/* |
|||
* Userinfo response processing: |
|||
*/ |
|||
ExtractUserinfoHttpResponse.Descriptor, |
|||
DisposeHttpResponse<ExtractUserinfoResponseContext>.Descriptor); |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of attaching the access token to the HTTP Authorization header.
|
|||
/// </summary>
|
|||
public class AttachBearerAccessToken : IOpenIddictClientHandler<PrepareUserinfoRequestContext> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareUserinfoRequestContext>() |
|||
.AddFilter<RequireHttpMetadataAddress>() |
|||
.UseSingletonHandler<AttachBearerAccessToken>() |
|||
.SetOrder(AttachFormParameters<PrepareUserinfoRequestContext>.Descriptor.Order - 1000) |
|||
.SetType(OpenIddictClientHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public ValueTask HandleAsync(PrepareUserinfoRequestContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
Debug.Assert(context.Request is not null, SR.GetResourceString(SR.ID4008)); |
|||
|
|||
// This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
|
|||
// this may indicate that the request was incorrectly processed by another client stack.
|
|||
var request = context.Transaction.GetHttpRequestMessage(); |
|||
if (request is null) |
|||
{ |
|||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); |
|||
} |
|||
|
|||
// Attach the authorization header containing the access token to the HTTP request.
|
|||
request.Headers.Authorization = new AuthenticationHeaderValue(Schemes.Bearer, context.Request.AccessToken); |
|||
|
|||
// Remove the access from the request payload to ensure it's not sent twice.
|
|||
context.Request.AccessToken = null; |
|||
|
|||
return default; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of extracting the response from the userinfo response.
|
|||
/// </summary>
|
|||
public class ExtractUserinfoHttpResponse : IOpenIddictClientHandler<ExtractUserinfoResponseContext> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictClientHandlerDescriptor.CreateBuilder<ExtractUserinfoResponseContext>() |
|||
.AddFilter<RequireHttpMetadataAddress>() |
|||
.UseSingletonHandler<ExtractUserinfoHttpResponse>() |
|||
.SetOrder(DisposeHttpResponse<ExtractUserinfoResponseContext>.Descriptor.Order - 50_000) |
|||
.SetType(OpenIddictClientHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public async ValueTask HandleAsync(ExtractUserinfoResponseContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
// This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved,
|
|||
// this may indicate that the request was incorrectly processed by another client stack.
|
|||
var response = context.Transaction.GetHttpResponseMessage(); |
|||
if (response is null) |
|||
{ |
|||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173)); |
|||
} |
|||
|
|||
// The status code is deliberately not validated to ensure even errored responses
|
|||
// (typically in the 4xx range) can be deserialized and handled by the event handlers.
|
|||
|
|||
// Note: userinfo responses can be of two types:
|
|||
// - application/json responses containing a JSON object listing the user claims as-is.
|
|||
// - application/jwt responses containing a signed/encrypted JSON Web Token containing the user claims.
|
|||
//
|
|||
// As such, this handler implements a selection routine to extract the userinfo token as-is
|
|||
// if the media type is application/jwt and fall back to JSON in any other case.
|
|||
|
|||
if (string.Equals(response.Content.Headers.ContentType?.MediaType, |
|||
ContentTypes.JsonWebToken, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
context.Response = new OpenIddictResponse(); |
|||
context.UserinfoToken = await response.Content.ReadAsStringAsync(); |
|||
} |
|||
|
|||
else |
|||
{ |
|||
// Note: ReadFromJsonAsync() automatically validates the content type and the content encoding
|
|||
// and transcode the response stream if a non-UTF-8 response is returned by the remote server.
|
|||
context.Response = await response.Content.ReadFromJsonAsync<OpenIddictResponse>(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,140 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Security.Claims; |
|||
|
|||
namespace OpenIddict.Client; |
|||
|
|||
public static partial class OpenIddictClientEvents |
|||
{ |
|||
/// <summary>
|
|||
/// Represents an event called for each request to the userinfo endpoint
|
|||
/// to give the user code a chance to add parameters to the userinfo request.
|
|||
/// </summary>
|
|||
public class PrepareUserinfoRequestContext : BaseExternalContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="PrepareUserinfoRequestContext"/> class.
|
|||
/// </summary>
|
|||
public PrepareUserinfoRequestContext(OpenIddictClientTransaction transaction) |
|||
: base(transaction) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the request.
|
|||
/// </summary>
|
|||
public OpenIddictRequest Request |
|||
{ |
|||
get => Transaction.Request!; |
|||
set => Transaction.Request = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called for each request to the userinfo endpoint
|
|||
/// to send the userinfo request to the remote authorization server.
|
|||
/// </summary>
|
|||
public class ApplyUserinfoRequestContext : BaseExternalContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="ApplyUserinfoRequestContext"/> class.
|
|||
/// </summary>
|
|||
public ApplyUserinfoRequestContext(OpenIddictClientTransaction transaction) |
|||
: base(transaction) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the request.
|
|||
/// </summary>
|
|||
public OpenIddictRequest Request |
|||
{ |
|||
get => Transaction.Request!; |
|||
set => Transaction.Request = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called for each userinfo response
|
|||
/// to extract the response parameters from the server response.
|
|||
/// </summary>
|
|||
public class ExtractUserinfoResponseContext : BaseExternalContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="ExtractUserinfoResponseContext"/> class.
|
|||
/// </summary>
|
|||
public ExtractUserinfoResponseContext(OpenIddictClientTransaction transaction) |
|||
: base(transaction) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the request.
|
|||
/// </summary>
|
|||
public OpenIddictRequest Request |
|||
{ |
|||
get => Transaction.Request!; |
|||
set => Transaction.Request = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the response, or <c>null</c> if it wasn't extracted yet.
|
|||
/// </summary>
|
|||
public OpenIddictResponse? Response |
|||
{ |
|||
get => Transaction.Response; |
|||
set => Transaction.Response = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the userinfo token, if available.
|
|||
/// </summary>
|
|||
public string? UserinfoToken { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Represents an event called for each userinfo response.
|
|||
/// </summary>
|
|||
public class HandleUserinfoResponseContext : BaseExternalContext |
|||
{ |
|||
/// <summary>
|
|||
/// Creates a new instance of the <see cref="HandleUserinfoResponseContext"/> class.
|
|||
/// </summary>
|
|||
public HandleUserinfoResponseContext(OpenIddictClientTransaction transaction) |
|||
: base(transaction) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the request.
|
|||
/// </summary>
|
|||
public OpenIddictRequest Request |
|||
{ |
|||
get => Transaction.Request!; |
|||
set => Transaction.Request = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the response.
|
|||
/// </summary>
|
|||
public OpenIddictResponse Response |
|||
{ |
|||
get => Transaction.Response!; |
|||
set => Transaction.Response = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the userinfo token, if available.
|
|||
/// </summary>
|
|||
public string? UserinfoToken { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the principal containing the claims resolved from the userinfo response.
|
|||
/// </summary>
|
|||
public ClaimsPrincipal? Principal { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
/* |
|||
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
|||
* See https://github.com/openiddict/openiddict-core for more information concerning
|
|||
* the license and the contributors participating to this project. |
|||
*/ |
|||
|
|||
using System.Collections.Immutable; |
|||
using System.Globalization; |
|||
using System.Security.Claims; |
|||
using System.Text.Json; |
|||
using Microsoft.IdentityModel.JsonWebTokens; |
|||
|
|||
namespace OpenIddict.Client; |
|||
|
|||
public static partial class OpenIddictClientHandlers |
|||
{ |
|||
public static class Userinfo |
|||
{ |
|||
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( |
|||
/* |
|||
* Userinfo response handling: |
|||
*/ |
|||
HandleErrorResponse<HandleUserinfoResponseContext>.Descriptor, |
|||
ValidateWellKnownClaims.Descriptor, |
|||
PopulateClaims.Descriptor); |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of validating the well-known parameters contained in the userinfo response.
|
|||
/// </summary>
|
|||
public class ValidateWellKnownClaims : IOpenIddictClientHandler<HandleUserinfoResponseContext> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleUserinfoResponseContext>() |
|||
.UseSingletonHandler<ValidateWellKnownClaims>() |
|||
.SetOrder(int.MinValue + 100_000) |
|||
.SetType(OpenIddictClientHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public ValueTask HandleAsync(HandleUserinfoResponseContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
// Ignore the response instance if a userinfo token was extracted.
|
|||
if (!string.IsNullOrEmpty(context.UserinfoToken)) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
foreach (var parameter in context.Response.GetParameters()) |
|||
{ |
|||
if (ValidateClaimType(parameter.Key, parameter.Value.Value)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
context.Reject( |
|||
error: Errors.ServerError, |
|||
description: SR.FormatID2107(parameter.Key), |
|||
uri: SR.FormatID8000(SR.ID2107)); |
|||
|
|||
return default; |
|||
} |
|||
|
|||
return default; |
|||
|
|||
static bool ValidateClaimType(string name, object? value) => name switch |
|||
{ |
|||
// The 'sub' parameter MUST be formatted as a string value.
|
|||
Claims.Subject => value is string or JsonElement { ValueKind: JsonValueKind.String }, |
|||
|
|||
// Parameters that are not in the well-known list can be of any type.
|
|||
_ => true |
|||
}; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Contains the logic responsible of extracting the claims from the introspection response.
|
|||
/// </summary>
|
|||
public class PopulateClaims : IOpenIddictClientHandler<HandleUserinfoResponseContext> |
|||
{ |
|||
/// <summary>
|
|||
/// Gets the default descriptor definition assigned to this handler.
|
|||
/// </summary>
|
|||
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
|||
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleUserinfoResponseContext>() |
|||
.UseSingletonHandler<PopulateClaims>() |
|||
.SetOrder(ValidateWellKnownClaims.Descriptor.Order + 1_000) |
|||
.SetType(OpenIddictClientHandlerType.BuiltIn) |
|||
.Build(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public ValueTask HandleAsync(HandleUserinfoResponseContext context) |
|||
{ |
|||
if (context is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(context)); |
|||
} |
|||
|
|||
// Ignore the response instance if a userinfo token was extracted.
|
|||
if (!string.IsNullOrEmpty(context.UserinfoToken)) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
// Create a new claims-based identity using the same authentication type
|
|||
// and the name/role claims as the one used by IdentityModel for JWT tokens.
|
|||
var identity = new ClaimsIdentity( |
|||
context.Registration.TokenValidationParameters.AuthenticationType, |
|||
context.Registration.TokenValidationParameters.NameClaimType, |
|||
context.Registration.TokenValidationParameters.RoleClaimType); |
|||
|
|||
// Resolve the issuer that will be attached to the claims created by this handler.
|
|||
// Note: at this stage, the optional issuer extracted from the response is assumed
|
|||
// to be valid, as it is guarded against unknown values by the ValidateIssuer handler.
|
|||
var issuer = (string?) context.Response[Claims.Issuer] ?? context.Issuer?.AbsoluteUri ?? ClaimsIdentity.DefaultIssuer; |
|||
|
|||
foreach (var parameter in context.Response.GetParameters()) |
|||
{ |
|||
// Always exclude null keys and values, as they can't be represented as valid claims.
|
|||
if (string.IsNullOrEmpty(parameter.Key) || OpenIddictParameter.IsNullOrEmpty(parameter.Value)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Exclude OpenIddict-specific private claims, that MUST NOT be set based on data returned
|
|||
// by the remote authorization server (that may or may not be an OpenIddict server).
|
|||
if (parameter.Key.StartsWith(Claims.Prefixes.Private, StringComparison.OrdinalIgnoreCase)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// Ignore all protocol claims that shouldn't be mapped to CLR claims.
|
|||
if (parameter.Key is Claims.Active or Claims.Issuer or Claims.NotBefore or Claims.TokenType) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
switch (parameter.Value.Value) |
|||
{ |
|||
// Claims represented as arrays are split and mapped to multiple CLR claims.
|
|||
case JsonElement { ValueKind: JsonValueKind.Array } value: |
|||
foreach (var element in value.EnumerateArray()) |
|||
{ |
|||
var item = element.GetString(); |
|||
if (string.IsNullOrEmpty(item)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
identity.AddClaim(new Claim(parameter.Key, item, |
|||
GetClaimValueType(value.ValueKind), issuer, issuer, identity)); |
|||
} |
|||
break; |
|||
|
|||
case JsonElement value: |
|||
identity.AddClaim(new Claim(parameter.Key, value.ToString()!, |
|||
GetClaimValueType(value.ValueKind), issuer, issuer, identity)); |
|||
break; |
|||
|
|||
// Note: in the typical case, the introspection parameters should be deserialized from
|
|||
// a JSON response and thus represented as System.Text.Json.JsonElement instances.
|
|||
// However, to support responses resolved from custom locations and parameters manually added
|
|||
// by the application using the events model, the CLR primitive types are also supported.
|
|||
|
|||
case bool value: |
|||
identity.AddClaim(new Claim(parameter.Key, value.ToString(), |
|||
ClaimValueTypes.Boolean, issuer, issuer, identity)); |
|||
break; |
|||
|
|||
case long value: |
|||
identity.AddClaim(new Claim(parameter.Key, value.ToString(CultureInfo.InvariantCulture), |
|||
ClaimValueTypes.Integer64, issuer, issuer, identity)); |
|||
break; |
|||
|
|||
case string value: |
|||
identity.AddClaim(new Claim(parameter.Key, value, ClaimValueTypes.String, issuer, issuer, identity)); |
|||
break; |
|||
|
|||
// Claims represented as arrays are split and mapped to multiple CLR claims.
|
|||
case string[] value: |
|||
for (var index = 0; index < value.Length; index++) |
|||
{ |
|||
identity.AddClaim(new Claim(parameter.Key, value[index], ClaimValueTypes.String, issuer, issuer, identity)); |
|||
} |
|||
break; |
|||
} |
|||
} |
|||
|
|||
context.Principal = new ClaimsPrincipal(identity); |
|||
|
|||
return default; |
|||
|
|||
static string GetClaimValueType(JsonValueKind kind) => kind switch |
|||
{ |
|||
JsonValueKind.True or JsonValueKind.False => ClaimValueTypes.Boolean, |
|||
|
|||
JsonValueKind.String => ClaimValueTypes.String, |
|||
JsonValueKind.Number => ClaimValueTypes.Integer64, |
|||
|
|||
JsonValueKind.Array => JsonClaimValueTypes.JsonArray, |
|||
JsonValueKind.Object or _ => JsonClaimValueTypes.Json |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
File diff suppressed because it is too large
Loading…
Reference in new issue