Browse Source

Add a built-in authentication scheme forwarding feature to the OpenIddict client OWIN and ASP.NET Core hosts

pull/1841/head
Kévin Chalet 3 years ago
parent
commit
7d1c704848
  1. 16
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  2. 4
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs
  3. 2
      sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml
  4. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs
  5. 7
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  6. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml
  7. 5
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs
  8. 12
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  9. 36
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs
  10. 72
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs
  11. 4
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs
  12. 116
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreForwarder.cs
  13. 15
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs
  14. 39
      src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs
  15. 54
      src/OpenIddict.Client.Owin/OpenIddictClientOwinConfiguration.cs
  16. 109
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  17. 110
      src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs
  18. 15
      src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs
  19. 2
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
  20. 5
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  21. 10
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  22. 2
      src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs
  23. 10
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs
  24. 2
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs

16
gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs

@ -198,6 +198,21 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
return Set(registration => registration.ProviderName = name);
}
/// <summary>
/// Sets the provider display name.
/// </summary>
/// <param name=""name"">The provider display name.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
public {{ provider.name }} SetProviderDisplayName(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(name));
}
return Set(registration => registration.ProviderDisplayName = name);
}
/// <summary>
/// Sets the registration identifier.
/// </summary>
@ -961,6 +976,7 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration
{{~ end ~}}
registration.ProviderName ??= Providers.{{ provider.name }};
registration.ProviderDisplayName ??= ""{{ provider.display_name }}"";
registration.Issuer ??= settings.Environment switch
{

4
sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs

@ -240,7 +240,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
.ToDictionary(pair => pair.Key, pair => pair.Value));
context.Authentication.SignIn(properties, identity);
return Redirect(properties.RedirectUri);
return Redirect(properties.RedirectUri ?? "/");
}
// Note: this controller uses the same callback action for all providers
@ -258,7 +258,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
// to the authorization server. Applications that prefer delaying the removal of the local cookie can
// remove the corresponding code from the logout action and remove the authentication cookie in this action.
return Redirect(result.Properties.RedirectUri);
return Redirect(result.Properties.RedirectUri ?? "/");
}
}
}

2
sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml

@ -66,7 +66,7 @@
</button>
<button class="btn btn-lg btn-success" type="submit" name="provider" value="Local+GitHub">
Sign in using the local OIDC server (using GitHub delegation)
Sign in using the local OIDC server (preferred service: GitHub)
</button>
<button class="btn btn-lg btn-success" type="submit" name="provider" value="GitHub">

2
sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthenticationController.cs

@ -124,7 +124,7 @@ namespace OpenIddict.Sandbox.AspNet.Server.Controllers
.ToDictionary(pair => pair.Key, pair => pair.Value));
context.Authentication.SignIn(properties, identity);
return Redirect(properties.RedirectUri);
return Redirect(properties.RedirectUri ?? "/");
}
}
}

7
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs

@ -199,7 +199,10 @@ public class AuthenticationController : Controller
roleType: ClaimTypes.Role);
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Items);
var properties = new AuthenticationProperties(result.Properties.Items)
{
RedirectUri = result.Properties.RedirectUri ?? "/"
};
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.
@ -234,6 +237,6 @@ public class AuthenticationController : Controller
// to the authorization server. Applications that prefer delaying the removal of the local cookie can
// remove the corresponding code from the logout action and remove the authentication cookie in this action.
return Redirect(result!.Properties!.RedirectUri);
return Redirect(result!.Properties!.RedirectUri ?? "/");
}
}

2
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml

@ -51,7 +51,7 @@
</button>
<button class="btn btn-lg btn-success" type="submit" name="provider" value="Local+GitHub">
Sign in using the local OIDC server (using GitHub delegation)
Sign in using the local OIDC server (preferred service: GitHub)
</button>
<button class="btn btn-lg btn-success" type="submit" name="provider" value="GitHub">

5
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Controllers/AuthenticationController.cs

@ -89,7 +89,10 @@ public class AuthenticationController : Controller
roleType: ClaimTypes.Role);
// Build the authentication properties based on the properties that were added when the challenge was triggered.
var properties = new AuthenticationProperties(result.Properties.Items);
var properties = new AuthenticationProperties(result.Properties.Items)
{
RedirectUri = result.Properties.RedirectUri ?? "/"
};
// If needed, the tokens returned by the authorization server can be stored in the authentication cookie.
// To make cookies less heavy, tokens that are not used are filtered out before creating the cookie.

