Browse Source

Support multiple registrations with the same issuer/provider name and introduce client record models

pull/1796/head
Kévin Chalet 3 years ago
parent
commit
64ade94294
  1. 4
      Directory.Packages.props
  2. 511
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  3. 16
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs
  4. 28
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs
  5. 4
      sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
  6. 6
      sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config
  7. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  8. 6
      sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config
  9. 16
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  10. 27
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs
  11. 6
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  12. 2
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs
  13. 44
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  14. 23
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  15. 16
      sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs
  16. 2
      sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs
  17. 16
      sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs
  18. 2
      sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs
  19. 1
      src/OpenIddict.Abstractions/OpenIddictConstants.cs
  20. 41
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  21. 1
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs
  22. 9
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  23. 14
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs
  24. 1
      src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs
  25. 9
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  26. 14
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs
  27. 1
      src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj
  28. 6
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
  29. 100
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs
  30. 44
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
  31. 3
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  32. 14
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs
  33. 109
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs
  34. 17
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs
  35. 2
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Authentication.cs
  36. 2
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs
  37. 26
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
  38. 16
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs
  39. 6
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs
  40. 32
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
  41. 135
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  42. 1
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationOptions.cs
  43. 132
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
  44. 12
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd
  45. 15
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs
  46. 1
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  47. 65
      src/OpenIddict.Client/OpenIddictClientConfiguration.cs
  48. 2
      src/OpenIddict.Client/OpenIddictClientEvents.Exchange.cs
  49. 34
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  50. 260
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  51. 760
      src/OpenIddict.Client/OpenIddictClientModels.cs
  52. 31
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  53. 1013
      src/OpenIddict.Client/OpenIddictClientService.cs
  54. 13
      src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

4
Directory.Packages.props

@ -189,11 +189,11 @@
<PackageVersion Include="Microsoft.AspNet.Mvc" Version="5.2.9" />
<PackageVersion Include="Microsoft.AspNet.Web.Optimization" Version="1.1.3" />
<PackageVersion Include="Microsoft.AspNet.WebApi.Owin" Version="5.2.9" />
<PackageVersion Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" Version="3.6.0" />
<PackageVersion Include="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" Version="4.1.0" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.14" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="2.1.1" />
<PackageVersion Include="Microsoft.Extensions.Logging.Debug" Version="2.1.1" />
<PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="3.8.0" />
<PackageVersion Include="Microsoft.Net.Compilers.Toolset" Version="4.6.0" />
<PackageVersion Include="Microsoft.Owin.Host.SystemWeb" Version="4.2.2" />
<PackageVersion Include="Microsoft.Owin.Security.Cookies" Version="4.2.2" />
<PackageVersion Include="Microsoft.Owin.Security.OAuth" Version="4.2.2" />

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

