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.
206 lines
10 KiB
206 lines
10 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.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using JetBrains.Annotations;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
using OpenIddict.Abstractions;
|
|
using static OpenIddict.Abstractions.OpenIddictConstants;
|
|
using static OpenIddict.Validation.OpenIddictValidationEvents;
|
|
using static OpenIddict.Validation.OpenIddictValidationHandlers;
|
|
using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpConstants;
|
|
using static OpenIddict.Validation.SystemNetHttp.OpenIddictValidationSystemNetHttpHandlerFilters;
|
|
|
|
namespace OpenIddict.Validation.SystemNetHttp
|
|
{
|
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
|
public static partial class OpenIddictValidationSystemNetHttpHandlers
|
|
{
|
|
public static ImmutableArray<OpenIddictValidationHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
|
|
/*
|
|
* Authentication processing:
|
|
*/
|
|
PopulateTokenValidationParameters.Descriptor);
|
|
|
|
/// <summary>
|
|
/// Contains the logic responsible of populating the token validation
|
|
/// parameters using OAuth 2.0/OpenID Connect discovery.
|
|
/// </summary>
|
|
public class PopulateTokenValidationParameters : IOpenIddictValidationHandler<ProcessAuthenticationContext>
|
|
{
|
|
private readonly IMemoryCache _cache;
|
|
private readonly IHttpClientFactory _factory;
|
|
|
|
public PopulateTokenValidationParameters(
|
|
[NotNull] IMemoryCache cache,
|
|
[NotNull] IHttpClientFactory factory)
|
|
{
|
|
_cache = cache;
|
|
_factory = factory;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the default descriptor definition assigned to this handler.
|
|
/// </summary>
|
|
public static OpenIddictValidationHandlerDescriptor Descriptor { get; }
|
|
= OpenIddictValidationHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
|
|
.AddFilter<RequireHttpMetadataAddress>()
|
|
.UseSingletonHandler<PopulateTokenValidationParameters>()
|
|
.SetOrder(ValidateIdentityModelToken.Descriptor.Order - 500)
|
|
.Build();
|
|
|
|
public async ValueTask HandleAsync([NotNull] ProcessAuthenticationContext context)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
var parameters = await _cache.GetOrCreateAsync(
|
|
key: string.Concat("af84c073-c27c-49fd-a54f-584fd60320d3", "\x1e", context.Issuer?.AbsoluteUri),
|
|
factory: async entry =>
|
|
{
|
|
entry.SetAbsoluteExpiration(TimeSpan.FromMinutes(30));
|
|
entry.SetPriority(CacheItemPriority.NeverRemove);
|
|
|
|
return await GetTokenValidationParametersAsync();
|
|
});
|
|
|
|
context.TokenValidationParameters.ValidIssuer = parameters.ValidIssuer;
|
|
context.TokenValidationParameters.IssuerSigningKeys = parameters.IssuerSigningKeys;
|
|
|
|
async ValueTask<TokenValidationParameters> GetTokenValidationParametersAsync()
|
|
{
|
|
using var client = _factory.CreateClient(Clients.Discovery);
|
|
var response = await SendHttpRequestMessageAsync(client, context.Options.MetadataAddress);
|
|
|
|
// Ensure the JWKS endpoint URL is present and valid.
|
|
if (!response.TryGetParameter(Metadata.JwksUri, out var endpoint) || OpenIddictParameter.IsNullOrEmpty(endpoint))
|
|
{
|
|
throw new InvalidOperationException("A discovery response containing an empty JWKS endpoint URL was returned.");
|
|
}
|
|
|
|
if (!Uri.TryCreate((string) endpoint, UriKind.Absolute, out Uri uri))
|
|
{
|
|
throw new InvalidOperationException("A discovery response containing an invalid JWKS endpoint URL was returned.");
|
|
}
|
|
|
|
return new TokenValidationParameters
|
|
{
|
|
ValidIssuer = (string) response[Metadata.Issuer],
|
|
IssuerSigningKeys = await GetSigningKeysAsync(client, uri).ToListAsync()
|
|
};
|
|
}
|
|
|
|
static async IAsyncEnumerable<SecurityKey> GetSigningKeysAsync(HttpClient client, Uri address)
|
|
{
|
|
var response = await SendHttpRequestMessageAsync(client, address);
|
|
|
|
var keys = response[JsonWebKeySetParameterNames.Keys];
|
|
if (keys == null)
|
|
{
|
|
throw new InvalidOperationException("The OAuth 2.0/OpenID Connect cryptography didn't contain any JSON web key");
|
|
}
|
|
|
|
foreach (var payload in keys.Value.GetParameters())
|
|
{
|
|
var type = (string) payload.Value[JsonWebKeyParameterNames.Kty];
|
|
if (string.IsNullOrEmpty(type))
|
|
{
|
|
throw new InvalidOperationException("A JWKS response containing an invalid key was returned.");
|
|
}
|
|
|
|
var key = type switch
|
|
{
|
|
JsonWebAlgorithmsKeyTypes.RSA => new JsonWebKey
|
|
{
|
|
Kty = JsonWebAlgorithmsKeyTypes.RSA,
|
|
E = (string) payload.Value[JsonWebKeyParameterNames.E],
|
|
N = (string) payload.Value[JsonWebKeyParameterNames.N]
|
|
},
|
|
|
|
JsonWebAlgorithmsKeyTypes.EllipticCurve => new JsonWebKey
|
|
{
|
|
Kty = JsonWebAlgorithmsKeyTypes.EllipticCurve,
|
|
Crv = (string) payload.Value[JsonWebKeyParameterNames.Crv],
|
|
X = (string) payload.Value[JsonWebKeyParameterNames.X],
|
|
Y = (string) payload.Value[JsonWebKeyParameterNames.Y]
|
|
},
|
|
|
|
_ => throw new InvalidOperationException("A JWKS response containing an unsupported key was returned.")
|
|
};
|
|
|
|
key.KeyId = (string) payload.Value[JsonWebKeyParameterNames.Kid];
|
|
key.X5t = (string) payload.Value[JsonWebKeyParameterNames.X5t];
|
|
key.X5tS256 = (string) payload.Value[JsonWebKeyParameterNames.X5tS256];
|
|
|
|
if (payload.Value.TryGetParameter(JsonWebKeyParameterNames.X5c, out var chain))
|
|
{
|
|
foreach (var certificate in chain.GetParameters())
|
|
{
|
|
var value = (string) certificate.Value;
|
|
if (string.IsNullOrEmpty(value))
|
|
{
|
|
throw new InvalidOperationException("A JWKS response containing an invalid key was returned.");
|
|
}
|
|
|
|
key.X5c.Add(value);
|
|
}
|
|
}
|
|
|
|
yield return key;
|
|
}
|
|
}
|
|
|
|
static async ValueTask<OpenIddictResponse> SendHttpRequestMessageAsync(HttpClient client, Uri address)
|
|
{
|
|
using var request = new HttpRequestMessage(HttpMethod.Get, address);
|
|
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
request.Headers.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8"));
|
|
|
|
using var response = await client.SendAsync(request, HttpCompletionOption.ResponseContentRead);
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
|
|
"The OAuth 2.0/OpenID Connect discovery failed because an invalid response was received:" +
|
|
"the identity provider returned returned a {0} response with the following payload: {1} {2}.",
|
|
/* Status: */ response.StatusCode,
|
|
/* Headers: */ response.Headers.ToString(),
|
|
/* Body: */ await response.Content.ReadAsStringAsync()));
|
|
}
|
|
|
|
var type = response.Content?.Headers.ContentType?.MediaType;
|
|
if (!string.Equals(type, "application/json", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture,
|
|
"The OAuth 2.0/OpenID Connect discovery failed because an invalid content type was received:" +
|
|
"the identity provider returned returned a {0} response with the following payload: {1} {2}.",
|
|
/* Status: */ response.StatusCode,
|
|
/* Headers: */ response.Headers.ToString(),
|
|
/* Body: */ await response.Content.ReadAsStringAsync()));
|
|
}
|
|
|
|
// Note: ReadAsStreamAsync() is deliberately not used here, as we can't guarantee that
|
|
// the validation handler will always be used with OAuth 2.0 servers returning UTF-8
|
|
// responses (which is not required by the OAuth 2.0/OpenID Connect discovery specs).
|
|
// Unlike ReadAsStreamAsync(), ReadAsStringAsync() will use the response charset
|
|
// to determine whether the payload is UTF-8-encoded and transcode it if necessary.
|
|
return JsonSerializer.Deserialize<OpenIddictResponse>(await response.Content.ReadAsStringAsync());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|