12
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1552,6 +1552,18 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0413" xml:space="preserve">
<value>The specified string is not a valid hexadecimal string.</value>
</data>
<data name="ID0414" xml:space="preserve">
<value>The '{0}' authentication scheme already exists and cannot be registered as a forwarded authentication scheme by the OpenIddict client. Consider removing the conflicting authentication handler or use a different provider name. Alternatively, automatic authentication scheme forwarding can be disabled using 'services.AddOpenIddict().AddClient().UseAspNetCore().DisableAutomaticAuthenticationSchemeForwarding()'.</value>
</data>
<data name="ID0415" xml:space="preserve">
<value>Multiple client registrations sharing the same provider name exist, which prevents registering an automatic forwarded authentication scheme with the name '{0}'. Consider using a unique provider name per client registration or disable automatic authentication scheme forwarding using 'services.AddOpenIddict().AddClient().UseAspNetCore().DisableAutomaticAuthenticationSchemeForwarding()'.</value>
</data>
<data name="ID0416" xml:space="preserve">
<value>Multiple client registrations sharing the same provider name exist, which prevents registering an automatic forwarded authentication type with the name '{0}'. Consider using a unique provider name per client registration or disable automatic authentication type forwarding using 'services.AddOpenIddict().AddClient().UseOwin().DisableAutomaticAuthenticationTypeForwarding()'.</value>
</data>
<data name="ID0417" xml:space="preserve">
<value>The authentication properties must not contain an '.issuer', '.provider_name' or '.registration_id' property when using a forwarded authentication scheme/type.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

36
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreBuilder.cs

@ -6,6 +6,7 @@
using System.ComponentModel;
using Microsoft.AspNetCore;
using OpenIddict.Client;
using OpenIddict.Client.AspNetCore;
namespace Microsoft.Extensions.DependencyInjection;
@ -47,6 +48,41 @@ public sealed class OpenIddictClientAspNetCoreBuilder
return this;
}
/// <summary>
/// Disables automatic authentication scheme forwarding. When automatic forwarding
/// is disabled, static client registrations are not mapped as individual
/// authentication schemes and calls to <see cref="IAuthenticationService"/> such as
/// <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>
/// cannot directly use the provider name associated to a client registration as the authentication
/// scheme and must set the provider name (or the issuer) as an authentication property instead.
/// </summary>
/// <returns>The <see cref="OpenIddictClientAspNetCoreBuilder"/> instance.</returns>
public OpenIddictClientAspNetCoreBuilder DisableAutomaticAuthenticationSchemeForwarding()
=> Configure(options => options.DisableAutomaticAuthenticationSchemeForwarding = true);
/// <summary>
/// Adds the specified authentication scheme to the list of forwarded authentication
/// schemes that are managed by the OpenIddict ASP.NET Core client host.
/// </summary>
/// <remarks>
/// Note: the <paramref name="provider"/> parameter MUST match
/// an existing <see cref="OpenIddictClientRegistration.ProviderName"/>.
/// </remarks>
/// <param name="provider">The provider name, also used as the authentication scheme.</param>
/// <param name="caption">The caption that will be used as the public/user-visible display name, if applicable.</param>
/// <returns>The <see cref="OpenIddictClientAspNetCoreBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientAspNetCoreBuilder AddForwardedAuthenticationScheme(string provider, string? caption)
{
if (string.IsNullOrEmpty(provider))
{
throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider));
}
return Configure(options => options.ForwardedAuthenticationSchemes.Add(
new AuthenticationScheme(provider, caption, typeof(OpenIddictClientAspNetCoreForwarder))));
}
/// <summary>
/// Disables the transport security requirement (HTTPS).
/// </summary>

72
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConfiguration.cs