@ -43,6 +43,10 @@ namespace OpenIddict.Client.WebIntegration.Generators
"OpenIddictClientWebIntegrationOptions.generated.cs",
SourceText.From(GenerateOptions(document), Encoding.UTF8));
context.AddSource(
"OpenIddictClientWebIntegrationSettings.generated.cs",
SourceText.From(GenerateSettings(document), Encoding.UTF8));
static string GenerateBuilderMethods(XDocument document)
{
var template = Template.Parse(@"#nullable enable
@ -75,18 +79,29 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
/// </summary>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete($""This method was replaced by {nameof(Add{{ provider.name }})} and will be removed in a future version."")]
public OpenIddictClientWebIntegrationBuilder.{{ provider.name }} Use{{ provider.name }}()
=> Add{{ provider.name }}();
/// <summary>
/// Adds a new {{ provider.display_name }} client registration.
{{~ if provider.documentation ~}}
/// For more information, read <see href=""{{ provider.documentation }}"">the documentation</see>.
{{~ end ~}}
/// </summary>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
public OpenIddictClientWebIntegrationBuilder.{{ provider.name }} Add{{ provider.name }}()
{
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
Services.TryAddEnumerable(new[]
var registration = new OpenIddictClientRegistration
{
ServiceDescriptor.Singleton<
IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>(),
ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictClientWebIntegrationOptions.{{ provider.name }}>, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>()
});
ProviderSettings = new OpenIddictClientWebIntegrationSettings.{{ provider.name }}(),
ProviderType = ProviderTypes.{{ provider.name }}
};
Services.Configure<OpenIddictClientOptions>(options => options.Registrations.Add(registration));
return new OpenIddictClientWebIntegrationBuilder.{{ provider.name }}(Services);
return new OpenIddictClientWebIntegrationBuilder.{{ provider.name }}(registration);
}
/// <summary>
@ -98,14 +113,27 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <param name=""configuration"">The delegate used to configure the OpenIddict/{{ provider.display_name }} options.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder""/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete($""This method was replaced by {nameof(Add{{ provider.name }})} and will be removed in a future version."")]
public OpenIddictClientWebIntegrationBuilder Use{{ provider.name }}(Action<OpenIddictClientWebIntegrationBuilder.{{ provider.name }}> configuration)
=> Add{{ provider.name }}(configuration);
/// <summary>
/// Adds a new {{ provider.display_name }} client registration.
{{~ if provider.documentation ~}}
/// For more information, read <see href=""{{ provider.documentation }}"">the documentation</see>.
{{~ end ~}}
/// </summary>
/// <param name=""configuration"">The delegate used to configure the OpenIddict/{{ provider.display_name }} options.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder""/> instance.</returns>
public OpenIddictClientWebIntegrationBuilder Add{{ provider.name }}(Action<OpenIddictClientWebIntegrationBuilder.{{ provider.name }}> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(Use{{ provider.name }}());
configuration(Add{{ provider.name }}());
return this;
}
@ -121,14 +149,28 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
/// Initializes a new instance of <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/>.
/// </summary>
/// <param name=""services"">The services collection.</param>
[Obsolete(""This constructor is no longer supported and will be removed in a future version."", error: true)]
public {{ provider.name }}(IServiceCollection services)
=> Services = services ?? throw new ArgumentNullException(nameof(services));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Initializes a new instance of <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/>.
/// </summary>
/// <param name=""registration"">The client registration.</param>
public {{ provider.name }}(OpenIddictClientRegistration registration)
=> Registration = registration ?? throw new ArgumentNullException(nameof(registration));
/// <summary>
/// Gets the client registration.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public OpenIddictClientRegistration Registration { get; }
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
public IServiceCollection Services => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Amends the default OpenIddict client {{ provider.display_name }} configuration.
@ -136,16 +178,38 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
/// <param name=""configuration"">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
[Obsolete(""This method is no longer supported and will be removed in a future version."", error: true)]
public {{ provider.name }} Configure(Action<OpenIddictClientWebIntegrationOptions.{{ provider.name }}> configuration)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Sets the provider name.
/// </summary>
/// <param name=""name"">The provider name.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
public {{ provider.name }} SetProviderName(string name)
{
if (configuration is null)
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(configuration));
throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(name));
}
Services.Configure(configuration);
return Set(registration => registration.ProviderName = name);
}
return this;
/// <summary>
/// Sets the registration identifier.
/// </summary>
/// <param name=""identifier"">The registration identifier.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
public {{ provider.name }} SetRegistrationId(string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(identifier));
}
return Set(registration => registration.RegistrationId = identifier);
}
/// <summary>
@ -160,7 +224,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentException(SR.GetResourceString(SR.ID0124), nameof(identifier));
}
return Configure(options => options.ClientId = identifier);
return Set(registration => registration.ClientId = identifier);
}
/// <summary>
@ -175,7 +239,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentException(SR.GetResourceString(SR.ID0125), nameof(secret));
}
return Configure(options => options.ClientSecret = secret);
return Set(registration => registration.ClientSecret = secret);
}
/// <summary>
@ -194,7 +258,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentNullException(nameof(uri));
}
return Configure(options => options.PostLogoutRedirectUri = uri);
return Set(registration => registration.PostLogoutRedirectUri = uri);
}
/// <summary>
@ -232,7 +296,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentNullException(nameof(uri));
}
return Configure(options => options.RedirectUri = uri);
return Set(registration => registration.RedirectUri = uri);
}
/// <summary>
@ -266,7 +330,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentNullException(nameof(scopes));
}
return Configure(options => options.Scopes.UnionWith(scopes));
return Set(registration => registration.Scopes.UnionWith(scopes));
}
{{~ for environment in provider.environments ~}}
@ -275,7 +339,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
/// </summary>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
public {{ provider.name }} Use{{ environment.name }}Environment()
=> Configure(options => options.Environment = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }});
=> Set(registration => registration.Get{{ provider.name }}Settings().Environment = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }});
{{~ end ~}}
{{~ for setting in provider.settings ~}}
@ -295,7 +359,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentNullException(nameof({{ setting.parameter_name }}));
}
return Configure(options => options.{{ setting.property_name }}.UnionWith({{ setting.parameter_name }}));
return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }}.UnionWith({{ setting.parameter_name }}));
}
{{~ else if setting.clr_type == 'ECDsaSecurityKey' ~}}
/// <summary>
@ -318,7 +382,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentException(SR.GetResourceString(SR.ID0055), nameof({{ setting.parameter_name }}));
}
return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }});
return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }});
}
#if SUPPORTS_PEM_ENCODED_KEY_IMPORT
@ -403,7 +467,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof({{ setting.parameter_name }}));
}
return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }});
return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }});
}
/// <summary>
@ -444,7 +508,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentException(SR.GetResourceString(SR.ID0061), nameof({{ setting.parameter_name }}));
}
return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }});
return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }});
}
/// <summary>
@ -615,7 +679,7 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
throw new ArgumentNullException(nameof({{ setting.parameter_name }}));
}
return Configure(options => options.{{ setting.property_name }} = {{ setting.parameter_name }});
return Set(registration => registration.Get{{ provider.name }}Settings().{{ setting.property_name }} = {{ setting.parameter_name }});
}
{{~ end ~}}
@ -632,6 +696,24 @@ public sealed partial class OpenIddictClientWebIntegrationBuilder
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override string? ToString() => base.ToString();
/// <summary>
/// Amends the client registration created by the {{ provider.display_name }} integration.
/// </summary>
/// <param name=""configuration"">The delegate used to configure the {{ provider.display_name }} client registration.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref=""OpenIddictClientRegistration""/> instance.</returns>
private {{ provider.name }} Set(Action<OpenIddictClientRegistration> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(Registration);
return this;
}
}
{{~ end ~}}
}
@ -713,6 +795,13 @@ public static partial class OpenIddictClientWebIntegrationConstants
public const string {{ provider.name }} = ""{{ provider.name }}"";
{{~ end ~}}
}
public static class ProviderTypes
{
{{~ for provider in providers ~}}
public const string {{ provider.name }} = ""{{ provider.id }}"";
{{~ end ~}}
}
}
");
return template.Render(new
@ -721,6 +810,7 @@ public static partial class OpenIddictClientWebIntegrationConstants
.Select(provider => new
{
Name = (string) provider.Attribute("Name"),
Id = (string) provider.Attribute("Id"),
Environments = provider.Elements("Environment").Select(environment => new
{
@ -751,36 +841,58 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration
/// <summary>
/// Contains the methods required to register the {{ provider.display_name }} integration in the OpenIddict client options.
/// </summary>
[Obsolete(""This class is no longer supported and will be removed in a future version."", error: true)]
public sealed class {{ provider.name }} : IConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientWebIntegrationOptions.{{ provider.name }}>
{
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref=""OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}"" /> class.
/// </summary>
/// <param name=""provider"">The service provider.</param>
/// <exception cref=""ArgumentException""><paramref name=""provider""/> is null.</exception>
[Obsolete(""This constructor is no longer supported and will be removed in a future version."", error: true)]
public {{ provider.name }}(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <inheritdoc/>
[Obsolete(""This method is no longer supported and will be removed in a future version."", error: true)]
public void PostConfigure(string? name, OpenIddictClientWebIntegrationOptions.{{ provider.name }} options)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <inheritdoc/>
[Obsolete(""This method is no longer supported and will be removed in a future version."", error: true)]
public void Configure(OpenIddictClientOptions options)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
{{~ end ~}}
static partial void ConfigureProvider(OpenIddictClientRegistration registration)
{
{{~ for provider in providers ~}}
{{~ if for.index == 0 ~}}
if (registration.ProviderType is ProviderTypes.{{ provider.name }})
{{~ else ~}}
else if (registration.ProviderType is ProviderTypes.{{ provider.name }})
{{~ end ~}}
{
if (registration.ProviderSettings is not OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0406));
}
{{~ for setting in provider.settings ~}}
{{~ if setting.default_value && setting.type == 'String' ~}}
if (string.IsNullOrEmpty(options.{{ setting.property_name }}))
if (string.IsNullOrEmpty(settings.{{ setting.property_name }}))
{
options.{{ setting.property_name }} = ""{{ setting.default_value }}"";
settings.{{ setting.property_name }} = ""{{ setting.default_value }}"";
}
{{~ end ~}}
{{~ if setting.collection ~}}
if (options.{{ setting.property_name }}.Count is 0)
if (settings.{{ setting.property_name }}.Count is 0)
{
{{~ for item in setting.items ~}}
{{~ if item.default && !item.required ~}}
options.{{ setting.property_name }}.Add(""{{ item.value }}"");
settings.{{ setting.property_name }}.Add(""{{ item.value }}"");
{{~ end ~}}
{{~ end ~}}
}
@ -788,202 +900,175 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration
{{~ for item in setting.items ~}}
{{~ if item.required ~}}
options.{{ setting.property_name }}.Add(""{{ item.value }}"");
settings.{{ setting.property_name }}.Add(""{{ item.value }}"");
{{~ end ~}}
{{~ end ~}}
{{~ end ~}}
{{~ for environment in provider.environments ~}}
if (options.Environment is OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }})
if (settings.Environment is OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }})
{
if (options.Scopes.Count is 0)
if (registration.Scopes.Count is 0)
{
{{~ for scope in environment.scopes ~}}
{{~ if scope.default && !scope.required ~}}
options.Scopes.Add(""{{ scope.name }}"");
registration.Scopes.Add(""{{ scope.name }}"");
{{~ end ~}}
{{~ end ~}}
}
{{~ for scope in environment.scopes ~}}
{{~ if scope.required ~}}
options.Scopes.Add(""{{ scope.name }}"");
registration.Scopes.Add(""{{ scope.name }}"");
{{~ end ~}}
{{~ end ~}}
}
{{~ end ~}}
if (string.IsNullOrEmpty(options.ClientId))
{
throw new InvalidOperationException(SR.FormatID0332(nameof(options.ClientId), Providers.{{ provider.name }}));
}
{{~ for setting in provider.settings ~}}
{{~ if setting.required ~}}
{{~ if setting.type == 'String' ~}}
if (string.IsNullOrEmpty(options.{{ setting.property_name }}))
if (string.IsNullOrEmpty(settings.{{ setting.property_name }}))
{{~ else ~}}
if (options.{{ setting.property_name }} is null)
if (settings.{{ setting.property_name }} is null)
{{~ end ~}}
{
throw new InvalidOperationException(SR.FormatID0332(nameof(options.{{ setting.property_name }}), Providers.{{ provider.name }}));
throw new InvalidOperationException(SR.FormatID0332(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }}));
}
{{~ end ~}}
{{~ if setting.type == 'Uri' ~}}
if (!options.{{ setting.property_name }}.IsAbsoluteUri || !options.{{ setting.property_name }}.IsWellFormedOriginalString())
if (!settings.{{ setting.property_name }}.IsAbsoluteUri || !settings.{{ setting.property_name }}.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.FormatID0350(nameof(options.{{ setting.property_name }}), Providers.{{ provider.name }}));
throw new InvalidOperationException(SR.FormatID0350(nameof(settings.{{ setting.property_name }}), Providers.{{ provider.name }}));
}
{{~ end ~}}
{{~ end ~}}
}
/// <inheritdoc/>
public void Configure(OpenIddictClientOptions options)
{
// Resolve the provider options from the service provider and create a registration based on the specified settings.
var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientWebIntegrationOptions.{{ provider.name }}>>().CurrentValue;
registration.ProviderName ??= Providers.{{ provider.name }};
var registration = new OpenIddictClientRegistration
registration.Issuer ??= settings.Environment switch
{
ProviderName = Providers.{{ provider.name }},
ProviderOptions = settings,
{{~ for environment in provider.environments ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}
=> new Uri($""{{ environment.issuer | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ end ~}}
Issuer = settings.Environment switch
{
{{~ for environment in provider.environments ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}
=> new Uri($""{{ environment.issuer | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ end ~}}
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
};
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
},
registration.ConfigurationEndpoint ??= settings.Environment switch
{
{{~ for environment in provider.environments ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}
{{~ if environment.configuration_endpoint ~}}
=> new Uri($""{{ environment.configuration_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ else ~}}
=> null,
{{~ end ~}}
{{~ end ~}}
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
};
ConfigurationEndpoint = settings.Environment switch
registration.Configuration ??= settings.Environment switch
{
{{~ for environment in provider.environments ~}}
{{~ if environment.configuration ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => new OpenIddictConfiguration
{
{{~ for environment in provider.environments ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }}
{{~ if environment.configuration_endpoint ~}}
=> new Uri($""{{ environment.configuration_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ else ~}}
=> null,
{{~ end ~}}
{{~ if environment.configuration.authorization_endpoint ~}}
AuthorizationEndpoint = new Uri($""{{ environment.configuration.authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ end ~}}
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
},
{{~ if environment.configuration.device_authorization_endpoint ~}}
DeviceAuthorizationEndpoint = new Uri($""{{ environment.configuration.device_authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ end ~}}
ClientId = settings.ClientId,
ClientSecret = settings.ClientSecret,
{{~ if environment.configuration.token_endpoint ~}}
TokenEndpoint = new Uri($""{{ environment.configuration.token_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ end ~}}
PostLogoutRedirectUri = settings.PostLogoutRedirectUri,
RedirectUri = settings.RedirectUri,
{{~ if environment.configuration.userinfo_endpoint ~}}
UserinfoEndpoint = new Uri($""{{ environment.configuration.userinfo_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ end ~}}
Configuration = settings.Environment switch
{
{{~ for environment in provider.environments ~}}
{{~ if environment.configuration ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => new OpenIddictConfiguration
CodeChallengeMethodsSupported =
{
{{~ if environment.configuration.authorization_endpoint ~}}
AuthorizationEndpoint = new Uri($""{{ environment.configuration.authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
{{~ for method in environment.configuration.code_challenge_methods_supported ~}}
""{{ method }}"",
{{~ end ~}}
},
{{~ if environment.configuration.device_authorization_endpoint ~}}
DeviceAuthorizationEndpoint = new Uri($""{{ environment.configuration.device_authorization_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
GrantTypesSupported =
{
{{~ for type in environment.configuration.grant_types_supported ~}}
""{{ type }}"",
{{~ end ~}}
},
{{~ if environment.configuration.token_endpoint ~}}
TokenEndpoint = new Uri($""{{ environment.configuration.token_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
ResponseModesSupported =
{
{{~ for mode in environment.configuration.response_modes_supported ~}}
""{{ mode }}"",
{{~ end ~}}
},
{{~ if environment.configuration.userinfo_endpoint ~}}
UserinfoEndpoint = new Uri($""{{ environment.configuration.userinfo_endpoint | string.replace '\'' '""' }}"", UriKind.Absolute),
ResponseTypesSupported =
{
{{~ for type in environment.configuration.response_types_supported ~}}
""{{ type }}"",
{{~ end ~}}
},
CodeChallengeMethodsSupported =
{
{{~ for method in environment.configuration.code_challenge_methods_supported ~}}
""{{ method }}"",
{{~ end ~}}
},
GrantTypesSupported =
{
{{~ for type in environment.configuration.grant_types_supported ~}}
""{{ type }}"",
{{~ end ~}}
},
ResponseModesSupported =
{
{{~ for mode in environment.configuration.response_modes_supported ~}}
""{{ mode }}"",
{{~ end ~}}
},
ResponseTypesSupported =
{
{{~ for type in environment.configuration.response_types_supported ~}}
""{{ type }}"",
{{~ end ~}}
},
ScopesSupported =
{
{{~ for scope in environment.configuration.scopes_supported ~}}
""{{ scope }}"",
{{~ end ~}}
},
DeviceAuthorizationEndpointAuthMethodsSupported =
{
{{~ for method in environment.configuration.device_authorization_endpoint_auth_methods_supported ~}}
""{{ method }}"",
{{~ end ~}}
},
TokenEndpointAuthMethodsSupported =
{
{{~ for method in environment.configuration.token_endpoint_auth_methods_supported ~}}
""{{ method }}"",
{{~ end ~}}
}
ScopesSupported =
{
{{~ for scope in environment.configuration.scopes_supported ~}}
""{{ scope }}"",
{{~ end ~}}
},
{{~ else ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => null,
{{~ end ~}}
{{~ end ~}}
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
},
DeviceAuthorizationEndpointAuthMethodsSupported =
{
{{~ for method in environment.configuration.device_authorization_endpoint_auth_methods_supported ~}}
""{{ method }}"",
{{~ end ~}}
},
EncryptionCredentials =
{
{{~ for setting in provider.settings ~}}
{{~ if setting.type == 'EncryptionKey' ~}}
new EncryptingCredentials(settings.{{ setting.property_name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512),
{{~ end ~}}
{{~ end ~}}
TokenEndpointAuthMethodsSupported =
{
{{~ for method in environment.configuration.token_endpoint_auth_methods_supported ~}}
""{{ method }}"",
{{~ end ~}}
}
},
{{~ else ~}}
OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.{{ environment.name }} => null,
{{~ end ~}}
{{~ end ~}}
SigningCredentials =
{
{{~ for setting in provider.settings ~}}
{{~ if setting.type == 'SigningKey' ~}}
new SigningCredentials(settings.{{ setting.property_name }}, ""{{ setting.signing_algorithm }}""),
{{~ end ~}}
{{~ end ~}}
}
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
};
registration.Scopes.UnionWith(settings.Scopes);
{{~ for setting in provider.settings ~}}
{{~ if setting.type == 'EncryptionKey' ~}}
registration.EncryptionCredentials.Add(new EncryptingCredentials(settings.{{ setting.property_name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512));
{{~ end ~}}
{{~ end ~}}
options.Registrations.Add(registration);
{{~ for setting in provider.settings ~}}
{{~ if setting.type == 'SigningKey' ~}}
registration.SigningCredentials.Add(new SigningCredentials(settings.{{ setting.property_name }}, ""{{ setting.signing_algorithm }}""));
{{~ end ~}}
{{~ end ~}}
}
{{~ end ~}}
else
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0407));
}
}
{{~ end ~}}
}
");
return template.Render(new
@ -1126,8 +1211,18 @@ public static partial class OpenIddictClientWebIntegrationHelpers
/// <param name=""registration"">The client registration.</param>
/// <returns>The {{ provider.display_name }} provider options.</returns>
/// <exception cref=""InvalidOperationException"">The provider options cannot be resolved.</exception>
[Obsolete($""This extension was replaced by {nameof(Get{{ provider.name }}Settings)} and will be removed in a future version."", error: true)]
public static OpenIddictClientWebIntegrationOptions.{{ provider.name }} Get{{ provider.name }}Options(this OpenIddictClientRegistration registration)
=> registration.ProviderOptions is OpenIddictClientWebIntegrationOptions.{{ provider.name }} options ? options :
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <summary>
/// Resolves the {{ provider.display_name }} provider settings from the specified registration.
/// </summary>
/// <param name=""registration"">The client registration.</param>
/// <returns>The {{ provider.display_name }} provider settings.</returns>
/// <exception cref=""InvalidOperationException"">The provider options cannot be resolved.</exception>
public static OpenIddictClientWebIntegrationSettings.{{ provider.name }} Get{{ provider.name }}Settings(this OpenIddictClientRegistration registration)
=> registration.ProviderSettings is OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings ? settings :
throw new InvalidOperationException(SR.FormatID0333(Providers.{{ provider.name }}));
{{~ end ~}}
@ -1160,6 +1255,7 @@ public sealed partial class OpenIddictClientWebIntegrationOptions
/// <summary>
/// Provides various options needed to configure the {{ provider.display_name }} integration.
/// </summary>
[Obsolete(""This class is no longer supported and will be removed in a future version."")]
public sealed class {{ provider.name }}
{
/// <summary>
@ -1260,6 +1356,89 @@ public sealed partial class OpenIddictClientWebIntegrationOptions
.ToList()
});
}
static string GenerateSettings(XDocument document)
{
var template = Template.Parse(@"#nullable enable
using System.Security.Cryptography.X509Certificates;
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Client.WebIntegration;
public sealed partial class OpenIddictClientWebIntegrationSettings
{
{{~ for provider in providers ~}}
/// <summary>
/// Provides various options needed to configure the {{ provider.display_name }} integration.
/// </summary>
public sealed class {{ provider.name }}
{
/// <summary>
/// Gets or sets the environment that determines the endpoints to use (by default, ""Production"").
/// </summary>
public string? Environment { get; set; } = OpenIddictClientWebIntegrationConstants.{{ provider.name }}.Environments.Production;
{{~ for setting in provider.settings ~}}
/// <summary>
/// Gets or sets {{ setting.description }}.
/// </summary>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
{{~ if setting.collection ~}}
public HashSet<{{ setting.clr_type }}> {{ setting.property_name }} { get; } = new();
{{~ else ~}}
public {{ setting.clr_type }}? {{ setting.property_name }} { get; set; }
{{~ end ~}}
{{~ end ~}}
}
{{~ end ~}}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new
{
Name = (string) provider.Attribute("Name"),
DisplayName = (string?) provider.Attribute("DisplayName") ?? (string) provider.Attribute("Name"),
Settings = provider.Elements("Setting").Select(setting => new
{
PropertyName = (string) setting.Attribute("PropertyName"),
Collection = (bool?) setting.Attribute("Collection") ?? false,
Obsolete = (bool?) setting.Attribute("Obsolete") ?? false,
Description = (string) setting.Attribute("Description") is string description ?
char.ToLower(description[0], CultureInfo.GetCultureInfo("en-US")) + description[1..] : null,
ClrType = (string) setting.Attribute("Type") switch
{
"EncryptionKey" when (string) setting.Element("EncryptionAlgorithm").Attribute("Value")
is "RS256" or "RS384" or "RS512" => "RsaSecurityKey",
"SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value")
is "ES256" or "ES384" or "ES512" => "ECDsaSecurityKey",
"SigningKey" when (string) setting.Element("SigningAlgorithm").Attribute("Value")
is "PS256" or "PS384" or "PS512" or
"RS256" or "RS384" or "RS512" => "RsaSecurityKey",
"Certificate" => "X509Certificate2",
"String" => "string",
"StringHashSet" => "HashSet<string>",
"Uri" => "Uri",
string value => value
}
})
.ToList()
})
.ToList()
});
}
}
public void Initialize(GeneratorInitializationContext context)

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

@ -98,16 +98,14 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
// Remove the local authentication cookie before triggering a redirection to the remote server.
context.Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
// Resolve the provider of the user identifier claim stored in the local authentication cookie.
// Extract the client registration identifier and retrieve the associated server configuration.
// If the provider is known to support remote sign-out, ask OpenIddict to initiate a logout request.
if (Uri.TryCreate(identity.FindFirst(Claims.AuthorizationServer)?.Value, UriKind.Absolute, out Uri issuer) &&
await _service.GetServerConfigurationAsync(issuer) is { EndSessionEndpoint: Uri })
if (identity.FindFirst(Claims.Private.RegistrationId)?.Value is string identifier &&
await _service.GetServerConfigurationByRegistrationIdAsync(identifier) is { EndSessionEndpoint: Uri })
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientOwinConstants.Properties.Issuer] = issuer.AbsoluteUri,
[OpenIddictClientOwinConstants.Properties.RegistrationId] = identifier,
// While not required, the specification encourages sending an id_token_hint
// parameter containing an identity token returned by the server for this user.
@ -205,8 +203,10 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
"http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider"
} => true,
// Preserve the identity of the authorization server as a dedicated claim.
{ Type: Claims.AuthorizationServer } => true,
// Preserve the client registration identifier as a dedicated claim so that the
// associated server configuration can be resolved from the logout endpoint to
// determine whether the authorization server supports client-initiated logouts.
{ Type: Claims.Private.RegistrationId } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,

28
sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/HomeController.cs

@ -1,5 +1,4 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
@ -9,6 +8,7 @@ using System.Web.Mvc;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using OpenIddict.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants;
namespace OpenIddict.Sandbox.AspNet.Client.Controllers
@ -55,32 +55,34 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
{
var context = HttpContext.GetOwinContext();
var result = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType);
if (!result.Properties.Dictionary.TryGetValue(Tokens.RefreshToken, out string token))
var ticket = await context.Authentication.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationType);
if (!ticket.Properties.Dictionary.TryGetValue(Tokens.RefreshToken, out string token))
{
return new HttpStatusCodeResult(400);
}
var (response, principal) = await _service.AuthenticateWithRefreshTokenAsync(
issuer: new Uri(result.Identity.Claims.Select(claim => claim.Issuer).First(), UriKind.Absolute),
token: token,
cancellationToken: cancellationToken);
var result = await _service.AuthenticateWithRefreshTokenAsync(new()
{
CancellationToken = cancellationToken,
RefreshToken = token,
RegistrationId = ticket.Identity.FindFirst(Claims.Private.RegistrationId)?.Value
});
var properties = new AuthenticationProperties(result.Properties.Dictionary)
var properties = new AuthenticationProperties(ticket.Properties.Dictionary)
{
RedirectUri = null
};
properties.Dictionary[Tokens.BackchannelAccessToken] = response.AccessToken;
properties.Dictionary[Tokens.BackchannelAccessToken] = result.AccessToken;
if (!string.IsNullOrEmpty(response.RefreshToken))
if (!string.IsNullOrEmpty(result.RefreshToken))
{
properties.Dictionary[Tokens.RefreshToken] = response.RefreshToken;
properties.Dictionary[Tokens.RefreshToken] = result.RefreshToken;
}
context.Authentication.SignIn(properties, result.Identity);
context.Authentication.SignIn(properties, ticket.Identity);
return View("Index", model: response.AccessToken);
return View("Index", model: result.AccessToken);
}
}
}

4
sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs

@ -83,13 +83,13 @@ namespace OpenIddict.Sandbox.AspNet.Client
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseGitHub(options =>
.AddGitHub(options =>
{
options.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122")
.SetRedirectUri("callback/login/github");
})
.UseGoogle(options =>
.AddGoogle(options =>
{
options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com")
.SetClientSecret("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")

6
sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!--
For more information on how to configure your ASP.NET application, please visit
https://go.microsoft.com/fwlink/?LinkId=301880
@ -163,8 +163,8 @@
</runtime>
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=3.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=3.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
</compilers>
</system.codedom>
</configuration>

2
sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs

@ -71,7 +71,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseGitHub()
.AddGitHub()
.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122")
.SetRedirectUri("callback/login/github");

6
sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!--
For more information on how to configure your ASP.NET application, please visit
https://go.microsoft.com/fwlink/?LinkId=301880
@ -192,8 +192,8 @@
</entityFramework>
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=3.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=3.6.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:1659;1699;1701" />
<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=4.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:default /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
</compilers>
</system.codedom>
</configuration>

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

@ -89,16 +89,14 @@ public class AuthenticationController : Controller
// Remove the local authentication cookie before triggering a redirection to the remote server.
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// Resolve the provider of the user identifier claim stored in the local authentication cookie.
// Extract the client registration identifier and retrieve the associated server configuration.
// If the provider is known to support remote sign-out, ask OpenIddict to initiate a logout request.
if (Uri.TryCreate(identity.FindFirst(Claims.AuthorizationServer)?.Value, UriKind.Absolute, out Uri issuer) &&
await _service.GetServerConfigurationAsync(issuer) is { EndSessionEndpoint: Uri })
if (identity.FindFirst(Claims.Private.RegistrationId)?.Value is string identifier &&
await _service.GetServerConfigurationByRegistrationIdAsync(identifier) is { EndSessionEndpoint: Uri })
{
var properties = new AuthenticationProperties(new Dictionary<string, string>
{
// Note: when only one client is registered in the client options,
// setting the issuer property is not required and can be omitted.
[OpenIddictClientAspNetCoreConstants.Properties.Issuer] = issuer.AbsoluteUri,
[OpenIddictClientAspNetCoreConstants.Properties.RegistrationId] = identifier,
// While not required, the specification encourages sending an id_token_hint
// parameter containing an identity token returned by the server for this user.
@ -183,8 +181,10 @@ public class AuthenticationController : Controller
// Preserve the basic claims that are necessary for the application to work correctly.
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Preserve the identity of the authorization server as a dedicated claim.
{ Type: Claims.AuthorizationServer } => true,
// Preserve the client registration identifier as a dedicated claim so that the
// associated server configuration can be resolved from the logout endpoint to
// determine whether the authorization server supports client-initiated logouts.
{ Type: Claims.Private.RegistrationId } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "bio", Issuer: "https://github.com/" } => true,

27
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/HomeController.cs

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Client;
using static OpenIddict.Abstractions.OpenIddictConstants;
using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants;
namespace OpenIddict.Sandbox.AspNetCore.Client.Controllers;
@ -43,32 +44,34 @@ public class HomeController : Controller
[Authorize, HttpPost("~/refresh-token"), ValidateAntiForgeryToken]
public async Task<ActionResult> RefreshToken(CancellationToken cancellationToken)
{
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var token = result?.Properties.GetTokenValue(Tokens.RefreshToken);
var ticket = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
var token = ticket?.Properties.GetTokenValue(Tokens.RefreshToken);
if (string.IsNullOrEmpty(token))
{
return BadRequest();
}
var (response, principal) = await _service.AuthenticateWithRefreshTokenAsync(
issuer: new Uri(result.Principal.Claims.Select(claim => claim.Issuer).First(), UriKind.Absolute),
token: token,
cancellationToken: cancellationToken);
var result = await _service.AuthenticateWithRefreshTokenAsync(new()
{
CancellationToken = cancellationToken,
RefreshToken = token,
RegistrationId = ticket.Principal.FindFirst(Claims.Private.RegistrationId)?.Value
});
var properties = new AuthenticationProperties(result.Properties.Items)
var properties = new AuthenticationProperties(ticket.Properties.Items)
{
RedirectUri = null
};
properties.UpdateTokenValue(Tokens.BackchannelAccessToken, response.AccessToken);
properties.UpdateTokenValue(Tokens.BackchannelAccessToken, result.AccessToken);
if (!string.IsNullOrEmpty(response.RefreshToken))
if (!string.IsNullOrEmpty(result.RefreshToken))
{
properties.UpdateTokenValue(Tokens.RefreshToken, response.RefreshToken);
properties.UpdateTokenValue(Tokens.RefreshToken, result.RefreshToken);
}
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, result.Principal, properties);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, ticket.Principal, properties);
return View("Index", model: response.AccessToken);
return View("Index", model: result.AccessToken);
}
}

6
sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs

@ -117,13 +117,13 @@ public class Startup
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseGitHub(options =>
.AddGitHub(options =>
{
options.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122")
.SetRedirectUri("callback/login/github");
})
.UseGoogle(options =>
.AddGoogle(options =>
{
options.SetClientId("1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com")
.SetClientSecret("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")
@ -131,7 +131,7 @@ public class Startup
.SetAccessType("offline")
.AddScopes(Scopes.Profile);
})
.UseReddit(options =>
.AddReddit(options =>
{
options.SetClientId("vDLNqhrkwrvqHgnoBWF3og")
.SetClientSecret("Tpab28Dz0upyZLqn7AN3GFD1O-zaAw")

2
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Startup.cs

@ -89,7 +89,7 @@ public class Startup
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseGitHub()
.AddGitHub()
.SetClientId("c4ade52327b01ddacff3")
.SetClientSecret("da6bed851b75e317bf6b2cb67013679d9467c122")
.SetRedirectUri("callback/login/github");

44
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -44,43 +44,57 @@ public class InteractiveService : BackgroundService
// Resolve the server configuration and determine the type of flow
// to use depending on the supported grants and the user selection.
var configuration = await _service.GetServerConfigurationAsync(provider, cancellationToken: stoppingToken);
var configuration = await _service.GetServerConfigurationAsync(provider, stoppingToken);
if (configuration.GrantTypesSupported.Contains(GrantTypes.DeviceCode) &&
configuration.DeviceAuthorizationEndpoint is not null &&
await UseDeviceAuthorizationGrantAsync(stoppingToken))
{
// Ask OpenIddict to send a device authorization request and write
// the complete verification endpoint URI to the console output.
var response = await _service.ChallengeUsingDeviceAsync(provider, cancellationToken: stoppingToken);
if (response.VerificationUriComplete is not null)
var result = await _service.ChallengeUsingDeviceAsync(new()
{
CancellationToken = stoppingToken,
ProviderName = provider
});
if (result.VerificationUriComplete is not null)
{
AnsiConsole.MarkupLineInterpolated(
$"[yellow]Please visit [link]{response.VerificationUriComplete}[/] and confirm the displayed code is '{response.UserCode}' to complete the authentication demand.[/]");
$"[yellow]Please visit [link]{result.VerificationUriComplete}[/] and confirm the displayed code is '{result.UserCode}' to complete the authentication demand.[/]");
}
else
{
AnsiConsole.MarkupLineInterpolated(
$"[yellow]Please visit [link]{response.VerificationUri}[/] and enter '{response.UserCode}' to complete the authentication demand.[/]");
$"[yellow]Please visit [link]{result.VerificationUri}[/] and enter '{result.UserCode}' to complete the authentication demand.[/]");
}
using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
cancellationTokenSource.CancelAfter(response.ExpiresIn < TimeSpan.FromMinutes(5) ?
response.ExpiresIn : TimeSpan.FromMinutes(5));
// Wait for the user to complete the demand on the other device.
(_, principal) = await _service.AuthenticateWithDeviceAsync(provider,
response.DeviceCode, cancellationToken: cancellationTokenSource.Token);
principal = (await _service.AuthenticateWithDeviceAsync(new()
{
DeviceCode = result.DeviceCode,
Interval = result.Interval,
ProviderName = provider,
Timeout = result.ExpiresIn < TimeSpan.FromMinutes(5) ? result.ExpiresIn : TimeSpan.FromMinutes(5)
})).Principal;
}
else
{
AnsiConsole.MarkupLine("[cyan]Launching the system browser.[/]");
// Ask OpenIddict to initiate the authentication flow (typically, by
// starting the system browser) and wait for the user to complete it.
(_, _, principal) = await _service.AuthenticateInteractivelyAsync(
provider, cancellationToken: stoppingToken);
// Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser).
var result = await _service.ChallengeInteractivelyAsync(new()
{
CancellationToken = stoppingToken,
ProviderName = provider
});
// Wait for the user to complete the authorization process.
principal = (await _service.AuthenticateInteractivelyAsync(new()
{
Nonce = result.Nonce
})).Principal;
}
AnsiConsole.MarkupLine("[green]Authentication successful:[/]");

23
sandbox/OpenIddict.Sandbox.Console.Client/Program.cs

@ -80,17 +80,18 @@ var host = new HostBuilder()
// address per provider, unless all the registered providers support returning an "iss"
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders(options =>
{
options.UseGitHub()
.SetClientId("992372d088f8676a7945")
.SetClientSecret("1f18c22f766e44d7bd4ea4a6510b9e337d48ab38")
.SetRedirectUri("callback/login/github");
options.UseTwitter()
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetRedirectUri("callback/login/twitter");
});
options.UseWebProviders()
.AddGitHub(options =>
{
options.SetClientId("992372d088f8676a7945")
.SetClientSecret("1f18c22f766e44d7bd4ea4a6510b9e337d48ab38")
.SetRedirectUri("callback/login/github");
})
.AddTwitter(options =>
{
options.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetRedirectUri("callback/login/twitter");
});
});
// Register the worker responsible for creating the database used to store tokens

16
sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs

@ -29,10 +29,18 @@ public partial class MainForm : Form, IWinFormsShell
try
{
// Ask OpenIddict to initiate the authentication flow (typically, by
// starting the system browser) and wait for the user to complete it.
var (_, _, principal) = await _service.AuthenticateInteractivelyAsync(
provider, cancellationToken: source.Token);
// Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser).
var result = await _service.ChallengeInteractivelyAsync(new()
{
CancellationToken = source.Token,
ProviderName = provider
});
// Wait for the user to complete the authorization process.
var principal = (await _service.AuthenticateInteractivelyAsync(new()
{
Nonce = result.Nonce
})).Principal;
#if SUPPORTS_WINFORMS_TASK_DIALOG
TaskDialog.ShowDialog(new TaskDialogPage

2
sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs

@ -81,7 +81,7 @@ var host = new HostBuilder()
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseGitHub()
.AddGitHub()
.SetClientId("cf8efb4d76c0cb7109d3")
.SetClientSecret("e8c0f6b869164411bb9052e42414cbcc52d518cd")
// Note: GitHub doesn't support the recommended ":/" syntax and requires using "://".

16
sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs

@ -30,10 +30,18 @@ public partial class MainWindow : Window, IWpfShell
try
{
// Ask OpenIddict to initiate the authentication flow (typically, by
// starting the system browser) and wait for the user to complete it.
var (_, _, principal) = await _service.AuthenticateInteractivelyAsync(
provider, cancellationToken: source.Token);
// Ask OpenIddict to initiate the authentication flow (typically, by starting the system browser).
var result = await _service.ChallengeInteractivelyAsync(new()
{
CancellationToken = source.Token,
ProviderName = provider
});
// Wait for the user to complete the authorization process.
var principal = (await _service.AuthenticateInteractivelyAsync(new()
{
Nonce = result.Nonce
})).Principal;
MessageBox.Show($"Welcome, {principal.FindFirst(Claims.Name)!.Value}.",
"Authentication successful", MessageBoxButton.OK, MessageBoxImage.Information);

2
sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs

@ -82,7 +82,7 @@ var host = new HostBuilder()
// parameter containing their URL as part of authorization responses. For more information,
// see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.4.
options.UseWebProviders()
.UseGitHub()
.AddGitHub()
.SetClientId("8abc54b6d5f4e39d78aa")
.SetClientSecret("f37ef38bdb18a0f5f2d430a8edbed4353c012dc3")
// Note: GitHub doesn't support the recommended ":/" syntax and requires using "://".

1
src/OpenIddict.Abstractions/OpenIddictConstants.cs

@ -138,6 +138,7 @@ public static class OpenIddictConstants
public const string Presenter = "oi_prst";
public const string RedirectUri = "oi_reduri";
public const string RefreshTokenLifetime = "oi_reft_lft";
public const string RegistrationId = "oi_reg_id";
public const string Resource = "oi_rsrc";
public const string ResponseType = "oi_rsp_typ";
public const string SigningAlgorithm = "oi_sign_alg";

41
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -579,7 +579,7 @@ Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.A
Reference the 'OpenIddict.Validation.SystemNetHttp' package and call 'services.AddOpenIddict().AddValidation().UseSystemNetHttp()' to register the default System.Net.Http-based integration.</value>
</data>
<data name="ID0136" xml:space="preserve">
<value>The issuer must be provided and must be an absolute URI.</value>
<value>The issuer must be a valid absolute URI.</value>
</data>
<data name="ID0137" xml:space="preserve">
<value>The issuer cannot contain a fragment or a query string.</value>
@ -1177,7 +1177,7 @@ To apply redirection responses, create a class implementing 'IOpenIddictClientHa
<value>No client registration was found in the client options. To add a registration, use 'services.AddOpenIddict().AddClient().AddRegistration()'.</value>
</data>
<data name="ID0305" xml:space="preserve">
<value>No issuer was specified in the challenge properties. When multiple clients are registered, an issuer (or a provider name) must be specified in the challenge properties.</value>
<value>No client registration information was specified in the challenge properties. When multiple clients are registered, an issuer, a provider name or a client registration identifier must be specified in the challenge properties.</value>
</data>
<data name="ID0306" xml:space="preserve">
<value>The specified issuer is not a valid or absolute URI.</value>
@ -1290,7 +1290,7 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<value>The mandatory '{0}' setting required by the {1} provider integration must be set.</value>
</data>
<data name="ID0333" xml:space="preserve">
<value>The '{0}' provider settings cannot be resolved from the event context. Make sure the provider was correctly registered using 'services.AddOpenIddict().AddClient().UseWebProviders().Add{0}()'.</value>
<value>The '{0}' provider settings cannot be resolved from the client registration. Make sure the provider was correctly registered using 'services.AddOpenIddict().AddClient().UseWebProviders().Add{0}()'.</value>
</data>
<data name="ID0334" xml:space="preserve">
<value>The '{0}' node cannot be extracted from the response or is not of the expected type.</value>
@ -1314,7 +1314,7 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<value>The endpoint type associated with the state token cannot be resolved.</value>
</data>
<data name="ID0341" xml:space="preserve">
<value>No issuer was specified in the sign-out properties. When multiple clients are registered, an issuer (or a provider name) must be specified in the sign-out properties.</value>
<value>No client registration information was specified in the sign-out properties. When multiple clients are registered, an issuer, a provider name or a client registration identifier must be specified in the sign-out properties.</value>
</data>
<data name="ID0342" xml:space="preserve">
<value>The same issuer cannot be used in multiple client registrations.</value>
@ -1332,13 +1332,13 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<value>The PEM-encoded key cannot be empty.</value>
</data>
<data name="ID0347" xml:space="preserve">
<value>The same provider name cannot be used in multiple client registrations.</value>
<value>The same registration identifier cannot be used in multiple client registrations. When multiple client registrations use the same issuer or the same provider name, an explicit identifier must be attached to each client registration. To attach a registration identifier, set 'OpenIddictClientRegistration.RegistrationId' (for a custom client registration) or use 'options.Add[Provider](options =&gt; options.SetRegistrationId())' (for a web provider integration).</value>
</data>
<data name="ID0348" xml:space="preserve">
<value>The issuer corresponding to the specified provider name cannot be found in the client options.</value>
<value>The specified client registration identifier doesn't match the identifier of the resolved client registration.</value>
</data>
<data name="ID0349" xml:space="preserve">
<value>The issuer associated with the resolved client registration doesn't match the specified provider name.</value>
<value>The specified provider name doesn't match the provider name associated with the resolved client registration.</value>
</data>
<data name="ID0350" xml:space="preserve">
<value>The '{0}' setting required by the {1} provider integration must be a valid absolute URI.</value>
@ -1519,6 +1519,33 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0402" xml:space="preserve">
<value>The grant type '{0}' is not supported by the ASP.NET Core and OWIN integrations.</value>
</data>
<data name="ID0403" xml:space="preserve">
<value>This API is no longer supported and will be removed in a future version.</value>
</data>
<data name="ID0404" xml:space="preserve">
<value>The specified issuer is used in multiple client registrations. To select a specific client registration, specify its identifier.</value>
</data>
<data name="ID0405" xml:space="preserve">
<value>The issuer must be provided for custom client registrations and must be a valid absolute URI. If the client registration is expected to use a provider integration provided by the OpenIddict.Client.WebIntegration package, make sure the 'OpenIddictClientRegistration.ProviderType' property is correctly set.</value>
</data>
<data name="ID0406" xml:space="preserve">
<value>The provider settings attached to the client registration are missing or of an incorrect type. When manually adding client registrations that depend on a web provider integration provided by OpenIddict.Client.WebIntegration, make sure the 'OpenIddictClientRegistration.ProviderSettings' property is populated with a instance of the correct provider settings.</value>
</data>
<data name="ID0407" xml:space="preserve">
<value>The specified provider type is not valid or is not supported by this version of the OpenIddict.Client.WebIntegration package.</value>
</data>
<data name="ID0408" xml:space="preserve">
<value>The specified issuer doesn't match the issuer associated with the resolved client registration.</value>
</data>
<data name="ID0409" xml:space="preserve">
<value>The specified provider name is used in multiple client registrations. To select a specific client registration, specify its identifier.</value>
</data>
<data name="ID0410" xml:space="preserve">
<value>The client registration corresponding to the specified identifier cannot be found in the client options.</value>
</data>
<data name="ID0411" xml:space="preserve">
<value>The issuer couldn't be resolved from the provider configuration or is not a valid absolute URI. Make sure the OpenIddict.Client.WebIntegration package is referenced and 'options.UseWebProviders()' is correctly called.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

1
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreConstants.cs

@ -26,6 +26,7 @@ public static class OpenIddictClientAspNetCoreConstants
public const string ErrorUri = ".error_uri";
public const string ProviderName = ".provider_name";
public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string RegistrationId = ".registration_id";
public const string StateTokenPrincipal = ".state_token_principal";
public const string UserinfoTokenPrincipal = ".userinfo_token_principal";
}

9
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs

@ -148,7 +148,7 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
else
{
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// A single main claims-based principal instance can be attached to an authentication ticket.
// To return the most appropriate one, the principal is selected based on the endpoint type.
@ -173,9 +173,10 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Op
return AuthenticateResult.NoResult();
}
// Attach the identity of the authorization server to the returned principal to allow resolving it even if no other
// claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri)
// Attach the registration identifier and identity of the authorization server to the returned principal to allow
// resolving it even if no other claim was added (e.g if no id_token was returned/no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
// Restore or create a new authentication properties collection and populate it.

14
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandlers.cs

@ -564,6 +564,13 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is { Items.Count: > 0 })
{
// If a registration identifier was explicitly set, update the challenge context to use it.
if (properties.Items.TryGetValue(Properties.RegistrationId, out string? identifier) &&
!string.IsNullOrEmpty(identifier))
{
context.RegistrationId = identifier;
}
// If an issuer was explicitly set, update the challenge context to use it.
if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer))
{
@ -797,6 +804,13 @@ public static partial class OpenIddictClientAspNetCoreHandlers
var properties = context.Transaction.GetProperty<AuthenticationProperties>(typeof(AuthenticationProperties).FullName!);
if (properties is { Items.Count: > 0 })
{
// If a registration identifier was explicitly set, update the sign-out context to use it.
if (properties.Items.TryGetValue(Properties.RegistrationId, out string? identifier) &&
!string.IsNullOrEmpty(identifier))
{
context.RegistrationId = identifier;
}
// If an issuer was explicitly set, update the sign-out context to use it.
if (properties.Items.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer))
{

1
src/OpenIddict.Client.Owin/OpenIddictClientOwinConstants.cs

@ -35,6 +35,7 @@ public static class OpenIddictClientOwinConstants
public const string ErrorUri = ".error_uri";
public const string ProviderName = ".provider_name";
public const string RefreshTokenPrincipal = ".refresh_token_principal";
public const string RegistrationId = ".registration_id";
public const string StateTokenPrincipal = ".state_token_principal";
public const string UserinfoTokenPrincipal = ".userinfo_token_principal";
}

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

@ -166,7 +166,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
else
{
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// A single main claims-based principal instance can be attached to an authentication ticket.
var principal = context.EndpointType switch
@ -188,9 +188,10 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<OpenIddi
return null;
}
// Attach the identity of the authorization server to the returned principal to allow resolving it even if no other
// claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri)
// Attach the registration identifier and identity of the authorization server to the returned principal to allow
// resolving it even if no other claim was added (e.g if no id_token was returned/no userinfo endpoint is available).
principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
// Restore or create a new authentication properties collection and populate it.

14
src/OpenIddict.Client.Owin/OpenIddictClientOwinHandlers.cs

@ -576,6 +576,13 @@ public static partial class OpenIddictClientOwinHandlers
return default;
}
// If a registration identifier was explicitly set, update the challenge context to use it.
if (properties.Dictionary.TryGetValue(Properties.RegistrationId, out string? identifier) &&
!string.IsNullOrEmpty(identifier))
{
context.RegistrationId = identifier;
}
// If an issuer was explicitly set, update the challenge context to use it.
if (properties.Dictionary.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer))
{
@ -835,6 +842,13 @@ public static partial class OpenIddictClientOwinHandlers
return default;
}
// If a registration identifier was explicitly set, update the sign-out context to use it.
if (properties.Dictionary.TryGetValue(Properties.RegistrationId, out string? identifier) &&
!string.IsNullOrEmpty(identifier))
{
context.RegistrationId = identifier;
}
// If an issuer was explicitly set, update the challenge context to use it.
if (properties.Dictionary.TryGetValue(Properties.Issuer, out string? issuer) && !string.IsNullOrEmpty(issuer))
{

1
src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj

@ -13,7 +13,6 @@
netstandard2.0
</TargetFrameworks>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>

6
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs

@ -724,14 +724,14 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019));
(context.Issuer, context.Configuration, context.Registration) = context.EndpointType switch
(context.Configuration, context.Registration) = context.EndpointType switch
{
// When the authentication context is marshalled, restore the
// issuer registration and configuration from the other instance.
OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification)
=> (notification.Issuer, notification.Configuration, notification.Registration),
=> (notification.Configuration, notification.Registration),
_ => (context.Issuer, context.Configuration, context.Registration)
_ => (context.Configuration, context.Registration)
};
return default;

100
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpBuilder.cs

@ -8,6 +8,7 @@ using System.ComponentModel;
using System.Net.Http.Headers;
using System.Net.Mail;
using System.Reflection;
using OpenIddict.Client;
using OpenIddict.Client.SystemNetHttp;
using Polly;
@ -53,7 +54,7 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
/// Configures the <see cref="HttpClient"/> used by the OpenIddict client/System.Net.Http integration.
/// </summary>
/// <remarks>
/// Note: customizations configured using this method apply to all providers.
/// Note: customizations configured using this method apply to all client registrations.
/// </remarks>
/// <param name="configuration">The delegate used to configure the <see cref="HttpClient"/>.</param>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
@ -65,25 +66,15 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options =>
{
if (options.HttpClientActions.TryGetValue(string.Empty, out var actions))
{
actions.Add(configuration);
}
else
{
options.HttpClientActions[string.Empty] = new(capacity: 1) { configuration };
}
});
return ConfigureHttpClient((registration, client) => configuration(client));
}
/// <summary>
/// Configures the <see cref="HttpClient"/> used by the OpenIddict client/System.Net.Http integration.
/// </summary>
/// <remarks>
/// Note: customizations configured using this method only apply to the specified provider.
/// Note: customizations configured using this method only apply
/// to client registrations that use the specified provider name.
/// </remarks>
/// <param name="provider">The provider name, to which the customizations are applied.</param>
/// <param name="configuration">The delegate used to configure the <see cref="HttpClient"/>.</param>
@ -101,25 +92,40 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options =>
return ConfigureHttpClient((registration, client) =>
{
if (options.HttpClientActions.TryGetValue(provider, out var actions))
if (string.Equals(registration.ProviderName, provider, StringComparison.Ordinal))
{
actions.Add(configuration);
}
else
{
options.HttpClientActions[provider] = new(capacity: 1) { configuration };
configuration(client);
}
});
}
/// <summary>
/// Configures the <see cref="HttpClient"/> used by the OpenIddict client/System.Net.Http integration.
/// </summary>
/// <remarks>
/// Note: customizations configured using this method apply to all client registrations, but the
/// configuration delegate can restrict the applied customizations to specific instances.
/// </remarks>
/// <param name="configuration">The delegate used to configure the <see cref="HttpClient"/>.</param>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientSystemNetHttpBuilder ConfigureHttpClient(Action<OpenIddictClientRegistration, HttpClient> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options => options.UnfilteredHttpClientActions.Add(configuration));
}
/// <summary>
/// Configures the <see cref="HttpClientHandler"/> used by the OpenIddict client/System.Net.Http integration.
/// </summary>
/// <remarks>
/// Note: customizations configured using this method apply to all providers.
/// Note: customizations configured using this method apply to all client registrations.
/// </remarks>
/// <param name="configuration">The delegate used to configure the <see cref="HttpClientHandler"/>.</param>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
@ -131,25 +137,15 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options =>
{
if (options.HttpClientHandlerActions.TryGetValue(string.Empty, out var actions))
{
actions.Add(configuration);
}
else
{
options.HttpClientHandlerActions[string.Empty] = new(capacity: 1) { configuration };
}
});
return ConfigureHttpClientHandler((registration, handler) => configuration(handler));
}
/// <summary>
/// Configures the <see cref="HttpClientHandler"/> used by the OpenIddict client/System.Net.Http integration.
/// </summary>
/// <remarks>
/// Note: customizations configured using this method only apply to the specified provider.
/// Note: customizations configured using this method only apply
/// to client registrations that use the specified provider name.
/// </remarks>
/// <param name="provider">The provider name, to which the customizations are applied.</param>
/// <param name="configuration">The delegate used to configure the <see cref="HttpClientHandler"/>.</param>
@ -167,20 +163,36 @@ public sealed class OpenIddictClientSystemNetHttpBuilder
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options =>
return ConfigureHttpClientHandler((registration, handler) =>
{
if (options.HttpClientHandlerActions.TryGetValue(provider, out var actions))
if (string.Equals(registration.ProviderName, provider, StringComparison.Ordinal))
{
actions.Add(configuration);
}
else
{
options.HttpClientHandlerActions[provider] = new(capacity: 1) { configuration };
configuration(handler);
}
});
}
/// <summary>
/// Configures the <see cref="HttpClientHandler"/> used by the OpenIddict client/System.Net.Http integration.
/// </summary>
/// <remarks>
/// Note: customizations configured using this method apply to all client registrations, but the
/// configuration delegate can restrict the applied customizations to specific instances.
/// </remarks>
/// <param name="configuration">The delegate used to configure the <see cref="HttpClientHandler"/>.</param>
/// <returns>The <see cref="OpenIddictClientSystemNetHttpBuilder"/> instance.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientSystemNetHttpBuilder ConfigureHttpClientHandler(
Action<OpenIddictClientRegistration, HttpClientHandler> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
return Configure(options => options.UnfilteredHttpClientHandlerActions.Add(configuration));
}
/// <summary>
/// Sets the contact address used in the "From" header that is attached
/// to the backchannel HTTP requests sent to the authorization server.

44
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs

@ -5,6 +5,7 @@
*/
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
@ -52,12 +53,18 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
}
// Only amend the HTTP client factory options if the instance is managed by OpenIddict.
var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
if (string.IsNullOrEmpty(name) || !name.StartsWith(assembly.Name!, StringComparison.Ordinal))
if (string.IsNullOrEmpty(name) || !TryResolveRegistrationId(name, out string? identifier))
{
return;
}
var service = _provider.GetRequiredService<OpenIddictClientService>();
// Note: while the client registration should be returned synchronously in most cases,
// the retrieval is always offloaded to the thread pool to help prevent deadlocks when
// the waiting is blocking and the operation is executed in a synchronization context.
var registration = Task.Run(async () => await service.GetClientRegistrationByIdAsync(identifier)).GetAwaiter().GetResult();
var settings = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientSystemNetHttpOptions>>().CurrentValue;
options.HttpClientActions.Add(client =>
@ -70,12 +77,9 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
});
// Register the user-defined HTTP client actions.
foreach (var action in settings.HttpClientActions
.Where(action => string.IsNullOrEmpty(action.Key) || // Note: actions that have an empty key apply to all providers.
action.Key.AsSpan().Equals(name.AsSpan(assembly.Name!.Length + 1), StringComparison.Ordinal))
.SelectMany(action => action.Value))
foreach (var action in settings.UnfilteredHttpClientActions)
{
options.HttpClientActions.Add(action);
options.HttpClientActions.Add(client => action(registration, client));
}
options.HttpMessageHandlerBuilderActions.Add(builder =>
@ -109,13 +113,27 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
});
// Register the user-defined HTTP client handler actions.
foreach (var action in settings.HttpClientHandlerActions
.Where(action => string.IsNullOrEmpty(action.Key) || // Note: actions that have an empty key apply to all providers.
action.Key.AsSpan().Equals(name.AsSpan(assembly.Name!.Length + 1), StringComparison.Ordinal))
.SelectMany(action => action.Value))
foreach (var action in settings.UnfilteredHttpClientHandlerActions)
{
options.HttpMessageHandlerBuilderActions.Add(builder => action(registration,
builder.PrimaryHandler as HttpClientHandler ??
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName))));
}
static bool TryResolveRegistrationId(string name, [NotNullWhen(true)] out string? value)
{
options.HttpMessageHandlerBuilderActions.Add(builder => action(builder.PrimaryHandler as HttpClientHandler ??
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName))));
var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
if (!name.StartsWith(assembly.Name!, StringComparison.Ordinal) ||
name.Length < assembly.Name!.Length + 1 ||
name[assembly.Name.Length] is not ':')
{
value = null;
return false;
}
value = name[(assembly.Name.Length + 1)..];
return true;
}
}
}