@ -5,6 +5,7 @@
*/
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace OpenIddict.Client.AspNetCore;
@ -15,8 +16,22 @@ namespace OpenIddict.Client.AspNetCore;
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions<AuthenticationOptions>,
IConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<AuthenticationOptions>
IPostConfigureOptions<AuthenticationOptions>,
IPostConfigureOptions<OpenIddictClientAspNetCoreOptions>
{
private readonly IServiceProvider _provider;
/// <inheritdoc/>
[Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)]
public OpenIddictClientAspNetCoreConfiguration() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientAspNetCoreConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
public OpenIddictClientAspNetCoreConfiguration(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/>
public void Configure(AuthenticationOptions options)
{
@ -34,6 +49,58 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions<
options.AddScheme<OpenIddictClientAspNetCoreHandler>(
OpenIddictClientAspNetCoreDefaults.AuthenticationScheme, displayName: null);
// Resolve the forwarded authentication schemes managed by the OpenIddict ASP.NET Core
// client host and add an entry for each scheme in the ASP.NET Core authentication options.
foreach (var scheme in _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientAspNetCoreOptions>>()
.CurrentValue.ForwardedAuthenticationSchemes)
{
if (options.SchemeMap.TryGetValue(scheme.Name, out builder) &&
builder.HandlerType != typeof(OpenIddictClientAspNetCoreForwarder))
{
throw new InvalidOperationException(SR.FormatID0414(scheme.Name));
}
options.AddScheme<OpenIddictClientAspNetCoreForwarder>(scheme.Name, scheme.DisplayName);
}
}
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientAspNetCoreOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
if (!options.DisableAutomaticAuthenticationSchemeForwarding)
{
foreach (var (provider, registrations) in _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>()
.CurrentValue.Registrations
.Where(registration => !string.IsNullOrEmpty(registration.ProviderName))
.GroupBy(registration => registration.ProviderName)
.Select(group => (ProviderName: group.Key, Registrations: group.ToList())))
{
// If an explicit mapping was already added, don't overwrite it.
if (options.ForwardedAuthenticationSchemes.Exists(scheme =>
string.Equals(scheme.Name, provider, StringComparison.Ordinal)))
{
continue;
}
// Ensure multiple client registrations don't share the same provider
// name when automatic authentication scheme forwarding is enabled.
if (registrations is not [OpenIddictClientRegistration registration])
{
throw new InvalidOperationException(SR.FormatID0415(provider));
}
options.ForwardedAuthenticationSchemes.Add(new AuthenticationScheme(
name : registration.ProviderName!,
displayName: registration.ProviderDisplayName,
handlerType: typeof(OpenIddictClientAspNetCoreForwarder)));
}
}
}
/// <inheritdoc/>
@ -91,7 +158,8 @@ public sealed class OpenIddictClientAspNetCoreConfiguration : IConfigureOptions<
return true;
}
return builder.HandlerType != typeof(OpenIddictClientAspNetCoreHandler);
return builder.HandlerType != typeof(OpenIddictClientAspNetCoreHandler) &&
builder.HandlerType != typeof(OpenIddictClientAspNetCoreForwarder);
}
}
}

4
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreExtensions.cs

@ -31,6 +31,7 @@ public static class OpenIddictClientAspNetCoreExtensions
builder.Services.AddAuthentication();
builder.Services.TryAddScoped<OpenIddictClientAspNetCoreForwarder>();
builder.Services.TryAddScoped<OpenIddictClientAspNetCoreHandler>();
// Register the built-in event handlers used by the OpenIddict ASP.NET Core client components.
@ -52,7 +53,8 @@ public static class OpenIddictClientAspNetCoreExtensions
ServiceDescriptor.Singleton<IConfigureOptions<AuthenticationOptions>, OpenIddictClientAspNetCoreConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<AuthenticationOptions>, OpenIddictClientAspNetCoreConfiguration>(),
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientAspNetCoreConfiguration>()
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientAspNetCoreConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientAspNetCoreOptions>, OpenIddictClientAspNetCoreConfiguration>()
});
return new OpenIddictClientAspNetCoreBuilder(builder.Services);

116
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreForwarder.cs