3
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -57,8 +57,7 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
}
var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
var name = !string.IsNullOrEmpty(context.Registration.ProviderName) ?
$"{assembly.Name}:{context.Registration.ProviderName}" : assembly.Name!;
var name = $"{assembly.Name}:{context.Registration.RegistrationId}";
// Create and store the HttpClient in the transaction properties.
context.Transaction.SetProperty(typeof(HttpClient).FullName!, _factory.CreateClient(name) ??

14
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpOptions.cs

@ -41,11 +41,25 @@ public sealed class OpenIddictClientSystemNetHttpOptions
/// Gets the user-defined actions used to amend the <see cref="HttpClient"/>
/// instances created by the OpenIddict client/System.Net.Http integration.
/// </summary>
public List<Action<OpenIddictClientRegistration, HttpClient>> UnfilteredHttpClientActions { get; } = new();
/// <summary>
/// Gets the user-defined actions used to amend the <see cref="HttpClientHandler"/>
/// instances created by the OpenIddict client/System.Net.Http integration.
/// </summary>
public List<Action<OpenIddictClientRegistration, HttpClientHandler>> UnfilteredHttpClientHandlerActions { get; } = new();
/// <summary>
/// Gets the user-defined actions used to amend the <see cref="HttpClient"/>
/// instances created by the OpenIddict client/System.Net.Http integration.
/// </summary>
[Obsolete($"This property was replaced by {nameof(UnfilteredHttpClientActions)} and will be removed in a future version.")]
public Dictionary<string, List<Action<HttpClient>>> HttpClientActions { get; } = new();
/// <summary>
/// Gets the user-defined actions used to amend the <see cref="HttpClientHandler"/>
/// instances created by the OpenIddict client/System.Net.Http integration.
/// </summary>
[Obsolete($"This property was replaced by {nameof(UnfilteredHttpClientActions)} and will be removed in a future version.")]
public Dictionary<string, List<Action<HttpClientHandler>>> HttpClientHandlerActions { get; } = new();
}