@ -0,0 +1,116 @@
/*
* 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.ComponentModel;
using Properties = OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants.Properties;
namespace OpenIddict.Client.AspNetCore;
/// <summary>
/// Provides the logic necessary to forward authentication operations
/// using the specified authentication scheme as the provider name.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientAspNetCoreForwarder : IAuthenticationHandler, IAuthenticationSignOutHandler
{
private HttpContext _context = default!;
private AuthenticationScheme _scheme = default!;
/// <inheritdoc/>
public async Task<AuthenticateResult> AuthenticateAsync()
// Resolve the authentication result returned by the OpenIddict ASP.NET Core client host:
// if the returned identity was created for the specified provider, return the result.
//
// Note: exceptions MUST NOT be caught to ensure they are properly surfaced to the caller
// (e.g if AuthenticateAsync("[provider name]") is called from an unsupported endpoint).
=> await _context.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme) switch
{
{ Succeeded: true } result when
result.Principal.FindFirst(Claims.Private.ProviderName)?.Value is string provider &&
string.Equals(provider, _scheme.Name, StringComparison.Ordinal)
=> AuthenticateResult.Success(new AuthenticationTicket(result.Principal, result.Properties, _scheme.Name)),
AuthenticateResult result => result,
null or _ => AuthenticateResult.NoResult()
};
/// <inheritdoc/>
public async Task ChallengeAsync(AuthenticationProperties? properties)
{
// Ensure no client registration information was attached to the authentication properties.
if (properties is not null && (properties.Items.ContainsKey(Properties.Issuer) ||
properties.Items.ContainsKey(Properties.ProviderName) ||
properties.Items.ContainsKey(Properties.RegistrationId)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0417));
}
// Note: exceptions MUST NOT be caught to ensure they are properly surfaced to the caller.
await _context.ChallengeAsync(
scheme: OpenIddictClientAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
items: new Dictionary<string, string?>(properties?.Items ?? ImmutableDictionary.Create<string, string?>())
{
[Properties.ProviderName] = _scheme.Name
},
parameters: properties?.Parameters));
}
/// <inheritdoc/>
public async Task ForbidAsync(AuthenticationProperties? properties)
{
// Ensure no client registration information was attached to the authentication properties.
if (properties is not null && (properties.Items.ContainsKey(Properties.Issuer) ||
properties.Items.ContainsKey(Properties.ProviderName) ||
properties.Items.ContainsKey(Properties.RegistrationId)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0417));
}
// Note: exceptions MUST NOT be caught to ensure they are properly surfaced to the caller.
await _context.ForbidAsync(
scheme: OpenIddictClientAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
items: new Dictionary<string, string?>(properties?.Items ?? ImmutableDictionary.Create<string, string?>())
{
[Properties.ProviderName] = _scheme.Name
},
parameters: properties?.Parameters));
}
/// <inheritdoc/>
public async Task SignOutAsync(AuthenticationProperties? properties)
{
// Ensure no client registration information was attached to the authentication properties.
if (properties is not null && (properties.Items.ContainsKey(Properties.Issuer) ||
properties.Items.ContainsKey(Properties.ProviderName) ||
properties.Items.ContainsKey(Properties.RegistrationId)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0417));
}
// Note: exceptions MUST NOT be caught to ensure they are properly surfaced to the caller.
await _context.SignOutAsync(
scheme: OpenIddictClientAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(
items: new Dictionary<string, string?>(properties?.Items ?? ImmutableDictionary.Create<string, string?>())
{
[Properties.ProviderName] = _scheme.Name
},
parameters: properties?.Parameters));
}
/// <inheritdoc/>
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_scheme = scheme ?? throw new ArgumentNullException(nameof(scheme));
return Task.CompletedTask;
}
}

15
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreOptions.cs

@ -13,6 +13,21 @@ namespace OpenIddict.Client.AspNetCore;
/// </summary>
public sealed class OpenIddictClientAspNetCoreOptions : AuthenticationSchemeOptions
{
/// <summary>
/// Gets or sets a boolean indicating whether the static client registrations with a non-null
/// provider name attached are automatically added to <see cref="ForwardedAuthenticationSchemes"/>.
/// When automatic forwarding is disabled, calls to <see cref="IAuthenticationService"/> such as
/// <see cref="IAuthenticationService.ChallengeAsync(HttpContext, string, AuthenticationProperties)"/>
/// cannot directly use the provider name associated to a client registration as the authentication
/// scheme and must set the provider name (or the issuer) as an authentication property instead.
/// </summary>
public bool DisableAutomaticAuthenticationSchemeForwarding { get; set; }
/// <summary>
/// Gets the forwarded authentication schemes that are managed by the OpenIddict ASP.NET Core client host.
/// </summary>
public List<AuthenticationScheme> ForwardedAuthenticationSchemes { get; } = new();
/// <summary>
/// Gets or sets a boolean indicating whether incoming requests arriving on insecure endpoints should be
/// rejected and whether challenge and sign-out operations can be triggered from non-HTTPS endpoints.

39
src/OpenIddict.Client.Owin/OpenIddictClientOwinBuilder.cs

@ -5,6 +5,7 @@
*/
using System.ComponentModel;
using OpenIddict.Client;
using OpenIddict.Client.Owin;
using Owin;
@ -47,6 +48,44 @@ public sealed class OpenIddictClientOwinBuilder
return this;
}
/// <summary>
/// Disables automatic authentication type forwarding. When automatic forwarding
/// is disabled, static client registrations are not mapped as individual
/// authentication schemes and calls to <see cref="IAuthenticationManager"/> such as
/// <see cref="IAuthenticationManager.Challenge(AuthenticationProperties, string[])"/>
/// cannot directly use the provider name associated to a client registration as the authentication
/// type and must set the provider name (or the issuer) as an authentication property instead.
/// </summary>
/// <returns>The <see cref="OpenIddictClientOwinBuilder"/> instance.</returns>
public OpenIddictClientOwinBuilder DisableAutomaticAuthenticationTypeForwarding()
=> Configure(options => options.DisableAutomaticAuthenticationTypeForwarding = true);
/// <summary>
/// Adds the specified authentication type to the list of forwarded authentication
/// types that are managed by the OpenIddict ASP.NET Core client host.
/// </summary>
/// <remarks>
/// Note: the <paramref name="provider"/> parameter MUST match
/// match an existing <see cref="OpenIddictClientRegistration.ProviderName"/>.
/// </remarks>
/// <param name="provider">The provider name, also used as the authentication type.</param>
/// <param name="caption">The caption that will be used as the public/user-visible display name, if applicable.</param>
/// <returns>The <see cref="OpenIddictClientOwinBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientOwinBuilder AddForwardedAuthenticationType(string provider, string? caption)
{
if (string.IsNullOrEmpty(provider))
{
throw new ArgumentException(SR.FormatID0366(nameof(provider)), nameof(provider));
}
return Configure(options => options.ForwardedAuthenticationTypes.Add(new AuthenticationDescription
{
AuthenticationType = provider,
Caption = caption
}));
}
/// <summary>
/// Disables the transport security requirement (HTTPS).
/// </summary>

54
src/OpenIddict.Client.Owin/OpenIddictClientOwinConfiguration.cs

@ -5,6 +5,7 @@
*/
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace OpenIddict.Client.Owin;
@ -16,6 +17,19 @@ namespace OpenIddict.Client.Owin;
public sealed class OpenIddictClientOwinConfiguration : IConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientOwinOptions>
{
private readonly IServiceProvider _provider;
/// <inheritdoc/>
[Obsolete("This constructor is no longer supported and will be removed in a future version.", error: true)]
public OpenIddictClientOwinConfiguration() => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientOwinConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
public OpenIddictClientOwinConfiguration(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/>
public void Configure(OpenIddictClientOptions options)
{
@ -40,5 +54,45 @@ public sealed class OpenIddictClientOwinConfiguration : IConfigureOptions<OpenId
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0314));
}
if (!options.DisableAutomaticAuthenticationTypeForwarding)
{
foreach (var (provider, registrations) in _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>()
.CurrentValue.Registrations
.Where(registration => !string.IsNullOrEmpty(registration.ProviderName))
.GroupBy(registration => registration.ProviderName)
.Select(group => (ProviderName: group.Key, Registrations: group.ToList())))
{
// If an explicit mapping was already added, don't overwrite it.
if (options.ForwardedAuthenticationTypes.Exists(type =>
string.Equals(type.AuthenticationType, provider, StringComparison.Ordinal)))
{
continue;
}
// Ensure multiple client registrations don't share the same provider
// name when automatic authentication type forwarding is enabled.
if (registrations is not [OpenIddictClientRegistration registration])
{
throw new InvalidOperationException(SR.FormatID0416(provider));
}
var description = new AuthenticationDescription
{
AuthenticationType = registration.ProviderName
};
// Note: the AuthenticationDescription.Caption property setter doesn't no-op
// when a null or empty display name is set. To ensure the "Caption" property
// is not added to AuthenticationDescription.Properties when a null display
// name is set, a null check is always performed first before assigning it.
if (!string.IsNullOrEmpty(registration.ProviderDisplayName))
{
description.Caption = registration.ProviderDisplayName;
}
options.ForwardedAuthenticationTypes.Add(description);
}
}
}
}

109
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs

@ -4,8 +4,11 @@
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Owin.Security.Infrastructure;
@ -277,12 +280,12 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
protected override async Task TeardownCoreAsync()
{
// Note: OWIN authentication handlers cannot reliabily write to the response stream
// from ApplyResponseGrantAsync or ApplyResponseChallengeAsync because these methods
// are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback,
// where calling Write or WriteAsync on the response stream may result in a deadlock
// from ApplyResponseGrantAsync() or ApplyResponseChallengeAsync() because these methods
// are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback(),
// where calling Write() or WriteAsync() on the response stream may result in a deadlock
// on hosts using streamed responses. To work around this limitation, this handler
// doesn't implement ApplyResponseGrantAsync but TeardownCoreAsync, which is never called
// by AuthenticationHandler.OnSendingHeaderCallback. In theory, this would prevent
// doesn't implement ApplyResponseGrantAsync() but TeardownCoreAsync(), which is never
// called by AuthenticationHandler.OnSendingHeaderCallback(). In theory, this would prevent
// OpenIddictClientOwinMiddleware from both applying the response grant and allowing
// the next middleware in the pipeline to alter the response stream but in practice,
// OpenIddictClientOwinMiddleware is assumed to be the only middleware allowed to write
@ -291,7 +294,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
// Note: unlike the ASP.NET Core host, the OWIN host MUST check whether the status code
// corresponds to a challenge response, as LookupChallenge() will always return a non-null
// value when active authentication is used, even if no challenge was actually triggered.
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);
var challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode) ?? LookupForwardedChallenge();
if (challenge is not null && Response.StatusCode is 401 or 403)
{
var transaction = Context.Get<OpenIddictClientTransaction>(typeof(OpenIddictClientTransaction).FullName) ??
@ -335,7 +338,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
}
}
var signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode);
var signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode) ?? LookupForwardedSignOut();
if (signout is not null)
{
var transaction = Context.Get<OpenIddictClientTransaction>(typeof(OpenIddictClientTransaction).FullName) ??
@ -378,5 +381,97 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
throw new InvalidOperationException(SR.GetResourceString(SR.ID0111));
}
}
AuthenticationResponseChallenge? LookupForwardedChallenge()
{
// Note: unlike its server counterpart, the OpenIddict OWIN client authentication handler allows
// associating additional authentication types to trigger a provider-specific challenge. For that,
// the authentication types attached to the context are iterated: if a type matches one of the types
// managed by OpenIddict, a challenge pointing to the OpenIddict OWIN client authentication handler
// is dynamically forwarded with the appropriate provider name authentication property attached.
if (Context.Authentication.AuthenticationResponseChallenge?.AuthenticationTypes is { Length: > 0 } types)
{
foreach (var type in types)
{
if (TryGetForwardedAuthenticationType(type, out _))
{
// Ensure no client registration information was attached to the authentication properties.
if (Context.Authentication.AuthenticationResponseChallenge.Properties is AuthenticationProperties properties &&
(properties.Dictionary.ContainsKey(Properties.Issuer) ||
properties.Dictionary.ContainsKey(Properties.ProviderName) ||
properties.Dictionary.ContainsKey(Properties.RegistrationId)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0417));
}
return new AuthenticationResponseChallenge(
authenticationTypes: new[] { OpenIddictClientOwinDefaults.AuthenticationType },
properties : new AuthenticationProperties(dictionary: new Dictionary<string, string>(
Context.Authentication.AuthenticationResponseChallenge.Properties.Dictionary ??
ImmutableDictionary.Create<string, string>())
{
[Properties.ProviderName] = type
}));
}
}
}
return null;
}
AuthenticationResponseRevoke? LookupForwardedSignOut()
{
// Note: unlike its server counterpart, the OpenIddict OWIN client authentication handler allows
// associating additional authentication types to trigger a provider-specific sign-out. For that,
// the authentication types attached to the context are iterated: if a type matches one of the types
// managed by OpenIddict, a sign-out pointing to the OpenIddict OWIN client authentication handler
// is dynamically forwarded with the appropriate provider name authentication property attached.
if (Context.Authentication.AuthenticationResponseRevoke?.AuthenticationTypes is { Length: > 0 } types)
{
foreach (var type in types)
{
if (TryGetForwardedAuthenticationType(type, out _))
{
// Ensure no client registration information was attached to the authentication properties.
if (Context.Authentication.AuthenticationResponseRevoke.Properties is AuthenticationProperties properties &&
(properties.Dictionary.ContainsKey(Properties.Issuer) ||
properties.Dictionary.ContainsKey(Properties.ProviderName) ||
properties.Dictionary.ContainsKey(Properties.RegistrationId)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0417));
}
return new AuthenticationResponseRevoke(
authenticationTypes: new[] { OpenIddictClientOwinDefaults.AuthenticationType },
properties : new AuthenticationProperties(dictionary: new Dictionary<string, string>(
Context.Authentication.AuthenticationResponseRevoke.Properties.Dictionary ??
ImmutableDictionary.Create<string, string>())
{
[Properties.ProviderName] = type
}));
}
}
}
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool TryGetForwardedAuthenticationType(string type, [NotNullWhen(true)] out AuthenticationDescription? result)
{
foreach (var description in Options.ForwardedAuthenticationTypes)
{
if (string.Equals(description.AuthenticationType, type, StringComparison.Ordinal))
{
result = description;
return true;
}
}
result = null;
return false;
}
}
}

110
src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs

@ -5,14 +5,29 @@
*/
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.Extensions.Options;
using Microsoft.Owin.Security.Infrastructure;
namespace OpenIddict.Client.Owin;
// See https://github.com/owin/owin/issues/7 for more information.
using AuthenticateDelegate = Func<
/* Authentication types: */ string[]?,
/* Callback: */ Action<
/* Identity: */ IIdentity?,
/* Authentication properties: */ IDictionary<string, string?>?,
/* Authentication description: */ IDictionary<string, object?>?,
/* State: */ object?>,
/* State: */ object?,
Task>;
/// <summary>
/// Provides the entry point necessary to register the OpenIddict client handler in an OWIN pipeline.
/// Note: this middleware is intented to be used with dependency injection containers
/// Note: this middleware is intended to be used with dependency injection containers
/// that support middleware resolution, like Autofac. Since it depends on scoped services,
/// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware.
/// </summary>
@ -40,6 +55,99 @@ public sealed class OpenIddictClientOwinMiddleware : AuthenticationMiddleware<Op
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
/// <inheritdoc/>
public override async Task Invoke(IOwinContext context)
{
// Retrieve the existing authentication delegate.
var function = context.Get<AuthenticateDelegate?>("security.Authenticate");
try
{
// Replace the security.Authenticate delegate responsible for listing authentication types and returning
// identities to handle the forwarded authentication types managed by the OpenIddict OWIN client host.
context.Set<AuthenticateDelegate>("security.Authenticate", async (types, callback, state) =>
{
// Note: a null array is typically used by OWIN to resolve all the configured authentication types.
// In this case, iterate all the forwarded authentication types and call the callback action for each type.
if (types is null)
{
foreach (var description in Options.ForwardedAuthenticationTypes)
{
callback(null, null, description.Properties, state);
}
}
else if (types.Length is > 0)
{
foreach (var type in types)
{
// If the specified authentication types don't match a forwarded authentication type
// managed by the OpenIddict OWIN client host, don't invoke the callback and let the
// corresponding authentication middleware handle it if it matches a registered type.
if (string.IsNullOrEmpty(type) ||
string.Equals(type, OpenIddictClientOwinDefaults.AuthenticationType, StringComparison.Ordinal) ||
!TryGetForwardedAuthenticationType(type, out AuthenticationDescription? description))
{
continue;
}
// Resolve the authentication result returned by the OpenIddict OWIN client host:
// if the returned identity was created by the specified provider, return the result
// and stop iterating (only a single identity is returned by the OWIN host).
//
// Note: exceptions MUST NOT be caught to ensure they are properly surfaced to the caller
// (e.g if AuthenticateAsync("[provider name]") is called from an unsupported endpoint).
if (await context.Authentication.AuthenticateAsync(OpenIddictClientOwinDefaults.AuthenticationType)
is { Identity: ClaimsIdentity identity } result &&
identity.FindFirst(Claims.Private.ProviderName)?.Value is string provider &&
string.Equals(provider, description.AuthenticationType, StringComparison.Ordinal))
{
callback(
new ClaimsIdentity(
identity, identity.Claims, description.AuthenticationType,
identity.NameClaimType, identity.RoleClaimType),
result.Properties.Dictionary, description.Properties, state);
break;
}
}
}
// Always invoke the original authentication delegate to allow the other
// authentication middleware to return the authentication types they
// support and the identities they were able to extract, if applicable.
if (function is not null)
{
await function(types, callback, state);
}
});
await base.Invoke(context);
}
finally
{
// Restore the original authentication delegate.
context.Set("security.Authenticate", function);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool TryGetForwardedAuthenticationType(string type, [NotNullWhen(true)] out AuthenticationDescription? result)
{
foreach (var description in Options.ForwardedAuthenticationTypes)
{
if (string.Equals(description.AuthenticationType, type, StringComparison.Ordinal))
{
result = description;
return true;
}
}
result = null;
return false;
}
}
/// <summary>
/// Creates and returns a new <see cref="OpenIddictClientOwinHandler"/> instance.
/// </summary>

15
src/OpenIddict.Client.Owin/OpenIddictClientOwinOptions.cs

@ -20,6 +20,21 @@ public sealed class OpenIddictClientOwinOptions : AuthenticationOptions
: base(OpenIddictClientOwinDefaults.AuthenticationType)
=> AuthenticationMode = AuthenticationMode.Passive;
/// <summary>
/// Gets or sets a boolean indicating whether the static client registrations with a non-null
/// provider name attached are automatically added to <see cref="ForwardedAuthenticationTypes"/>.
/// When automatic forwarding is disabled, calls to <see cref="IAuthenticationManager"/> such as
/// <see cref="IAuthenticationManager.Challenge(AuthenticationProperties, string[])"/>
/// cannot directly use the provider name associated to a client registration as the authentication
/// scheme and must set the provider name (or the issuer) as an authentication property instead.
/// </summary>
public bool DisableAutomaticAuthenticationTypeForwarding { get; set; }
/// <summary>
/// Gets the forwarded authentication types that are managed by the OpenIddict OWIN client host.
/// </summary>
public List<AuthenticationDescription> ForwardedAuthenticationTypes { get; } = new();
/// <summary>
/// Gets or sets a boolean indicating whether incoming requests arriving on insecure endpoints should be
/// rejected and whether challenge and sign-out operations can be triggered from non-HTTPS endpoints.

2
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml

@ -14,7 +14,7 @@
-->
<Provider Name="ActiveDirectoryFederationServices"
DisplayName="Microsoft Active Directory Federation Services" Id="01bcc179-3f17-41db-8923-8f05e6c26a8c"
DisplayName="Active Directory Federation Services" Id="01bcc179-3f17-41db-8923-8f05e6c26a8c"
Documentation="https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/overview/ad-fs-openid-connect-oauth-flows-scenarios">
<!--
Note: Active Directory Federation Services (ADFS) is a self-hosted identity provider that

5
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -108,6 +108,11 @@ public sealed class OpenIddictClientRegistration
/// </summary>
public Uri? Issuer { get; set; }
/// <summary>
/// Gets or sets the provider display name.
/// </summary>
public string? ProviderDisplayName { get; set; }
/// <summary>
/// Gets or sets the provider name.
/// </summary>

10
src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs

@ -266,12 +266,12 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<OpenIddi
protected override async Task TeardownCoreAsync()
{
// Note: OWIN authentication handlers cannot reliabily write to the response stream
// from ApplyResponseGrantAsync or ApplyResponseChallengeAsync because these methods
// are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback,
// where calling Write or WriteAsync on the response stream may result in a deadlock
// from ApplyResponseGrantAsync() or ApplyResponseChallengeAsync() because these methods
// are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback(),
// where calling Write() or WriteAsync() on the response stream may result in a deadlock
// on hosts using streamed responses. To work around this limitation, this handler
// doesn't implement ApplyResponseGrantAsync but TeardownCoreAsync, which is never called
// by AuthenticationHandler.OnSendingHeaderCallback. In theory, this would prevent
// doesn't implement ApplyResponseGrantAsync() but TeardownCoreAsync(), which is never
// called by AuthenticationHandler.OnSendingHeaderCallback(). In theory, this would prevent
// OpenIddictServerOwinMiddleware from both applying the response grant and allowing
// the next middleware in the pipeline to alter the response stream but in practice,
// OpenIddictServerOwinMiddleware is assumed to be the only middleware allowed to write

2
src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs

@ -12,7 +12,7 @@ namespace OpenIddict.Server.Owin;
/// <summary>
/// Provides the entry point necessary to register the OpenIddict server handler in an OWIN pipeline.
/// Note: this middleware is intented to be used with dependency injection containers
/// Note: this middleware is intended to be used with dependency injection containers
/// that support middleware resolution, like Autofac. Since it depends on scoped services,
/// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware.
/// </summary>

10
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs

@ -197,12 +197,12 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Open
protected override async Task TeardownCoreAsync()
{
// Note: OWIN authentication handlers cannot reliabily write to the response stream
// from ApplyResponseGrantAsync or ApplyResponseChallengeAsync because these methods
// are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback,
// where calling Write or WriteAsync on the response stream may result in a deadlock
// from ApplyResponseGrantAsync() or ApplyResponseChallengeAsync() because these methods
// are susceptible to be invoked from AuthenticationHandler.OnSendingHeaderCallback(),
// where calling Write() or WriteAsync() on the response stream may result in a deadlock
// on hosts using streamed responses. To work around this limitation, this handler
// doesn't implement ApplyResponseGrantAsync but TeardownCoreAsync, which is never called
// by AuthenticationHandler.OnSendingHeaderCallback. In theory, this would prevent
// doesn't implement ApplyResponseGrantAsync() but TeardownCoreAsync(), which is never
// called by AuthenticationHandler.OnSendingHeaderCallback(). In theory, this would prevent
// OpenIddictValidationOwinMiddleware from both applying the response grant and allowing
// the next middleware in the pipeline to alter the response stream but in practice,
// OpenIddictValidationOwinMiddleware is assumed to be the only middleware allowed to write

2
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs

@ -12,7 +12,7 @@ namespace OpenIddict.Validation.Owin;
/// <summary>
/// Provides the entry point necessary to register the OpenIddict validation handler in an OWIN pipeline.
/// Note: this middleware is intented to be used with dependency injection containers
/// Note: this middleware is intended to be used with dependency injection containers
/// that support middleware resolution, like Autofac. Since it depends on scoped services,
/// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware.
/// </summary>

Loading…
Cancel
Save