109
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs

@ -6,7 +6,6 @@
using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using OpenIddict.Client.SystemNetHttp;
@ -19,18 +18,25 @@ namespace OpenIddict.Client.WebIntegration;
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfigureOptions<OpenIddictClientOptions>,
IConfigureNamedOptions<HttpClientFactoryOptions>
IConfigureOptions<OpenIddictClientSystemNetHttpOptions>,
IConfigureNamedOptions<HttpClientFactoryOptions>,
IPostConfigureOptions<OpenIddictClientOptions>
{
#if !SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWebIntegrationConfiguration"/> class.
/// </summary>
public OpenIddictClientWebIntegrationConfiguration()
{
}
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWebIntegrationConfiguration"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
[Obsolete("This constructor is no longer supported and will be removed in a future version.")]
public OpenIddictClientWebIntegrationConfiguration(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
#endif
{
}
/// <inheritdoc/>
public void Configure(OpenIddictClientOptions options)
@ -45,58 +51,65 @@ public sealed partial class OpenIddictClientWebIntegrationConfiguration : IConfi
}
/// <inheritdoc/>
public void Configure(HttpClientFactoryOptions options) => Configure(Options.DefaultName, options);
/// <inheritdoc/>
public void Configure(string? name, HttpClientFactoryOptions options)
public void Configure(OpenIddictClientSystemNetHttpOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Only amend the HTTP client factory options if the instance is managed by OpenIddict
// and contains the name of a provider managed by OpenIddict.Client.WebIntegration.
var assembly = typeof(OpenIddictClientSystemNetHttpOptions).Assembly.GetName();
if (string.IsNullOrEmpty(name) || !name.StartsWith(assembly.Name!, StringComparison.Ordinal) ||
name.Length < assembly.Name!.Length + 1 || name[assembly.Name.Length] is not ':')
options.UnfilteredHttpClientHandlerActions.Add(static (registration, handler) =>
{
// Note: while not enforced yet, Pro Santé Connect's specification requires sending a TLS
// client certificate when communicating with its backchannel OpenID Connect endpoints.
//
// For that, the primary HTTP handler must be altered or replaced by an instance that
// includes the client certificate set in the options in its certificate collection.
//
// For more information, see EXI PSC 24 in the annex part of
// https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000045551195.
if (registration.ProviderType is ProviderTypes.ProSantéConnect &&
registration.GetProSantéConnectSettings() is { ClientCertificate: X509Certificate2 certificate })
{
handler.ClientCertificates.Add(certificate);
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
}
});
}
/// <inheritdoc/>
[Obsolete("This method is no longer supported and will be removed in a future version.", error: true)]
public void Configure(HttpClientFactoryOptions options)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <inheritdoc/>
[Obsolete("This method is no longer supported and will be removed in a future version.", error: true)]
public void Configure(string? name, HttpClientFactoryOptions options)
=> throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientOptions options)
{
if (options is null)
{
return;
throw new ArgumentNullException(nameof(options));
}
// Note: while not enforced yet, Pro Santé Connect's specification requires sending a TLS
// client certificate when communicating with its backchannel OpenID Connect endpoints.
//
// For that, the primary HTTP handler must be altered or replaced by an instance that
// includes the client certificate set in the options in its certificate collection.
//
// For more information, see EXI PSC 24 in the annex part of
// https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000045551195.
if (name.AsSpan(assembly.Name.Length + 1) is Providers.ProSantéConnect)
options.Registrations.ForEach(static registration =>
{
options.HttpMessageHandlerBuilderActions.Add(builder =>
// If the client registration has a provider type attached, apply
// the configuration logic corresponding to the specified provider.
if (!string.IsNullOrEmpty(registration.ProviderType))
{
// Note: the client registration instance is not available here,
// so the provider options must be resolved via the DI container.
#if SUPPORTS_SERVICE_PROVIDER_IN_HTTP_MESSAGE_HANDLER_BUILDER
var options = builder.Services.GetRequiredService<IOptionsMonitor<
OpenIddictClientWebIntegrationOptions.ProSantéConnect>>().CurrentValue;
#else
var options = _provider.GetRequiredService<IOptionsMonitor<
OpenIddictClientWebIntegrationOptions.ProSantéConnect>>().CurrentValue;
#endif
if (builder.PrimaryHandler is not HttpClientHandler handler)
{
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName));
}
// If a client certificate was specified, update the HTTP handler to use it.
if (options.ClientCertificate is X509Certificate certificate)
{
handler.ClientCertificates.Add(certificate);
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
}
});
}
ConfigureProvider(registration);
}
});
}
/// <summary>
/// Amends the registration with the provider-specific configuration logic.
/// </summary>
/// <param name="registration">The client registration.</param>
// Note: the implementation of this method is automatically generated by the source generator.
static partial void ConfigureProvider(OpenIddictClientRegistration registration);
}

17
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs

@ -5,9 +5,9 @@
*/
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using OpenIddict.Client;
using OpenIddict.Client.SystemNetHttp;
using OpenIddict.Client.WebIntegration;
namespace Microsoft.Extensions.DependencyInjection;
@ -15,7 +15,7 @@ namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Exposes extensions allowing to register the OpenIddict client Web integration services.
/// </summary>
public static class OpenIddictClientWebIntegrationExtensions
public static partial class OpenIddictClientWebIntegrationExtensions
{
/// <summary>
/// Registers the OpenIddict client Web integration services in the DI container.
@ -42,9 +42,20 @@ public static class OpenIddictClientWebIntegrationExtensions
builder.Services.TryAddEnumerable(new[]
{
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration>(),
ServiceDescriptor.Singleton<IConfigureOptions<HttpClientFactoryOptions>, OpenIddictClientWebIntegrationConfiguration>()
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientSystemNetHttpOptions>, OpenIddictClientWebIntegrationConfiguration>()
});
// Note: the IPostConfigureOptions<OpenIddictClientOptions> service responsible for populating
// the client registrations MUST be registered before OpenIddictClientConfiguration to ensure
// the registrations are correctly populated before being validated.
if (!builder.Services.Any(static descriptor =>
descriptor.ServiceType == typeof(IPostConfigureOptions<OpenIddictClientOptions>) &&
descriptor.ImplementationType == typeof(OpenIddictClientWebIntegrationConfiguration)))
{
builder.Services.Insert(0, ServiceDescriptor.Singleton<
IPostConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration>());
}
return new OpenIddictClientWebIntegrationBuilder(builder.Services);
}

2
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Authentication.cs

@ -49,7 +49,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// corresponds to the legacy OAuth 2.0-only implicit flow, it is deliberately not
// supported, so the only supported value is "web_server" (aka authorization code flow).
if (context.Registration.ProviderName is Providers.Basecamp)
if (context.Registration.ProviderType is ProviderTypes.Basecamp)
{
context.Request["type"] = "web_server";
context.Request.ResponseType = null;

2
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Device.cs

@ -50,7 +50,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Note: Google doesn't return a standard "verification_uri" parameter
// but returns a custom "verification_url" that serves the same purpose.
if (context.Registration.ProviderName is Providers.Google)
if (context.Registration.ProviderType is ProviderTypes.Google)
{
context.Response[Parameters.VerificationUri] = context.Response["verification_url"];
context.Response["verification_url"] = null;

26
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs

@ -49,7 +49,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.Response[Metadata.Issuer] = context.Registration.ProviderName switch
context.Response[Metadata.Issuer] = context.Registration.ProviderType switch
{
// Note: the server configuration metadata returned by the Microsoft Account special tenants
// uses "https://login.microsoftonline.com/{tenantid}/v2.0" as the issuer to indicate that
@ -60,7 +60,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
//
// For more information about the special tenants supported by Microsoft Account/Azure AD, see
// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri.
Providers.Microsoft when context.Registration.GetMicrosoftOptions() is { Tenant: string tenant } =>
ProviderTypes.Microsoft when context.Registration.GetMicrosoftSettings() is { Tenant: string tenant } =>
string.Equals(tenant, "common", StringComparison.OrdinalIgnoreCase) ? "https://login.microsoftonline.com/common/v2.0" :
string.Equals(tenant, "consumers", StringComparison.OrdinalIgnoreCase) ? "https://login.microsoftonline.com/consumers/v2.0" :
string.Equals(tenant, "organizations", StringComparison.OrdinalIgnoreCase) ? "https://login.microsoftonline.com/organizations/v2.0" :
@ -101,25 +101,25 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// authorization code or implicit flows). To work around that, the list of supported grant
// types is amended to include the known supported types for the providers that require it.
if (context.Registration.ProviderName is Providers.Apple or Providers.LinkedIn or Providers.QuickBooksOnline)
if (context.Registration.ProviderType is ProviderTypes.Apple or ProviderTypes.LinkedIn or ProviderTypes.QuickBooksOnline)
{
context.Configuration.GrantTypesSupported.Add(GrantTypes.AuthorizationCode);
context.Configuration.GrantTypesSupported.Add(GrantTypes.RefreshToken);
}
else if (context.Registration.ProviderName is Providers.Cognito or Providers.EpicGames or Providers.Microsoft)
else if (context.Registration.ProviderType is ProviderTypes.Cognito or ProviderTypes.EpicGames or ProviderTypes.Microsoft)
{
context.Configuration.GrantTypesSupported.Add(GrantTypes.AuthorizationCode);
context.Configuration.GrantTypesSupported.Add(GrantTypes.ClientCredentials);
context.Configuration.GrantTypesSupported.Add(GrantTypes.RefreshToken);
}
else if (context.Registration.ProviderName is Providers.Google)
else if (context.Registration.ProviderType is ProviderTypes.Google)
{
context.Configuration.GrantTypesSupported.Add(GrantTypes.Implicit);
}
else if (context.Registration.ProviderName is Providers.Asana or Providers.Slack)
else if (context.Registration.ProviderType is ProviderTypes.Asana or ProviderTypes.Slack)
{
context.Configuration.GrantTypesSupported.Add(GrantTypes.RefreshToken);
}
@ -156,7 +156,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// doesn't list them in the server configuration metadata. To ensure the OpenIddict
// client uses Proof Key for Code Exchange for the Microsoft provider, the 2 methods
// are manually added to the list of supported code challenge methods by this handler.
if (context.Registration.ProviderName is Providers.Microsoft)
if (context.Registration.ProviderType is ProviderTypes.Microsoft)
{
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Plain);
context.Configuration.CodeChallengeMethodsSupported.Add(CodeChallengeMethods.Sha256);
@ -192,7 +192,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// While it is a recommended node, some providers don't include "scopes_supported" in their
// configuration and thus are treated as OAuth 2.0-only providers by the OpenIddict client.
// To avoid that, the "openid" scope is manually added to indicate OpenID Connect is supported.
if (context.Registration.ProviderName is Providers.EpicGames or Providers.Xero)
if (context.Registration.ProviderType is ProviderTypes.EpicGames or ProviderTypes.Xero)
{
context.Configuration.ScopesSupported.Add(Scopes.OpenId);
}
@ -230,7 +230,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// generic "invalid_request" request when using "client_secret_basic" instead of
// sending the client identifier in the request form. To work around this limitation,
// "client_secret_post" is listed as the only supported client authentication method.
if (context.Registration.ProviderName is Providers.Google)
if (context.Registration.ProviderType is ProviderTypes.Google)
{
context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Clear();
context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Add(
@ -272,7 +272,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// is the same as private_key_jwt, the configuration is amended to assume Apple supports
// private_key_jwt and an event handler is responsible for populating the client_secret
// parameter using the client assertion token once it has been generated by OpenIddict.
if (context.Registration.ProviderName is Providers.Apple)
if (context.Registration.ProviderType is ProviderTypes.Apple)
{
context.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.PrivateKeyJwt);
@ -282,7 +282,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// doesn't return a "token_endpoint_auth_methods_supported" node containing alternative
// authentication methods, making basic authentication the default authentication method.
// To work around this compliance issue, "client_secret_post" is manually added here.
else if (context.Registration.ProviderName is Providers.LinkedIn)
else if (context.Registration.ProviderType is ProviderTypes.LinkedIn)
{
context.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.ClientSecretPost);
@ -319,8 +319,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// by the sandbox environment always contains the production endpoints, which would
// prevent the OpenIddict integration from working properly when using the sandbox mode.
// To work around that, the endpoints are manually overriden when this environment is used.
if (context.Registration.ProviderName is Providers.PayPal &&
context.Registration.GetPayPalOptions() is { Environment: string environment } &&
if (context.Registration.ProviderType is ProviderTypes.PayPal &&
context.Registration.GetPayPalSettings() is { Environment: string environment } &&
string.Equals(environment, PayPal.Environments.Sandbox, StringComparison.OrdinalIgnoreCase))
{
context.Configuration.AuthorizationEndpoint =

16
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs

@ -65,7 +65,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// didn't support the "response_type" parameter but relied on a "type"
// parameter to determine the type of request (web server or refresh).
if (context.Registration.ProviderName is Providers.Basecamp)
if (context.Registration.ProviderType is ProviderTypes.Basecamp)
{
context.Request["type"] = context.Request.GrantType switch
{
@ -121,7 +121,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// These providers require using basic authentication to flow the client_id
// for all types of client applications, even when there's no client_secret.
if (context.Registration.ProviderName is Providers.Reddit &&
if (context.Registration.ProviderType is ProviderTypes.Reddit &&
!string.IsNullOrEmpty(context.Request.ClientId))
{
// Important: the credentials MUST be formURL-encoded before being base64-encoded.
@ -140,7 +140,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// These providers don't implement the standard version of the client_secret_basic
// authentication method as they don't support formURL-encoding the client credentials.
else if (context.Registration.ProviderName is Providers.EpicGames &&
else if (context.Registration.ProviderType is ProviderTypes.EpicGames &&
!string.IsNullOrEmpty(context.Request.ClientId) &&
!string.IsNullOrEmpty(context.Request.ClientSecret))
{
@ -196,7 +196,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Trovo requires sending the client identifier in a non-standard "client-id" header and
// the client secret in the payload (formatted using JSON instead of the standard format).
if (context.Registration.ProviderName is Providers.Trovo)
if (context.Registration.ProviderType is ProviderTypes.Trovo)
{
request.Headers.Add("Client-ID", context.Request.ClientId);
@ -246,7 +246,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// By default, Deezer returns non-standard token responses formatted as formurl-encoded
// payloads and declared as "text/html" content but allows sending an "output" query string
// parameter containing "json" to get a response conforming to the OAuth 2.0 specification.
if (context.Registration.ProviderName is Providers.Deezer)
if (context.Registration.ProviderType is ProviderTypes.Deezer)
{
request.RequestUri = OpenIddictHelpers.AddQueryStringParameter(
request.RequestUri, name: "output", value: "json");
@ -287,11 +287,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
var request = context.Transaction.GetHttpRequestMessage() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
request.Content = context.Registration.ProviderName switch
request.Content = context.Registration.ProviderType switch
{
// Trovo returns a 500 internal server error when using the standard
// "application/x-www-form-urlencoded" format and requires using JSON.
Providers.Trovo => JsonContent.Create(context.Transaction.Request,
ProviderTypes.Trovo => JsonContent.Create(context.Transaction.Request,
new MediaTypeHeaderValue(MediaTypes.Json)
{
CharSet = Charsets.Utf8
@ -335,7 +335,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Note: Deezer doesn't return a standard "expires_in" parameter
// but returns an equivalent "expires" integer parameter instead.
if (context.Registration.ProviderName is Providers.Deezer)
if (context.Registration.ProviderType is ProviderTypes.Deezer)
{
context.Response[Parameters.ExpiresIn] = context.Response["expires"];
context.Response["expires"] = null;

6
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Protection.cs

@ -50,7 +50,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
return default;
}
context.TokenValidationParameters.ValidateIssuer = context.Registration.ProviderName switch
context.TokenValidationParameters.ValidateIssuer = context.Registration.ProviderType switch
{
// When the Microsoft Account provider is configured to use one of the special tenants,
// the returned tokens include a dynamic issuer claim corresponding to the tenant
@ -59,8 +59,8 @@ public static partial class OpenIddictClientWebIntegrationHandlers
//
// For more information about the special tenants supported by Microsoft Account/Azure AD, see
// https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc#find-your-apps-openid-configuration-document-uri.
Providers.Microsoft when
context.Registration.GetMicrosoftOptions() is { Tenant: string tenant } &&
ProviderTypes.Microsoft when
context.Registration.GetMicrosoftSettings() is { Tenant: string tenant } &&
(string.Equals(tenant, "common", StringComparison.OrdinalIgnoreCase) ||
string.Equals(tenant, "consumers", StringComparison.OrdinalIgnoreCase) ||
string.Equals(tenant, "organizations", StringComparison.OrdinalIgnoreCase))

32
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs

@ -65,14 +65,14 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Trakt requires sending both an API key (which is always the client identifier) and an API version
// (which is statically set to the last version known to be supported by the OpenIddict integration).
if (context.Registration.ProviderName is Providers.Trakt)
if (context.Registration.ProviderType is ProviderTypes.Trakt)
{
request.Headers.Add("trakt-api-key", context.Registration.ClientId);
request.Headers.Add("trakt-api-version", "2");
}
// Trovo requires sending the client identifier as a separate, non-standard header.
else if (context.Registration.ProviderName is Providers.Trovo)
else if (context.Registration.ProviderType is ProviderTypes.Trovo)
{
request.Headers.Add("Client-ID", context.Registration.ClientId);
}
@ -117,7 +117,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// or using a non-standard authentication scheme (e.g OAuth instead of Bearer).
// These providers require sending the access token as part of the request payload.
if (context.Registration.ProviderName is Providers.Deezer or Providers.Mixcloud or Providers.StackExchange)
if (context.Registration.ProviderType is ProviderTypes.Deezer or ProviderTypes.Mixcloud or ProviderTypes.StackExchange)
{
context.Request.AccessToken = request.Headers.Authorization?.Parameter;
@ -126,7 +126,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
// Trovo requires using the "OAuth" scheme instead of the standard "Bearer" value.
else if (context.Registration.ProviderName is Providers.Trovo)
else if (context.Registration.ProviderType is ProviderTypes.Trovo)
{
request.Headers.Authorization = new AuthenticationHeaderValue("OAuth",
request.Headers.Authorization?.Parameter);
@ -162,7 +162,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// ArcGIS Online doesn't support header-based content negotiation and requires using
// the non-standard "f" parameter to get back JSON responses instead of HTML pages.
if (context.Registration.ProviderName is Providers.ArcGisOnline)
if (context.Registration.ProviderType is ProviderTypes.ArcGisOnline)
{
context.Request["f"] = "json";
}
@ -209,10 +209,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// OpenIddict from extracting userinfo responses. To work around that, the declared
// content type is replaced by the correct value for the providers that require it.
response.Content.Headers.ContentType = context.Registration.ProviderName switch
response.Content.Headers.ContentType = context.Registration.ProviderType switch
{
// Mixcloud returns JSON-formatted contents declared as "text/javascript".
Providers.Mixcloud => new MediaTypeHeaderValue(MediaTypes.Json)
ProviderTypes.Mixcloud => new MediaTypeHeaderValue(MediaTypes.Json)
{
CharSet = Charsets.Utf8
},
@ -255,49 +255,49 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// logic from mapping the parameters to CLR claims. To work around that, this handler
// is responsible for extracting the nested payload and replacing the userinfo response.
context.Response = context.Registration.ProviderName switch
context.Response = context.Registration.ProviderType switch
{
// Basecamp returns a nested "identity" object and a collection of "accounts".
Providers.Basecamp => new(context.Response["identity"]?.GetNamedParameters() ??
ProviderTypes.Basecamp => new(context.Response["identity"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("identity")))
{
["accounts"] = context.Response["accounts"]
},
// Fitbit returns a nested "user" object.
Providers.Fitbit => new(context.Response["user"]?.GetNamedParameters() ??
ProviderTypes.Fitbit => new(context.Response["user"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("user"))),
// Harvest returns a nested "user" object and a collection of "accounts".
Providers.Harvest => new(context.Response["user"]?.GetNamedParameters() ??
ProviderTypes.Harvest => new(context.Response["user"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("user")))
{
["accounts"] = context.Response["accounts"]
},
// Patreon returns a nested "attributes" object that is itself nested in a "data" node.
Providers.Patreon => new(context.Response["data"]?["attributes"]?.GetNamedParameters() ??
ProviderTypes.Patreon => new(context.Response["data"]?["attributes"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("data/attributes"))),
// ServiceChannel returns a nested "UserProfile" object.
Providers.ServiceChannel => new(context.Response["UserProfile"]?.GetNamedParameters() ??
ProviderTypes.ServiceChannel => new(context.Response["UserProfile"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("UserProfile"))),
// StackExchange returns an "items" array containing a single element.
Providers.StackExchange => new(context.Response["items"]?[0]?.GetNamedParameters() ??
ProviderTypes.StackExchange => new(context.Response["items"]?[0]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("items/0"))),
// Streamlabs splits the user data into multiple service-specific nodes (e.g "twitch"/"facebook").
//
// To make claims easier to use, the parameters are flattened and prefixed with the service name.
Providers.Streamlabs => new(
ProviderTypes.Streamlabs => new(
from parameter in context.Response.GetParameters()
from node in parameter.Value.GetNamedParameters()
let name = $"{parameter.Key}_{node.Key}"
select new KeyValuePair<string, OpenIddictParameter>(name, node.Value)),
// Twitter returns a nested "data" object.
Providers.Twitter => new(context.Response["data"]?.GetNamedParameters() ??
ProviderTypes.Twitter => new(context.Response["data"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("data"))),
_ => context.Response

135
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs

@ -81,7 +81,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Errors that are not handled here will be automatically handled
// by the standard handler present in the core OpenIddict client.
if (context.Registration.ProviderName is Providers.Deezer)
if (context.Registration.ProviderType is ProviderTypes.Deezer)
{
// Note: Deezer uses a custom "error_reason" parameter instead of the
// standard "error" parameter defined by the OAuth 2.0 specification.
@ -99,7 +99,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
else if (context.Registration.ProviderName is Providers.LinkedIn)
else if (context.Registration.ProviderType is ProviderTypes.LinkedIn)
{
var error = (string?) context.Request[Parameters.Error];
if (string.Equals(error, "user_cancelled_authorize", StringComparison.Ordinal) ||
@ -114,7 +114,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
}
}
else if (context.Registration.ProviderName is Providers.Mixcloud)
else if (context.Registration.ProviderType is ProviderTypes.Mixcloud)
{
var error = (string?) context.Request[Parameters.Error];
if (string.Equals(error, "user_denied", StringComparison.Ordinal))
@ -156,13 +156,13 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.TokenEndpoint = context.Registration.ProviderName switch
context.TokenEndpoint = context.Registration.ProviderType switch
{
// Trovo uses a different token endpoint for the refresh token grant.
//
// For more information, see
// https://developer.trovo.live/docs/APIs.html#_4-3-refresh-access-token.
Providers.Trovo when context.GrantType is GrantTypes.RefreshToken
ProviderTypes.Trovo when context.GrantType is GrantTypes.RefreshToken
=> new Uri("https://open-api.trovo.live/openplatform/refreshtoken", UriKind.Absolute),
_ => context.TokenEndpoint
@ -205,11 +205,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
//
// For more information about the custom client authentication method implemented by Apple,
// see https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens.
if (context.Registration.ProviderName is Providers.Apple)
if (context.Registration.ProviderType is ProviderTypes.Apple)
{
var options = context.Registration.GetAppleOptions();
var settings = context.Registration.GetAppleSettings();
context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, options.TeamId);
context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, settings.TeamId);
context.ClientAssertionTokenPrincipal.SetAudiences("https://appleid.apple.com");
}
@ -251,7 +251,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// is the same as private_key_jwt, the configuration is amended to assume Apple supports
// private_key_jwt and an event handler is responsible for populating the client_secret
// parameter using the client assertion token once it has been generated by OpenIddict.
if (context.Registration.ProviderName is Providers.Apple)
if (context.Registration.ProviderType is ProviderTypes.Apple)
{
context.TokenRequest.ClientSecret = context.TokenRequest.ClientAssertion;
context.TokenRequest.ClientAssertion = null;
@ -305,10 +305,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// of the authorization requests, the value persisted in the state token principal
// MUST be replaced to include the state token received by the redirection endpoint.
context.TokenRequest.RedirectUri = context.Registration.ProviderName switch
context.TokenRequest.RedirectUri = context.Registration.ProviderType switch
{
Providers.Deezer or
Providers.Mixcloud => OpenIddictHelpers.AddQueryStringParameter(
ProviderTypes.Deezer or
ProviderTypes.Mixcloud => OpenIddictHelpers.AddQueryStringParameter(
uri: new Uri(context.TokenRequest.RedirectUri, UriKind.Absolute),
name: Parameters.State,
value: context.StateToken).AbsoluteUri,
@ -346,11 +346,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
(context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken,
context.ValidateBackchannelIdentityToken) = context.Registration.ProviderName switch
context.ValidateBackchannelIdentityToken) = context.Registration.ProviderType switch
{
// While PayPal claims the OpenID Connect flavor of the code flow is supported,
// their implementation doesn't return an id_token from the token endpoint.
Providers.PayPal => (false, false, false),
ProviderTypes.PayPal => (false, false, false),
_ => (context.ExtractBackchannelIdentityToken,
context.RequireBackchannelIdentityToken,
@ -392,10 +392,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Despite being an important security feature, nonce validation is explicitly disabled
// for the providers that are known to cause errors when nonce validation is enforced.
context.DisableBackchannelIdentityTokenNonceValidation = context.Registration.ProviderName switch
context.DisableBackchannelIdentityTokenNonceValidation = context.Registration.ProviderType switch
{
// These providers don't include the nonce in their identity tokens:
Providers.Asana or Providers.Dropbox or Providers.LinkedIn or Providers.QuickBooksOnline => true,
ProviderTypes.Asana or ProviderTypes.Dropbox or
ProviderTypes.LinkedIn or ProviderTypes.QuickBooksOnline => true,
_ => context.DisableBackchannelIdentityTokenNonceValidation
};
@ -428,11 +429,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.UserinfoEndpoint = context.Registration.ProviderName switch
context.UserinfoEndpoint = context.Registration.ProviderType switch
{
// HubSpot doesn't have a static userinfo endpoint but allows retrieving basic information
// by using an access token info endpoint that requires sending the token in the URI path.
Providers.HubSpot when
ProviderTypes.HubSpot when
(context.BackchannelAccessToken ?? context.FrontchannelAccessToken) is { Length: > 0 } token
=> OpenIddictHelpers.CreateAbsoluteUri(
left : new Uri("https://api.hubapi.com/oauth/v1/access-tokens", UriKind.Absolute),
@ -440,7 +441,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// SuperOffice doesn't expose a static OpenID Connect userinfo endpoint but offers an API whose
// absolute URI needs to be computed based on a special claim returned in the identity token.
Providers.SuperOffice when
ProviderTypes.SuperOffice when
(context.BackchannelIdentityTokenPrincipal ?? // Always prefer the backchannel identity token when available.
context.FrontchannelIdentityTokenPrincipal) is ClaimsPrincipal principal &&
Uri.TryCreate(principal.GetClaim("http://schemes.superoffice.net/identity/webapi_url"), UriKind.Absolute, out Uri? uri)
@ -476,14 +477,14 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.SendUserinfoRequest = context.Registration.ProviderName switch
context.SendUserinfoRequest = context.Registration.ProviderType switch
{
// Note: the frontchannel or backchannel access tokens returned by Azure AD when a
// Xbox scope is requested cannot be used with the userinfo endpoint as they use a
// legacy format that is not supported by the Azure AD userinfo implementation.
//
// To work around this limitation, userinfo retrieval is disabled when a Xbox scope is requested.
Providers.Microsoft => context.GrantType switch
ProviderTypes.Microsoft => context.GrantType switch
{
GrantTypes.AuthorizationCode or GrantTypes.Implicit when
context.StateTokenPrincipal is ClaimsPrincipal principal &&
@ -534,10 +535,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
//
// To ensure OpenIddict can be used with these providers, validation is disabled when necessary.
context.DisableUserinfoValidation = context.Registration.ProviderName switch
context.DisableUserinfoValidation = context.Registration.ProviderType switch
{
// SuperOffice doesn't offer a standard OpenID Connect userinfo endpoint.
Providers.SuperOffice => true,
ProviderTypes.SuperOffice => true,
_ => context.DisableUserinfoValidation
};
@ -576,35 +577,35 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Facebook limits the number of fields returned by the userinfo endpoint
// but allows returning additional information using special parameters that
// determine what fields will be returned as part of the userinfo response.
if (context.Registration.ProviderName is Providers.Facebook)
if (context.Registration.ProviderType is ProviderTypes.Facebook)
{
var options = context.Registration.GetFacebookOptions();
var settings = context.Registration.GetFacebookSettings();
context.UserinfoRequest["fields"] = string.Join(",", options.Fields);
context.UserinfoRequest["fields"] = string.Join(",", settings.Fields);
}
// Patreon limits the number of fields returned by the userinfo endpoint
// but allows returning additional information using special parameters that
// determine what fields will be returned as part of the userinfo response.
else if (context.Registration.ProviderName is Providers.Patreon)
else if (context.Registration.ProviderType is ProviderTypes.Patreon)
{
var options = context.Registration.GetPatreonOptions();
var settings = context.Registration.GetPatreonSettings();
context.UserinfoRequest["fields[user]"] = string.Join(",", options.UserFields);
context.UserinfoRequest["fields[user]"] = string.Join(",", settings.UserFields);
}
// StackOverflow requires sending an application key and a site parameter
// containing the name of the site from which the user profile is retrieved.
else if (context.Registration.ProviderName is Providers.StackExchange)
else if (context.Registration.ProviderType is ProviderTypes.StackExchange)
{
var options = context.Registration.GetStackExchangeOptions();
var settings = context.Registration.GetStackExchangeSettings();
context.UserinfoRequest["key"] = options.ApplicationKey;
context.UserinfoRequest["site"] = options.Site;
context.UserinfoRequest["key"] = settings.ApplicationKey;
context.UserinfoRequest["site"] = settings.Site;
}
// Trakt allows retrieving additional user details via the "extended" parameter.
else if (context.Registration.ProviderName is Providers.Trakt)
else if (context.Registration.ProviderType is ProviderTypes.Trakt)
{
context.UserinfoRequest["extended"] = "full";
}
@ -612,13 +613,13 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Twitter limits the number of fields returned by the userinfo endpoint
// but allows returning additional information using special parameters that
// determine what fields will be returned as part of the userinfo response.
else if (context.Registration.ProviderName is Providers.Twitter)
else if (context.Registration.ProviderType is ProviderTypes.Twitter)
{
var options = context.Registration.GetTwitterOptions();
var settings = context.Registration.GetTwitterSettings();
context.UserinfoRequest["expansions"] = string.Join(",", options.Expansions);
context.UserinfoRequest["tweet.fields"] = string.Join(",", options.TweetFields);
context.UserinfoRequest["user.fields"] = string.Join(",", options.UserFields);
context.UserinfoRequest["expansions"] = string.Join(",", settings.Expansions);
context.UserinfoRequest["tweet.fields"] = string.Join(",", settings.TweetFields);
context.UserinfoRequest["user.fields"] = string.Join(",", settings.UserFields);
}
return default;
@ -663,15 +664,15 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// To work around that, this handler is responsible for extracting these parameters
// from the token response and creating a userinfo token principal containing them.
var parameters = context.Registration.ProviderName switch
var parameters = context.Registration.ProviderType switch
{
// For Strava, include all the parameters contained in the "athlete" object.
//
// Note: the "athlete" node is not returned for grant_type=refresh_token requests.
Providers.Strava => context.TokenResponse["athlete"]?.GetNamedParameters(),
ProviderTypes.Strava => context.TokenResponse["athlete"]?.GetNamedParameters(),
// For Stripe, only include "livemode" and the parameters that are prefixed with "stripe_":
Providers.StripeConnect =>
ProviderTypes.StripeConnect =>
from parameter in context.TokenResponse.GetParameters()
where string.Equals(parameter.Key, "livemode", StringComparison.OrdinalIgnoreCase) ||
parameter.Key.StartsWith("stripe_", StringComparison.OrdinalIgnoreCase)
@ -745,7 +746,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.AuthorizationEndpoint = context.Registration.ProviderName switch
context.AuthorizationEndpoint = context.Registration.ProviderType switch
{
// Stripe uses a different authorization endpoint for express accounts.
//
@ -754,7 +755,7 @@ public static partial class OpenIddictClientWebIntegrationHandlers
//
// For more information, see
// https://stripe.com/docs/connect/oauth-reference?locale=en-us#get-authorize.
Providers.StripeConnect when context.Properties.TryGetValue(".stripe_account_type", out string? type) =>
ProviderTypes.StripeConnect when context.Properties.TryGetValue(".stripe_account_type", out string? type) =>
string.Equals(type, "express", StringComparison.OrdinalIgnoreCase) ?
new Uri("https://connect.stripe.com/express/oauth/authorize", UriKind.Absolute) :
new Uri("https://connect.stripe.com/oauth/authorize", UriKind.Absolute),
@ -792,10 +793,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.ResponseMode = context.Registration.ProviderName switch
context.ResponseMode = context.Registration.ProviderType switch
{
// Note: Apple requires using form_post when the "email" or "name" scopes are requested.
Providers.Apple when context.Scopes.Contains(Scopes.Email) || context.Scopes.Contains("name")
ProviderTypes.Apple when context.Scopes.Contains(Scopes.Email) || context.Scopes.Contains("name")
=> ResponseModes.FormPost,
_ => context.ResponseMode
@ -830,15 +831,15 @@ public static partial class OpenIddictClientWebIntegrationHandlers
throw new ArgumentNullException(nameof(context));
}
context.Request.Scope = context.Registration.ProviderName switch
context.Request.Scope = context.Registration.ProviderType switch
{
// The following providers are known to use comma-separated scopes instead of
// the standard format (that requires using a space as the scope separator):
Providers.Deezer or Providers.Strava => string.Join(",", context.Scopes),
ProviderTypes.Deezer or ProviderTypes.Strava => string.Join(",", context.Scopes),
// The following providers are known to use plus-separated scopes instead of
// the standard format (that requires using a space as the scope separator):
Providers.Trovo => string.Join("+", context.Scopes),
ProviderTypes.Trovo => string.Join("+", context.Scopes),
_ => context.Request.Scope
};
@ -887,10 +888,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Note: this workaround only works for providers that allow dynamic
// redirection URIs and implement a relaxed validation policy logic.
(context.Request.RedirectUri, context.Request.State) = context.Registration.ProviderName switch
(context.Request.RedirectUri, context.Request.State) = context.Registration.ProviderType switch
{
Providers.Deezer or
Providers.Mixcloud => (OpenIddictHelpers.AddQueryStringParameter(
ProviderTypes.Deezer or
ProviderTypes.Mixcloud => (OpenIddictHelpers.AddQueryStringParameter(
uri: new Uri(context.RedirectUri, UriKind.Absolute),
name: Parameters.State,
value: context.Request.State).AbsoluteUri, null),
@ -929,47 +930,47 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Active Directory Federation Services allows sending a custom "resource"
// parameter to define what API resources the access token will give access to.
if (context.Registration.ProviderName is Providers.ActiveDirectoryFederationServices)
if (context.Registration.ProviderType is ProviderTypes.ActiveDirectoryFederationServices)
{
var options = context.Registration.GetActiveDirectoryFederationServicesOptions();
var settings = context.Registration.GetActiveDirectoryFederationServicesSettings();
context.Request["resource"] = options.Resource;
context.Request["resource"] = settings.Resource;
}
// By default, Google doesn't return a refresh token but allows sending an "access_type"
// parameter to retrieve one (but it is only returned during the first authorization dance).
else if (context.Registration.ProviderName is Providers.Google)
else if (context.Registration.ProviderType is ProviderTypes.Google)
{
var options = context.Registration.GetGoogleOptions();
var settings = context.Registration.GetGoogleSettings();
context.Request["access_type"] = options.AccessType;
context.Request["access_type"] = settings.AccessType;
}
// Pro Santé Connect's specification requires sending an acr_values parameter containing
// the desired level of authentication (currently, only "eidas1" is supported). For more
// information, see https://www.legifrance.gouv.fr/jorf/id/JORFTEXT000045551195.
else if (context.Registration.ProviderName is Providers.ProSantéConnect)
else if (context.Registration.ProviderType is ProviderTypes.ProSantéConnect)
{
var options = context.Registration.GetProSantéConnectOptions();
var settings = context.Registration.GetProSantéConnectSettings();
context.Request.AcrValues = options.AuthenticationLevel;
context.Request.AcrValues = settings.AuthenticationLevel;
}
// By default, Reddit doesn't return a refresh token but
// allows sending a "duration" parameter to retrieve one.
else if (context.Registration.ProviderName is Providers.Reddit)
else if (context.Registration.ProviderType is ProviderTypes.Reddit)
{
var options = context.Registration.GetRedditOptions();
var settings = context.Registration.GetRedditSettings();
context.Request["duration"] = options.Duration;
context.Request["duration"] = settings.Duration;
}
// Slack allows sending an optional "team" parameter to simplify the login process.
else if (context.Registration.ProviderName is Providers.Slack)
else if (context.Registration.ProviderType is ProviderTypes.Slack)
{
var options = context.Registration.GetSlackOptions();
var settings = context.Registration.GetSlackSettings();
context.Request["team"] = options.Team;
context.Request["team"] = settings.Team;
}
return default;

1
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationOptions.cs

@ -11,5 +11,4 @@ namespace OpenIddict.Client.WebIntegration;
/// </summary>
public sealed partial class OpenIddictClientWebIntegrationOptions
{
// Note: provider options are automatically generated by the source generator.
}

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

@ -13,7 +13,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="ActiveDirectoryFederationServices" DisplayName="Microsoft Active Directory Federation Services"
<Provider Name="ActiveDirectoryFederationServices"
DisplayName="Microsoft 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
@ -37,7 +38,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="ArcGisOnline" DisplayName="ArcGIS Online"
<Provider Name="ArcGisOnline" DisplayName="ArcGIS Online" Id="ae00d744-0090-425e-b932-34c5e395f588"
Documentation="https://developers.arcgis.com/documentation/mapping-apis-and-services/security/oauth-2.0/">
<Environment Issuer="https://www.arcgis.com/">
<Configuration AuthorizationEndpoint="https://www.arcgis.com/sharing/rest/oauth2/authorize"
@ -61,7 +62,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Apple" DisplayName="Sign in with Apple"
<Provider Name="Apple" DisplayName="Sign in with Apple" Id="f59a420e-9d00-4a85-94a3-8ecacc5968d8"
Documentation="https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api">
<Environment Issuer="https://appleid.apple.com/" />
@ -82,7 +83,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Asana" Documentation="https://developers.asana.com/docs/openid-connect">
<Provider Name="Asana" Id="c04d789a-a3d3-43f9-a06f-bd56935af13f"
Documentation="https://developers.asana.com/docs/openid-connect">
<Environment Issuer="https://app.asana.com/api/1.0" />
</Provider>
@ -94,7 +96,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Basecamp" Documentation="https://github.com/basecamp/api/blob/master/sections/authentication.md">
<Provider Name="Basecamp" Id="0f3cb4f5-e520-4b99-ae39-1c967d9d77ce"
Documentation="https://github.com/basecamp/api/blob/master/sections/authentication.md">
<!--
Note: Basecamp implements an old draft of the OAuth 2.0 specification and doesn't support the
"response_type" and "grant_type" parameters adopted in the final version of the standard.
@ -121,7 +124,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="BattleNet" DisplayName="Battle.net"
<Provider Name="BattleNet" DisplayName="Battle.net" Id="9baed518-5e8d-4544-b562-686218326b14"
Documentation="https://develop.battle.net/documentation/guides/using-oauth">
<!--
Note: most Battle.net regions use the same issuer URI but a different domain is required for China.
@ -145,7 +148,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Bitbucket" Documentation="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/">
<Provider Name="Bitbucket" Id="2e71023e-9548-4b04-a9fe-afe93daf64ea"
Documentation="https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/">
<Environment Issuer="https://bitbucket.org/">
<Configuration AuthorizationEndpoint="https://bitbucket.org/site/oauth2/authorize"
TokenEndpoint="https://bitbucket.org/site/oauth2/access_token"
@ -164,7 +168,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Cognito" DisplayName="Amazon Cognito"
<Provider Name="Cognito" DisplayName="Amazon Cognito" Id="37931265-19ea-41e9-8a8c-06bb7deb9a1d"
Documentation="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-reference.html">
<Environment Issuer="https://cognito-idp.{settings.Region}.amazonaws.com/{settings.UserPoolId}" />
@ -183,7 +187,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Deezer" Documentation="https://developers.deezer.com/api/oauth">
<Provider Name="Deezer" Id="8e6d9b1c-09c8-498f-8c27-8363d5de4c94"
Documentation="https://developers.deezer.com/api/oauth">
<!--
Note: the Deezer documentation describes an implementation with important deviations from the OAuth 2.0 standard,
including the use of many non-standard and custom parameters. Luckily, while the documentation hasn't been fixed
@ -207,7 +212,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="DeviantArt" Documentation="https://www.deviantart.com/developers/authentication">
<Provider Name="DeviantArt" Id="343233b4-661d-45be-804f-98bf2d865d60"
Documentation="https://www.deviantart.com/developers/authentication">
<Environment Issuer="https://www.deviantart.com/">
<Configuration AuthorizationEndpoint="https://www.deviantart.com/oauth2/authorize"
TokenEndpoint="https://www.deviantart.com/oauth2/token"
@ -227,7 +233,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Discord" Documentation="https://discord.com/developers/docs/topics/oauth2">
<Provider Name="Discord" Id="542ac9f1-514f-4b8a-bc4c-d56d2b8356e5"
Documentation="https://discord.com/developers/docs/topics/oauth2">
<Environment Issuer="https://discord.com/">
<Configuration AuthorizationEndpoint="https://discord.com/oauth2/authorize"
TokenEndpoint="https://discord.com/api/oauth2/token"
@ -254,7 +261,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Dropbox" Documentation="https://developers.dropbox.com/oidc-guide">
<Provider Name="Dropbox" Id="9f0764f5-a148-4890-b38e-b7ad368824cd"
Documentation="https://developers.dropbox.com/oidc-guide">
<Environment Issuer="https://www.dropbox.com/">
<!--
Note: Dropbox requires sending at least either the "profile" or "email" scope.
@ -273,7 +281,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="EpicGames" DisplayName="Epic Games"
<Provider Name="EpicGames" DisplayName="Epic Games" Id="53f941ab-9a5a-4849-9d8e-21bbe8d85a15"
Documentation="https://dev.epicgames.com/docs/web-api-ref/authentication">
<Environment Issuer="https://api.epicgames.dev/epic/oauth/v1" />
</Provider>
@ -286,7 +294,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Facebook" Documentation="https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow">
<Provider Name="Facebook" Id="43eabe57-af21-448a-888f-641b1ce8a402"
Documentation="https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow">
<Environment Issuer="https://www.facebook.com/">
<Configuration AuthorizationEndpoint="https://www.facebook.com/v16.0/dialog/oauth"
TokenEndpoint="https://graph.facebook.com/v16.0/oauth/access_token"
@ -312,7 +321,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Fitbit" Documentation="https://dev.fitbit.com/build/reference/web-api/developer-guide/authorization/">
<Provider Name="Fitbit" Id="10a558b9-8c81-47cc-8941-e54d0432fd51"
Documentation="https://dev.fitbit.com/build/reference/web-api/developer-guide/authorization/">
<Environment Issuer="https://www.fitbit.com/">
<Configuration AuthorizationEndpoint="https://www.fitbit.com/oauth2/authorize"
TokenEndpoint="https://api.fitbit.com/oauth2/token"
@ -339,7 +349,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="GitHub" Documentation="https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps">
<Provider Name="GitHub" Id="87edae0b-e71e-4163-960f-cf7e2a780d77"
Documentation="https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps">
<Environment Issuer="https://github.com/">
<Configuration AuthorizationEndpoint="https://github.com/login/oauth/authorize"
DeviceAuthorizationEndpoint="https://github.com/login/device/code"
@ -359,7 +370,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="GitLab" Documentation="https://docs.gitlab.com/ee/integration/openid_connect_provider.html">
<Provider Name="GitLab" Id="521825df-8e65-4572-b192-f1a68b3e943f"
Documentation="https://docs.gitlab.com/ee/integration/openid_connect_provider.html">
<Environment Issuer="https://gitlab.com/" />
</Provider>
@ -371,7 +383,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Google" Documentation="https://developers.google.com/identity/protocols/oauth2/openid-connect">
<Provider Name="Google" Id="e0e90ce7-adb5-4b05-9f54-594941e5d960"
Documentation="https://developers.google.com/identity/protocols/oauth2/openid-connect">
<Environment Issuer="https://accounts.google.com/" />
<Setting PropertyName="AccessType" ParameterName="type" Type="String" Required="false"
@ -386,7 +399,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Harvest" Documentation="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">
<Provider Name="Harvest" Id="1a94de4b-187b-45d2-aa8e-9510c0ede1de"
Documentation="https://help.getharvest.com/api-v2/authentication-api/authentication/authentication/">
<Environment Issuer="https://id.getharvest.com/">
<Configuration AuthorizationEndpoint="https://id.getharvest.com/oauth2/authorize"
TokenEndpoint="https://id.getharvest.com/api/v2/oauth2/token"
@ -405,7 +419,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="HubSpot" Documentation="https://developers.hubspot.com/docs/api/oauth-quickstart-guide">
<Provider Name="HubSpot" Id="416ca088-0096-4215-b85d-2e83c47abf89"
Documentation="https://developers.hubspot.com/docs/api/oauth-quickstart-guide">
<Environment Issuer="https://www.hubspot.com/">
<Configuration AuthorizationEndpoint="https://app.hubspot.com/oauth/authorize"
TokenEndpoint="https://api.hubapi.com/oauth/v1/token">
@ -429,7 +444,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Keycloak" Documentation="https://www.keycloak.org/getting-started/getting-started-docker">
<Provider Name="Keycloak" Id="1097a0ea-08a8-431f-bc54-b547b1000420"
Documentation="https://www.keycloak.org/getting-started/getting-started-docker">
<!--
Note: Keycloak is a self-hosted-only identity provider that doesn't have a generic issuer URI.
As such, the complete URI must always be set in the options and include the realm, if applicable.
@ -449,7 +465,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="LinkedIn" Documentation="https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2">
<Provider Name="LinkedIn" Id="eb3bc226-ed25-4258-8a37-2bfcc4d628a2"
Documentation="https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2">
<Environment Issuer="https://www.linkedin.com/" ConfigurationEndpoint="https://www.linkedin.com/oauth/.well-known/openid-configuration">
<!--
Note: LinkedIn requires sending the "profile" scope to be able to use the userinfo endpoint.
@ -477,7 +494,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Mailchimp" Documentation="https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/#oauth-2-workflow-overview">
<Provider Name="Mailchimp" Id="5453049d-4069-4668-b8d0-162a8bbd4b40"
Documentation="https://mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/#oauth-2-workflow-overview">
<Environment Issuer="https://login.mailchimp.com/">
<Configuration AuthorizationEndpoint="https://login.mailchimp.com/oauth2/authorize"
TokenEndpoint="https://login.mailchimp.com/oauth2/token"
@ -493,7 +511,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Microsoft" DisplayName="Microsoft Account/Azure Active Directory"
<Provider Name="Microsoft" DisplayName="Microsoft Account/Azure Active Directory" Id="b533a06a-3fd6-4754-aeca-025d4e3666ad"
Documentation="https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-protocols-oidc">
<!--
Note: Microsoft is a multitenant provider that relies on virtual paths to identify instances.
@ -516,7 +534,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Mixcloud" Documentation="https://www.mixcloud.com/developers/#authorization">
<Provider Name="Mixcloud" Id="cf9df6cb-2e1e-44c2-a4c1-42688fb0bf2c"
Documentation="https://www.mixcloud.com/developers/#authorization">
<Environment Issuer="https://www.mixcloud.com/">
<Configuration AuthorizationEndpoint="https://www.mixcloud.com/oauth/authorize"
TokenEndpoint="https://www.mixcloud.com/oauth/access_token"
@ -532,7 +551,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Patreon" Documentation="https://docs.patreon.com/#oauth">
<Provider Name="Patreon" Id="0bf83b54-a005-4384-aa4e-828a0601d799"
Documentation="https://docs.patreon.com/#oauth">
<Environment Issuer="https://www.patreon.com/">
<Configuration AuthorizationEndpoint="https://www.patreon.com/oauth2/authorize"
TokenEndpoint="https://www.patreon.com/api/oauth2/token"
@ -566,7 +586,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="PayPal" Documentation="https://developer.paypal.com/docs/log-in-with-paypal/">
<Provider Name="PayPal" Id="37cf4e18-138b-4b98-b8c0-ba564c0910c4"
Documentation="https://developer.paypal.com/docs/log-in-with-paypal/">
<!--
Note: PayPal offers a production and a sandbox environment, but the sandbox server metadata
document doesn't reflect the configuration used by the sandbox environment (e.g the production
@ -587,7 +608,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="PingOne" Documentation="https://docs.pingidentity.com/r/en-us/pingoneforenterprise/p14e_connect_oidc">
<Provider Name="PingOne" Id="632402f9-af28-47bb-b6b8-53c38a6c49d4"
Documentation="https://docs.pingidentity.com/r/en-us/pingoneforenterprise/p14e_connect_oidc">
<!--
Note: PingOne is a multitenant identity provider that doesn't have a generic issuer URI.
As such, the complete URI must always be set in the options (and include the environment ID).
@ -607,7 +629,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="ProSantéConnect" DisplayName="Pro Santé Connect"
<Provider Name="ProSantéConnect" DisplayName="Pro Santé Connect" Id="445a1710-8c71-4f00-8595-ac8c04c23777"
Documentation="https://industriels.esante.gouv.fr/en/products-services/health-pro-authentication-pro-sante-connect">
<!--
Note: Pro Santé Connect requires sending the "scope_all" scope (which is currently the only supported value).
@ -638,7 +660,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="QuickBooksOnline" DisplayName="QuickBooks Online"
<Provider Name="QuickBooksOnline" DisplayName="QuickBooks Online" Id="ee5fc05e-76bb-40e3-82f1-f369030205e9"
Documentation="https://developer.intuit.com/app/developer/qbo/docs/develop/authentication-and-authorization/openid-connect">
<Environment Name="Production" Issuer="https://oauth.platform.intuit.com/op/v1"
ConfigurationEndpoint="https://developer.api.intuit.com/.well-known/openid_configuration" />
@ -655,7 +677,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Reddit" Documentation="https://github.com/reddit-archive/reddit/wiki/OAuth2">
<Provider Name="Reddit" Id="01ae8033-935c-43b9-8568-eaf4d08c0613"
Documentation="https://github.com/reddit-archive/reddit/wiki/OAuth2">
<Environment Issuer="https://www.reddit.com/">
<Configuration AuthorizationEndpoint="https://www.reddit.com/api/v1/authorize"
TokenEndpoint="https://www.reddit.com/api/v1/access_token"
@ -689,7 +712,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="ServiceChannel" Documentation="https://developer.servicechannel.com/basics/general/authentication/">
<Provider Name="ServiceChannel" Id="3c1df98a-23e5-4ce8-af9e-1714cb875560"
Documentation="https://developer.servicechannel.com/basics/general/authentication/">
<Environment Name="Production" Issuer="https://servicechannel.com/">
<Configuration AuthorizationEndpoint="https://login.servicechannel.com/oauth/authorize"
TokenEndpoint="https://login.servicechannel.com/oauth/token"
@ -721,7 +745,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Slack" Documentation="https://api.slack.com/authentication/sign-in-with-slack">
<Provider Name="Slack" Id="57c5ef63-1fbf-47d2-b4a3-432feae2eafc"
Documentation="https://api.slack.com/authentication/sign-in-with-slack">
<Environment Issuer="https://slack.com/" />
<Setting PropertyName="Team" ParameterName="team" Type="String" Required="false"
@ -736,7 +761,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Smartsheet" Documentation="https://smartsheet.redoc.ly/#section/OAuth-Walkthrough">
<Provider Name="Smartsheet" Id="e5f58c2a-0f45-4aa4-a700-e2540cd57419"
Documentation="https://smartsheet.redoc.ly/#section/OAuth-Walkthrough">
<Environment Issuer="https://www.smartsheet.com/">
<Configuration AuthorizationEndpoint="https://app.smartsheet.com/b/authorize"
TokenEndpoint="https://api.smartsheet.com/2.0/token"
@ -755,7 +781,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Spotify" Documentation="https://developer.spotify.com/documentation/general/guides/authorization/">
<Provider Name="Spotify" Id="4474ccd3-07d5-4687-8085-4e31d1ff27fa"
Documentation="https://developer.spotify.com/documentation/general/guides/authorization/">
<Environment Issuer="https://accounts.spotify.com/">
<Configuration AuthorizationEndpoint="https://accounts.spotify.com/authorize"
TokenEndpoint="https://accounts.spotify.com/api/token"
@ -775,7 +802,7 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="StackExchange" DisplayName="Stack Exchange"
<Provider Name="StackExchange" DisplayName="Stack Exchange" Id="1f34cc2e-c466-4a6a-a903-fd8040fb8454"
Documentation="https://api.stackexchange.com/docs/authentication">
<Environment Issuer="https://api.stackexchange.com/">
<Configuration AuthorizationEndpoint="https://stackoverflow.com/oauth"
@ -798,7 +825,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Strava" Documentation="https://developers.strava.com/docs/authentication/">
<Provider Name="Strava" Id="7400476b-2fdf-48fc-9d06-167101ffd3f6"
Documentation="https://developers.strava.com/docs/authentication/">
<Environment Issuer="http://www.strava.com/">
<!--
Note: Strava doesn't provide a userinfo endpoint and returns
@ -821,7 +849,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="StripeConnect" DisplayName="Stripe Connect" Documentation="https://stripe.com/docs/connect/oauth-reference">
<Provider Name="StripeConnect" DisplayName="Stripe Connect" Id="ea608674-1d4f-470c-adee-3f47dbce688f"
Documentation="https://stripe.com/docs/connect/oauth-reference">
<Environment Issuer="https://connect.stripe.com/">
<!--
Note: Stripe uses a different authorization endpoint for Express accounts. It also doesn't provide
@ -857,7 +886,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Streamlabs" Documentation="https://dev.streamlabs.com/docs/oauth-2">
<Provider Name="Streamlabs" Id="b140539d-ba83-4bcb-b46b-ecc19a3b8997"
Documentation="https://dev.streamlabs.com/docs/oauth-2">
<Environment Issuer="https://streamlabs.com/">
<Configuration AuthorizationEndpoint="https://streamlabs.com/api/v2.0/authorize"
TokenEndpoint="https://streamlabs.com/api/v2.0/token"
@ -876,7 +906,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="SuperOffice" Documentation="https://docs.superoffice.com/en/authentication/online/api.html">
<Provider Name="SuperOffice" Id="570e055e-4652-415d-8237-740e196d7e89"
Documentation="https://docs.superoffice.com/en/authentication/online/api.html">
<Environment Name="Production" Issuer="https://online.superoffice.com/" />
<Environment Name="Development" Issuer="https://sod.superoffice.com/" />
<Environment Name="Staging" Issuer="https://qaonline.superoffice.com/" />
@ -890,7 +921,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Trakt" Documentation="https://trakt.docs.apiary.io/#reference/authentication-oauth">
<Provider Name="Trakt" Id="8c38bccc-1588-4435-b612-55a443e67264"
Documentation="https://trakt.docs.apiary.io/#reference/authentication-oauth">
<Environment Issuer="https://trakt.tv/">
<Configuration AuthorizationEndpoint="https://trakt.tv/oauth/authorize"
TokenEndpoint="https://api.trakt.tv/oauth/token"
@ -909,7 +941,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Trovo" Documentation="https://developer.trovo.live/docs/APIs.html#_3-authentication">
<Provider Name="Trovo" Id="00091647-b315-4b7c-af7c-5bc5da397b7a"
Documentation="https://developer.trovo.live/docs/APIs.html#_3-authentication">
<Environment Issuer="https://trovo.live/">
<!--
Note: Trovo uses a different token endpoint for the refresh token grant. To accommodate this requirement,
@ -940,7 +973,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Twitter" Documentation="https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code">
<Provider Name="Twitter" Id="1fd20ab5-d3f2-40aa-8c91-094f71652c65"
Documentation="https://developer.twitter.com/en/docs/authentication/oauth-2-0/authorization-code">
<Environment Issuer="https://twitter.com/">
<Configuration AuthorizationEndpoint="https://twitter.com/i/oauth2/authorize"
TokenEndpoint="https://api.twitter.com/2/oauth2/token"
@ -1019,7 +1053,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Vimeo" Documentation="https://developer.vimeo.com/api/authentication">
<Provider Name="Vimeo" Id="bc5e79a9-ddef-4752-b86c-c238989ff2f1"
Documentation="https://developer.vimeo.com/api/authentication">
<Environment Issuer="https://api.vimeo.com/">
<Configuration AuthorizationEndpoint="https://api.vimeo.com/oauth/authorize"
TokenEndpoint="https://api.vimeo.com/oauth/access_token"
@ -1035,7 +1070,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="WordPress" Documentation="https://developer.wordpress.com/docs/oauth2/">
<Provider Name="WordPress" Id="1f32ad54-d87c-454b-b47a-65f18933f4b6"
Documentation="https://developer.wordpress.com/docs/oauth2/">
<Environment Issuer="https://wordpress.com/">
<Configuration AuthorizationEndpoint="https://public-api.wordpress.com/oauth2/authorize"
TokenEndpoint="https://public-api.wordpress.com/oauth2/token"
@ -1058,7 +1094,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Xero" Documentation="https://developer.xero.com/documentation/xero-app-store/app-partner-guides/sign-in/">
<Provider Name="Xero" Id="7426a750-1cd3-446a-bdaa-1b4b0d33a105"
Documentation="https://developer.xero.com/documentation/xero-app-store/app-partner-guides/sign-in/">
<Environment Issuer="https://identity.xero.com/" />
</Provider>
@ -1070,7 +1107,8 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->
<Provider Name="Yahoo" Documentation="https://developer.yahoo.com/oauth2/guide/openid_connect/">
<Provider Name="Yahoo" Id="874d78ec-3d79-4492-ab79-76a7dd7fa0b5"
Documentation="https://developer.yahoo.com/oauth2/guide/openid_connect/">
<Environment Issuer="https://api.login.yahoo.com/" />
</Provider>

12
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd

@ -473,6 +473,18 @@
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Id" use="required">
<xs:annotation>
<xs:documentation>The provider identifier, represented as a GUID.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})|(\{[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\})" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Documentation" type="xs:anyURI" use="optional">
<xs:annotation>
<xs:documentation>The documentation URI, if applicable.</xs:documentation>

15
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs

@ -0,0 +1,15 @@
/*
* 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.WebIntegration;
/// <summary>
/// Provides various settings needed to configure the OpenIddict client Web providers.
/// </summary>
public sealed partial class OpenIddictClientWebIntegrationSettings
{
// Note: provider settings are automatically generated by the source generator.
}

1
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -922,7 +922,6 @@ public sealed class OpenIddictClientBuilder
/// specific OAuth 2.0 flow, visit https://tools.ietf.org/html/rfc8628.
/// </summary>
/// <returns>The <see cref="OpenIddictClientBuilder"/> instance.</returns>
[RequiresPreviewFeatures]
public OpenIddictClientBuilder AllowDeviceCodeFlow()
=> Configure(options => options.GrantTypes.Add(GrantTypes.DeviceCode));

65
src/OpenIddict.Client/OpenIddictClientConfiguration.cs

@ -6,6 +6,9 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;
@ -43,7 +46,14 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
foreach (var registration in options.Registrations)
{
if (registration.Issuer is not { IsAbsoluteUri: true })
if (registration.Issuer is null)
{
throw string.IsNullOrEmpty(registration.ProviderType) ?
new InvalidOperationException(SR.GetResourceString(SR.ID0405)) :
new InvalidOperationException(SR.GetResourceString(SR.ID0411));
}
if (!registration.Issuer.IsAbsoluteUri || !registration.Issuer.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0136));
}
@ -53,6 +63,29 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
throw new InvalidOperationException(SR.GetResourceString(SR.ID0137));
}
// If no explicit registration identifier was set, compute a stable
// hash based on the issuer URI and the provider name, if available.
if (string.IsNullOrEmpty(registration.RegistrationId))
{
using var algorithm = CryptoConfig.CreateFromName("OpenIddict SHA-256 Cryptographic Provider") switch
{
SHA256 result => result,
null => SHA256.Create(),
var result => throw new CryptographicException(SR.FormatID0351(result.GetType().FullName))
};
TransformBlock(algorithm, registration.Issuer.AbsoluteUri);
if (!string.IsNullOrEmpty(registration.ProviderName))
{
TransformBlock(algorithm, registration.ProviderName);
}
algorithm.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
registration.RegistrationId = Base64UrlEncoder.Encode(algorithm.Hash);
}
if (registration.ConfigurationManager is null)
{
if (registration.Configuration is not null)
@ -165,24 +198,13 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
}
}
// Ensure issuers are not used in multiple client registrations.
if (options.Registrations.Count != options.Registrations.Select(registration => registration.Issuer)
.Distinct()
.Count())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0342));
}
// Ensure provider names are not used in multiple client registrations.
// Ensure registration identifiers are not used in multiple client registrations.
//
// Note: a string comparer ignoring casing is deliberately used to prevent
// two providers using the same name with a different casing from being added.
if (options.Registrations
.Where(registration => !string.IsNullOrEmpty(registration.ProviderName))
.Count() != options.Registrations.Select(registration => registration.ProviderName)
.Where(name => !string.IsNullOrEmpty(name))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count())
// Note: a string comparer ignoring casing is deliberately used to prevent two
// registrations using the same identifier with a different casing from being added.
if (options.Registrations.Count != options.Registrations.Select(registration => registration.RegistrationId)
.Distinct(StringComparer.OrdinalIgnoreCase)
.Count())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0347));
}
@ -281,5 +303,12 @@ public sealed class OpenIddictClientConfiguration : IPostConfigureOptions<OpenId
return null;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void TransformBlock(HashAlgorithm algorithm, string input)
{
var buffer = Encoding.UTF8.GetBytes(input);
algorithm.TransformBlock(buffer, 0, buffer.Length, outputBuffer: null, outputOffset: 0);
}
}
}

2
src/OpenIddict.Client/OpenIddictClientEvents.Exchange.cs

@ -170,7 +170,7 @@ public static partial class OpenIddictClientEvents
/// <summary>
/// Gets or sets the principal containing the claims resolved from the token response.
/// </summary>
[Obsolete]
[Obsolete("This property is no longer supported and will be removed in a future version.")]
public ClaimsPrincipal? Principal { get; set; }
}
}

34
src/OpenIddict.Client/OpenIddictClientEvents.cs

@ -329,10 +329,22 @@ public static partial class OpenIddictClientEvents
public string? Nonce { get; set; }
/// <summary>
/// Gets or sets the issuer used for the authentication demand, if applicable.
/// Gets or sets the identifier that will be used to resolve the client registration, if applicable.
/// </summary>
public string? RegistrationId { get; set; }
/// <summary>
/// Gets or sets the issuer URI of the provider that will be
/// used to resolve the client registration, if applicable.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary>
/// Gets or sets the name of the provider that will be
/// used to resolve the client registration, if applicable.
/// </summary>
public string? ProviderName { get; set; }
/// <summary>
/// Gets or sets the grant type used for the authentication demand, if applicable.
/// </summary>
@ -878,13 +890,19 @@ public static partial class OpenIddictClientEvents
public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the issuer used for the challenge demand, if applicable.
/// Gets or sets the identifier that will be used to resolve the client registration, if applicable.
/// </summary>
public string? RegistrationId { get; set; }
/// <summary>
/// Gets or sets the issuer URI of the provider that will be
/// used to resolve the client registration, if applicable.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary>
/// Gets or sets the name of the provider that will be
/// used to resolve the issuer identity, if applicable.
/// used to resolve the client registration, if applicable.
/// </summary>
public string? ProviderName { get; set; }
@ -1189,13 +1207,19 @@ public static partial class OpenIddictClientEvents
public Dictionary<string, string?> Properties { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets or sets the issuer used for the sign-out demand, if applicable.
/// Gets or sets the identifier that will be used to resolve the client registration, if applicable.
/// </summary>
public string? RegistrationId { get; set; }
/// <summary>
/// Gets or sets the issuer URI of the provider that will be
/// used to resolve the client registration, if applicable.
/// </summary>
public Uri? Issuer { get; set; }
/// <summary>
/// Gets or sets the name of the provider that will be
/// used to resolve the issuer identity, if applicable.
/// used to resolve the client registration, if applicable.
/// </summary>
public string? ProviderName { get; set; }

260
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -325,15 +325,14 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0311));
}
// If no issuer was explicitly attached and a single client is registered, use it.
// Otherwise, throw an exception to indicate that setting an explicit issuer
// is required when multiple clients are registered.
context.Issuer ??= context.Options.Registrations.Count switch
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
1 => context.Options.Registrations[0].Issuer,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0355))
};
throw context.Options.Registrations.Count is 0 ?
new InvalidOperationException(SR.GetResourceString(SR.ID0304)) :
new InvalidOperationException(SR.GetResourceString(SR.ID0355));
}
}
break;
@ -377,15 +376,48 @@ public static partial class OpenIddictClientHandlers
//
// Client registrations/configurations that need to be resolved as part of authentication demands
// triggered from the redirection or post-logout redirection requests are handled elsewhere.
if (context.Issuer is null || context.EndpointType is not OpenIddictClientEndpointType.Unknown)
if (!string.IsNullOrEmpty(context.Nonce) || context.EndpointType is not OpenIddictClientEndpointType.Unknown)
{
return;
}
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Registration ??= await _service.GetClientRegistrationAsync(context.Issuer, context.CancellationToken);
context.Registration ??= context switch
{
// If specified, resolve the registration using the attached registration identifier.
{ RegistrationId: string identifier } when !string.IsNullOrEmpty(identifier)
=> await _service.GetClientRegistrationByIdAsync(identifier, context.CancellationToken),
// If specified, resolve the registration using the attached issuer URI.
{ Issuer: Uri issuer } => await _service.GetClientRegistrationAsync(issuer, context.CancellationToken),
// If specified, resolve the registration using the attached provider name.
{ ProviderName: string provider } when !string.IsNullOrEmpty(provider)
=> await _service.GetClientRegistrationAsync(provider, context.CancellationToken),
// Otherwise, default to the unique registration available, if possible.
{ Options.Registrations: [OpenIddictClientRegistration registration] } => registration,
// If no registration was added or multiple registrations are present, throw an exception.
{ Options.Registrations: [] } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
{ Options.Registrations: _ } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0355))
};
if (!string.IsNullOrEmpty(context.RegistrationId) &&
!string.Equals(context.RegistrationId, context.Registration.RegistrationId, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0348));
}
if (!string.IsNullOrEmpty(context.ProviderName) &&
!string.Equals(context.ProviderName, context.Registration.ProviderName, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0349));
}
if (context.Issuer is not null && context.Issuer != context.Registration.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0408));
}
try
{
@ -967,19 +999,15 @@ public static partial class OpenIddictClientHandlers
Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Retrieve the client definition using the authorization server stored in the state token.
// Retrieve the client registration using the private claim stored in the state token.
//
// Note: there's no guarantee that the state token was not replaced by a malicious actor
// with a state token meant to be used with a different authorization server as part of a
// mix-up attack where the state token and the authorization code or access/identity tokens
// wouldn't match. To mitigate this, additional defenses are added later by other handlers.
// Restore the identity of the authorization server from the special "as" claim.
// See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09#section-2
// for more information.
var server = context.StateTokenPrincipal.GetClaim(Claims.AuthorizationServer);
if (string.IsNullOrEmpty(server) || !Uri.TryCreate(server, UriKind.Absolute, out Uri? issuer) ||
!issuer.IsWellFormedOriginalString())
context.RegistrationId = context.StateTokenPrincipal.GetClaim(Claims.Private.RegistrationId);
if (string.IsNullOrEmpty(context.RegistrationId))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0291));
}
@ -987,17 +1015,7 @@ public static partial class OpenIddictClientHandlers
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Issuer = issuer;
context.Registration = await _service.GetClientRegistrationAsync(issuer, context.CancellationToken);
// If an explicit provider name was also added, ensure the two values point to the same issuer.
var provider = context.StateTokenPrincipal.GetClaim(Claims.Private.ProviderName);
if (!string.IsNullOrEmpty(provider) &&
!string.IsNullOrEmpty(context.Registration.ProviderName) &&
!string.Equals(provider, context.Registration.ProviderName, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0349));
}
context.Registration = await _service.GetClientRegistrationByIdAsync(context.RegistrationId, context.CancellationToken);
try
{
@ -1046,8 +1064,6 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// To help mitigate mix-up attacks, the identity of the issuer can be returned by
// authorization servers that support it as a part of the "iss" parameter, which
// allows comparing it to the issuer in the state token. Depending on the selected
@ -1078,7 +1094,7 @@ public static partial class OpenIddictClientHandlers
// If the two values don't match, this may indicate a mix-up attack attempt.
if (!Uri.TryCreate(issuer, UriKind.Absolute, out Uri? uri) ||
!uri.IsWellFormedOriginalString() || uri != context.Issuer)
!uri.IsWellFormedOriginalString() || uri != context.Registration.Issuer)
{
context.Reject(
error: Errors.InvalidRequest,
@ -2340,7 +2356,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// Create a new principal that will be used to store the client assertion claims.
var principal = new ClaimsPrincipal(new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType));
@ -2365,7 +2381,7 @@ public static partial class OpenIddictClientHandlers
// If the token endpoint URI is not available, use the issuer URI as the audience.
else
{
principal.SetAudiences(context.Issuer.OriginalString);
principal.SetAudiences(context.Registration.Issuer.OriginalString);
}
// Use the client_id as both the subject and the issuer, as required by the specifications.
@ -2546,7 +2562,8 @@ public static partial class OpenIddictClientHandlers
try
{
context.TokenResponse = await _service.SendTokenRequestAsync(
context.Registration, context.TokenRequest, context.TokenEndpoint);
context.Registration, context.Configuration,
context.TokenRequest, context.TokenEndpoint);
}
catch (ProtocolException exception)
@ -3554,7 +3571,9 @@ public static partial class OpenIddictClientHandlers
try
{
(context.UserinfoResponse, (context.UserinfoTokenPrincipal, context.UserinfoToken)) =
await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoRequest, context.UserinfoEndpoint);
await _service.SendUserinfoRequestAsync(
context.Registration, context.Configuration,
context.UserinfoRequest, context.UserinfoEndpoint);
}
catch (ProtocolException exception)
@ -3913,32 +3932,15 @@ public static partial class OpenIddictClientHandlers
}
}
// If a provider name was specified, resolve the corresponding issuer.
if (!string.IsNullOrEmpty(context.ProviderName))
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
var registration = context.Options.Registrations.Find(registration => string.Equals(
registration.ProviderName, context.ProviderName, StringComparison.Ordinal)) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0348));
// If an explicit issuer was also attached, ensure the two values point to the same instance.
if (context.Issuer is not null && context.Issuer != registration.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0349));
}
context.Issuer = registration.Issuer;
throw context.Options.Registrations.Count is 0 ?
new InvalidOperationException(SR.GetResourceString(SR.ID0304)) :
new InvalidOperationException(SR.GetResourceString(SR.ID0305));
}
// If no issuer was explicitly attached and a single client is registered, use it.
// Otherwise, throw an exception to indicate that setting an explicit issuer
// is required when multiple clients are registered.
context.Issuer ??= context.Options.Registrations.Count switch
{
0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
1 => context.Options.Registrations[0].Issuer,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0305))
};
return default;
}
}
@ -3971,12 +3973,43 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
context.Registration ??= context switch
{
// If specified, resolve the registration using the attached registration identifier.
{ RegistrationId: string identifier } when !string.IsNullOrEmpty(identifier)
=> await _service.GetClientRegistrationByIdAsync(identifier, context.CancellationToken),
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Registration ??= await _service.GetClientRegistrationAsync(context.Issuer, context.CancellationToken);
// If specified, resolve the registration using the attached issuer URI.
{ Issuer: Uri issuer } => await _service.GetClientRegistrationAsync(issuer, context.CancellationToken),
// If specified, resolve the registration using the attached provider name.
{ ProviderName: string provider } when !string.IsNullOrEmpty(provider)
=> await _service.GetClientRegistrationAsync(provider, context.CancellationToken),
// Otherwise, default to the unique registration available, if possible.
{ Options.Registrations: [OpenIddictClientRegistration registration] } => registration,
// If no registration was added or multiple registrations are present, throw an exception.
{ Options.Registrations: [] } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
{ Options.Registrations: _ } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0305))
};
if (!string.IsNullOrEmpty(context.RegistrationId) &&
!string.Equals(context.RegistrationId, context.Registration.RegistrationId, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0348));
}
if (!string.IsNullOrEmpty(context.ProviderName) &&
!string.Equals(context.ProviderName, context.Registration.ProviderName, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0349));
}
if (context.Issuer is not null && context.Issuer != context.Registration.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0408));
}
try
{
@ -4704,7 +4737,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Create a new principal containing only the filtered claims.
@ -4743,14 +4776,15 @@ public static partial class OpenIddictClientHandlers
// Use the client identity as the token issuer.
principal.SetClaim(Claims.Private.Issuer, (context.Options.ClientUri ?? context.BaseUri)?.AbsoluteUri);
// Store the identity of the authorization server in the state token principal to allow
// Store the identifier of the client registration in the state token principal to allow
// resolving it when handling the authorization callback. Note: additional security checks
// are generally required to ensure the state token was not replaced with a state token
// meant to be used with a different authorization server (e.g using the "iss" parameter).
//
// See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09
// for more information about this special claim.
principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri)
// for more information about the "as" claim.
principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
// Store the request forgery protection in the state token so it can be later used to
@ -5140,7 +5174,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
// Create a new principal that will be used to store the client assertion claims.
var principal = new ClaimsPrincipal(new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType));
@ -5154,7 +5188,7 @@ public static partial class OpenIddictClientHandlers
// Use the issuer URI as the audience. Applications that need to
// use a different value can register a custom event handler.
principal.SetAudiences(context.Issuer.OriginalString);
principal.SetAudiences(context.Registration.Issuer.OriginalString);
// Use the client_id as both the subject and the issuer, as required by the specifications.
principal.SetClaim(Claims.Private.Issuer, context.ClientId)
@ -5331,7 +5365,8 @@ public static partial class OpenIddictClientHandlers
try
{
context.DeviceAuthorizationResponse = await _service.SendDeviceAuthorizationRequestAsync(
context.Registration, context.DeviceAuthorizationRequest, context.DeviceAuthorizationEndpoint);
context.Registration, context.Configuration,
context.DeviceAuthorizationRequest, context.DeviceAuthorizationEndpoint);
}
catch (ProtocolException exception)
@ -5517,32 +5552,15 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0358));
}
// If a provider name was specified, resolve the corresponding issuer.
if (!string.IsNullOrEmpty(context.ProviderName))
if (context.Registration is null && string.IsNullOrEmpty(context.RegistrationId) &&
context.Issuer is null && string.IsNullOrEmpty(context.ProviderName) &&
context.Options.Registrations.Count is not 1)
{
var registration = context.Options.Registrations.Find(registration => string.Equals(
registration.ProviderName, context.ProviderName, StringComparison.Ordinal)) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0348));
// If an explicit issuer was also attached, ensure the two values point to the same instance.
if (context.Issuer is not null && context.Issuer != registration.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0349));
}
context.Issuer = registration.Issuer;
throw context.Options.Registrations.Count is 0 ?
new InvalidOperationException(SR.GetResourceString(SR.ID0304)) :
new InvalidOperationException(SR.GetResourceString(SR.ID0341));
}
// If no issuer was explicitly attached and a single client is registered, use it.
// Otherwise, throw an exception to indicate that setting an explicit issuer
// is required when multiple clients are registered.
context.Issuer ??= context.Options.Registrations.Count switch
{
0 => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
1 => context.Options.Registrations[0].Issuer,
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0341))
};
return default;
}
}
@ -5575,12 +5593,43 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
context.Registration ??= context switch
{
// If specified, resolve the registration using the attached registration identifier.
{ RegistrationId: string identifier } when !string.IsNullOrEmpty(identifier)
=> await _service.GetClientRegistrationByIdAsync(identifier, context.CancellationToken),
// Note: if the static registration cannot be found in the options, this may indicate
// the client was removed after the authorization dance started and thus, can no longer
// be used to authenticate users. In this case, throw an exception to abort the flow.
context.Registration ??= await _service.GetClientRegistrationAsync(context.Issuer, context.CancellationToken);
// If specified, resolve the registration using the attached issuer URI.
{ Issuer: Uri issuer } => await _service.GetClientRegistrationAsync(issuer, context.CancellationToken),
// If specified, resolve the registration using the attached provider name.
{ ProviderName: string provider } when !string.IsNullOrEmpty(provider)
=> await _service.GetClientRegistrationAsync(provider, context.CancellationToken),
// Otherwise, default to the unique registration available, if possible.
{ Options.Registrations: [OpenIddictClientRegistration registration] } => registration,
// If no registration was added or multiple registrations are present, throw an exception.
{ Options.Registrations: [] } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0304)),
{ Options.Registrations: _ } => throw new InvalidOperationException(SR.GetResourceString(SR.ID0341))
};
if (!string.IsNullOrEmpty(context.RegistrationId) &&
!string.Equals(context.RegistrationId, context.Registration.RegistrationId, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0348));
}
if (!string.IsNullOrEmpty(context.ProviderName) &&
!string.Equals(context.ProviderName, context.Registration.ProviderName, StringComparison.Ordinal))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0349));
}
if (context.Issuer is not null && context.Issuer != context.Registration.Issuer)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0408));
}
try
{
@ -5812,7 +5861,7 @@ public static partial class OpenIddictClientHandlers
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Registration.Issuer is { IsAbsoluteUri: true }, SR.GetResourceString(SR.ID4013));
Debug.Assert(context.Principal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// Create a new principal containing only the filtered claims.
@ -5851,12 +5900,13 @@ public static partial class OpenIddictClientHandlers
// Use the client identity as the token issuer.
principal.SetClaim(Claims.Private.Issuer, (context.Options.ClientUri ?? context.BaseUri)?.AbsoluteUri);
// Store the identity of the authorization server in the state token
// Store the identifier of the client registration in the state token
// principal to allow resolving it when handling the post-logout callback.
//
// See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09
// for more information about this special claim.
principal.SetClaim(Claims.AuthorizationServer, context.Issuer.AbsoluteUri)
// for more information about the "as" claim.
principal.SetClaim(Claims.AuthorizationServer, context.Registration.Issuer.AbsoluteUri)
.SetClaim(Claims.Private.RegistrationId, context.Registration.RegistrationId)
.SetClaim(Claims.Private.ProviderName, context.Registration.ProviderName);
// Store the request forgery protection in the state token so it can be later used to

760
src/OpenIddict.Client/OpenIddictClientModels.cs

@ -0,0 +1,760 @@
/*
* 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.ComponentModel;
using System.Security.Claims;
namespace OpenIddict.Client;
/// <summary>
/// Exposes various records used to represent client requests and responses.
/// </summary>
public static class OpenIddictClientModels
{
/// <summary>
/// Represents an interactive authentication request.
/// </summary>
public sealed record class InteractiveAuthenticationRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the token request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the nonce that was returned during the challenge operation.
/// </summary>
public required string Nonce { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
}
/// <summary>
/// Represents an interactive authentication result.
/// </summary>
public sealed record class InteractiveAuthenticationResult
{
/// <summary>
/// Gets or sets the authorization code, if available.
/// </summary>
public required string? AuthorizationCode { get; init; }
/// <summary>
/// Gets or sets the authorization response.
/// </summary>
public required OpenIddictResponse AuthorizationResponse { get; init; }
/// <summary>
/// Gets or sets the backchannel access token, if available.
/// </summary>
public required string? BackchannelAccessToken { get; init; }
/// <summary>
/// Gets or sets the backchannel identity token, if available.
/// </summary>
public required string? BackchannelIdentityToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the backchannel identity token, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? BackchannelIdentityTokenPrincipal { get; init; }
/// <summary>
/// Gets or sets the frontchannel access token, if available.
/// </summary>
public required string? FrontchannelAccessToken { get; init; }
/// <summary>
/// Gets or sets the frontchannel identity token, if available.
/// </summary>
public required string? FrontchannelIdentityToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the frontchannel identity token, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? FrontchannelIdentityTokenPrincipal { get; init; }
/// <summary>
/// Gets or sets a merged principal containing all the claims
/// extracted from the identity token and userinfo token principals.
/// </summary>
public required ClaimsPrincipal Principal { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
/// <summary>
/// Gets or sets the refresh token, if available.
/// </summary>
public required string? RefreshToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the state token, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? StateTokenPrincipal { get; init; }
/// <summary>
/// Gets or sets the token response.
/// </summary>
public required OpenIddictResponse TokenResponse { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the userinfo token or response, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; }
}
/// <summary>
/// Represents an interactive challenge request.
/// </summary>
public sealed record class InteractiveChallengeRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the authorization request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalAuthorizationRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents an interactive challenge result.
/// </summary>
public sealed record class InteractiveChallengeResult
{
/// <summary>
/// Gets or sets the nonce that is used as a unique identifier for the challenge operation.
/// </summary>
public required string Nonce { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
}
/// <summary>
/// Represents a client credentials authentication request.
/// </summary>
public sealed record class ClientCredentialsAuthenticationRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the token request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents a client credentials authentication result.
/// </summary>
public sealed record class ClientCredentialsAuthenticationResult
{
/// <summary>
/// Gets or sets the access token.
/// </summary>
public required string AccessToken { get; init; }
/// <summary>
/// Gets or sets the identity token, if available.
/// </summary>
/// <remarks>
/// Note: this property is generally not set, unless when dealing with an identity
/// provider that returns an identity token for the client credentials grant.
/// </remarks>
public required string? IdentityToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the identity token, if available.
/// </summary>
/// <remarks>
/// Note: this property is generally not set, unless when dealing with an identity
/// provider that returns an identity token for the client credentials grant.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; }
/// <summary>
/// Gets or sets a merged principal containing all the claims
/// extracted from the identity token and userinfo token principals.
/// </summary>
/// <remarks>
/// Note: in most cases, an empty principal will be returned, unless the authorization server
/// supports returning a non-standard identity token for the client credentials grant or
/// allows sending userinfo requests with an access token representing a client application.
/// </remarks>
public required ClaimsPrincipal Principal { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
/// <summary>
/// Gets or sets the refresh token, if available.
/// </summary>
public required string? RefreshToken { get; init; }
/// <summary>
/// Gets or sets the token response.
/// </summary>
public required OpenIddictResponse TokenResponse { get; init; }
/// <summary>
/// Gets or sets the userinfo token, if available.
/// </summary>
/// <remarks>
/// Note: this property is generally not set, unless when dealing with non-standard providers.
/// </remarks>
public required string? UserinfoToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the userinfo token or response, if available.
/// </summary>
/// <remarks>
/// Note: this property is generally not set, unless when dealing with non-standard providers.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; }
}
/// <summary>
/// Represents a device authentication request.
/// </summary>
public sealed record class DeviceAuthenticationRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the token request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the device code that will be sent to the authorization server.
/// </summary>
public required string DeviceCode { get; init; }
/// <summary>
/// Gets or sets the maximum duration during which token requests will be sent
/// (typically, the same value as the "expires_in" parameter returned by the
/// authorization server during the challenge phase or a lower value).
/// </summary>
public required TimeSpan Timeout { get; init; }
/// <summary>
/// Gets or sets the interval at which token requests will be sent (typically, the same
/// value as the one returned by the authorization server during the challenge phase).
/// </summary>
public required TimeSpan Interval { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents a device authentication result.
/// </summary>
public sealed record class DeviceAuthenticationResult
{
/// <summary>
/// Gets or sets the access token.
/// </summary>
public required string AccessToken { get; init; }
/// <summary>
/// Gets or sets the identity token, if available.
/// </summary>
public required string? IdentityToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the identity token, if available.
/// </summary>
///
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; }
/// <summary>
/// Gets or sets a merged principal containing all the claims
/// extracted from the identity token and userinfo token principals.
/// </summary>
public required ClaimsPrincipal Principal { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
/// <summary>
/// Gets or sets the refresh token, if available.
/// </summary>
public required string? RefreshToken { get; init; }
/// <summary>
/// Gets or sets the token response.
/// </summary>
public required OpenIddictResponse TokenResponse { get; init; }
/// <summary>
/// Gets or sets the userinfo token, if available.
/// </summary>
public required string? UserinfoToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the userinfo token or response, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; }
}
/// <summary>
/// Represents a device challenge request.
/// </summary>
public sealed record class DeviceChallengeRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the device authorization request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalDeviceAuthorizationRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents a device challenge result.
/// </summary>
public sealed record class DeviceChallengeResult
{
/// <summary>
/// Gets or sets the device authorization response.
/// </summary>
public required OpenIddictResponse DeviceAuthorizationResponse { get; init; }
/// <summary>
/// Gets or sets the device code.
/// </summary>
public required string DeviceCode { get; init; }
/// <summary>
/// Gets or sets the remaining lifetime of the device and user codes.
/// </summary>
public required TimeSpan ExpiresIn { get; init; }
/// <summary>
/// Gets or sets the interval at which token requests should be sent.
/// </summary>
public required TimeSpan Interval { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
/// <summary>
/// Gets or sets the user code.
/// </summary>
public required string UserCode { get; init; }
/// <summary>
/// Gets or sets the verification URI.
/// </summary>
public required Uri VerificationUri { get; init; }
/// <summary>
/// Gets or sets the complete verification URI, if available.
/// </summary>
public Uri? VerificationUriComplete { get; init; }
}
/// <summary>
/// Represents a resource owner password credentials authentication request.
/// </summary>
public sealed record class PasswordAuthenticationRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the token request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the password that will be sent to the authorization server.
/// </summary>
public required string Password { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the username that will be sent to the authorization server.
/// </summary>
public required string Username { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents a resource owner password credentials authentication result.
/// </summary>
public sealed record class PasswordAuthenticationResult
{
/// <summary>
/// Gets or sets the access token.
/// </summary>
public required string AccessToken { get; init; }
/// <summary>
/// Gets or sets the identity token, if available.
/// </summary>
public required string? IdentityToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the identity token, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; }
/// <summary>
/// Gets or sets a merged principal containing all the claims
/// extracted from the identity token and userinfo token principals.
/// </summary>
public required ClaimsPrincipal Principal { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
/// <summary>
/// Gets or sets the refresh token, if available.
/// </summary>
public required string? RefreshToken { get; init; }
/// <summary>
/// Gets or sets the token response.
/// </summary>
public required OpenIddictResponse TokenResponse { get; init; }
/// <summary>
/// Gets or sets the userinfo token, if available.
/// </summary>
public required string? UserinfoToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the userinfo token or response, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; }
}
/// <summary>
/// Represents a refresh token authentication request.
/// </summary>
public sealed record class RefreshTokenAuthenticationRequest
{
/// <summary>
/// Gets or sets the parameters that will be added to the token request.
/// </summary>
public Dictionary<string, OpenIddictParameter>? AdditionalTokenRequestParameters { get; init; }
/// <summary>
/// Gets or sets the cancellation token that will be
/// used to determine if the operation was aborted.
/// </summary>
public CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that will be added to the context.
/// </summary>
public Dictionary<string, string?>? Properties { get; init; }
/// <summary>
/// Gets or sets the provider name used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations use the same provider name.
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public string? ProviderName { get; init; }
/// <summary>
/// Gets or sets the unique identifier of the client registration that will be used.
/// </summary>
public string? RegistrationId { get; init; }
/// <summary>
/// Gets the scopes that will be sent to the authorization server.
/// </summary>
public List<string>? Scopes { get; init; }
/// <summary>
/// Gets or sets the refresh token that will be sent to the authorization server.
/// </summary>
public required string RefreshToken { get; init; }
/// <summary>
/// Gets or sets the issuer used to resolve the client registration.
/// </summary>
/// <remarks>
/// Note: if multiple client registrations point to the same issuer,
/// the <see cref="RegistrationId"/> property must be explicitly set.
/// </remarks>
public Uri? Issuer { get; init; }
}
/// <summary>
/// Represents a refresh token authentication result.
/// </summary>
public sealed record class RefreshTokenAuthenticationResult
{
/// <summary>
/// Gets or sets the access token.
/// </summary>
public required string AccessToken { get; init; }
/// <summary>
/// Gets or sets the identity token, if available.
/// </summary>
public required string? IdentityToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the identity token, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? IdentityTokenPrincipal { get; init; }
/// <summary>
/// Gets or sets a merged principal containing all the claims
/// extracted from the identity token and userinfo token principals.
/// </summary>
public required ClaimsPrincipal Principal { get; init; }
/// <summary>
/// Gets or sets the application-specific properties that were present in the context.
/// </summary>
public required Dictionary<string, string?> Properties { get; init; }
/// <summary>
/// Gets or sets the refresh token, if available.
/// </summary>
public required string? RefreshToken { get; init; }
/// <summary>
/// Gets or sets the token response.
/// </summary>
public required OpenIddictResponse TokenResponse { get; init; }
/// <summary>
/// Gets or sets the userinfo token, if available.
/// </summary>
public required string? UserinfoToken { get; init; }
/// <summary>
/// Gets or sets the principal extracted from the userinfo token or response, if available.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public required ClaimsPrincipal? UserinfoTokenPrincipal { get; init; }
}
}

31
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -16,6 +16,11 @@ namespace OpenIddict.Client;
[DebuggerDisplay("{Issuer,nq}")]
public sealed class OpenIddictClientRegistration
{
/// <summary>
/// Gets or sets the unique identifier assigned to the registration.
/// </summary>
public string? RegistrationId { get; set; }
/// <summary>
/// Gets or sets the client identifier assigned by the authorization server.
/// </summary>
@ -104,18 +109,36 @@ public sealed class OpenIddictClientRegistration
public Uri? Issuer { get; set; }
/// <summary>
/// Gets or sets the provider name, if applicable.
/// Gets or sets the provider name.
/// </summary>
/// <remarks>
/// If a Web provider integration with the same name was enabled, the
/// provider-specific options will be automatically imported and applied.
/// The provider name can be safely used as a stable public identifier.
/// </remarks>
public string? ProviderName { get; set; }
/// <summary>
/// Gets or sets the provider options, if applicable.
/// </summary>
public dynamic? ProviderOptions { get; set; }
[Obsolete($"This property was replaced by {nameof(ProviderSettings)} and will be removed in a future version.")]
public dynamic? ProviderOptions
{
get => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
set => throw new NotSupportedException(SR.GetResourceString(SR.ID0403));
}
/// <summary>
/// Gets or sets the provider settings, if applicable.
/// </summary>
public dynamic? ProviderSettings { get; set; }
/// <summary>
/// Gets or sets the provider type, if applicable.
/// </summary>
/// <remarks>
/// Note: when manually set, the specified value MUST match the type of an existing
/// provider supported by the OpenIddict.Client.WebIntegration companion package.
/// </remarks>
public string? ProviderType { get; set; }
/// <summary>
/// Gets or sets the static server configuration, if applicable.

1013
src/OpenIddict.Client/OpenIddictClientService.cs

File diff suppressed because it is too large

13
src/OpenIddict.Validation/OpenIddictValidationConfiguration.cs

@ -44,10 +44,17 @@ public sealed class OpenIddictValidationConfiguration : IPostConfigureOptions<Op
throw new InvalidOperationException(SR.GetResourceString(SR.ID0128));
}
if (options.Issuer is not null && (!string.IsNullOrEmpty(options.Issuer.Fragment) ||
!string.IsNullOrEmpty(options.Issuer.Query)))
if (options.Issuer is not null)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0137));
if (!options.Issuer.IsAbsoluteUri || !options.Issuer.IsWellFormedOriginalString())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0136));
}
if (!string.IsNullOrEmpty(options.Issuer.Fragment) || !string.IsNullOrEmpty(options.Issuer.Query))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0137));
}
}
if (options.ValidationType is OpenIddictValidationType.Introspection)

Loading…
Cancel
Save