Browse Source

Introduce a web providers companion package

pull/1453/head
Kévin Chalet 4 years ago
committed by GitHub
parent
commit
68ae22d8b5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      OpenIddict.sln
  2. 8
      Packages.props
  3. 39
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddict.Client.WebIntegration.Generators.csproj
  4. 621
      gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
  5. 27
      sandbox/OpenIddict.Sandbox.AspNet.Client/Controllers/AuthenticationController.cs
  6. 1
      sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj
  7. 28
      sandbox/OpenIddict.Sandbox.AspNet.Client/Startup.cs
  8. 20
      sandbox/OpenIddict.Sandbox.AspNet.Client/Views/Home/Index.cshtml
  9. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Startup.cs
  10. 28
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Controllers/AuthenticationController.cs
  11. 1
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj
  12. 37
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Startup.cs
  13. 21
      sandbox/OpenIddict.Sandbox.AspNetCore.Client/Views/Home/Index.cshtml
  14. 6
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  15. 14
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  16. 40
      src/OpenIddict.Client.WebIntegration/OpenIddict.Client.WebIntegration.csproj
  17. 61
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationBuilder.cs
  18. 31
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConfiguration.cs
  19. 21
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs
  20. 15
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationEnvironments.cs
  21. 70
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationExtensions.cs
  22. 64
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Discovery.cs
  23. 21
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Exchange.cs
  24. 21
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
  25. 249
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
  26. 66
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs
  27. 18
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationOptions.cs
  28. 46
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProvider.cs
  29. 51
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
  30. 349
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xsd
  31. 15
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationScopes.cs
  32. 33
      src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationSettings.cs
  33. 14
      src/OpenIddict.Client/OpenIddictClientBuilder.cs
  34. 5
      src/OpenIddict.Client/OpenIddictClientRegistration.cs
  35. 1
      src/OpenIddict/OpenIddict.csproj

16
OpenIddict.sln

@ -126,6 +126,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.AspNet.C
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.DataProtection", "src\OpenIddict.Client.DataProtection\OpenIddict.Client.DataProtection.csproj", "{E4D77737-4C73-4520-99E8-8A9E586C69A1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.WebIntegration", "src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj", "{867317E3-364E-4F4D-9D6D-A206E1F72B9F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{4CF2AFFA-A31B-4925-ADF4-062E9BDD1381}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenIddict.Client.WebIntegration.Generators", "gen\OpenIddict.Client.WebIntegration.Generators\OpenIddict.Client.WebIntegration.Generators.csproj", "{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -300,6 +306,14 @@ Global
{E4D77737-4C73-4520-99E8-8A9E586C69A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4D77737-4C73-4520-99E8-8A9E586C69A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4D77737-4C73-4520-99E8-8A9E586C69A1}.Release|Any CPU.Build.0 = Release|Any CPU
{867317E3-364E-4F4D-9D6D-A206E1F72B9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{867317E3-364E-4F4D-9D6D-A206E1F72B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{867317E3-364E-4F4D-9D6D-A206E1F72B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{867317E3-364E-4F4D-9D6D-A206E1F72B9F}.Release|Any CPU.Build.0 = Release|Any CPU
{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -347,6 +361,8 @@ Global
{0DFA4EC2-035A-46D3-AAF6-4BF1DFBC1040} = {F47D1283-0EE9-4728-8026-58405C29B786}
{BFF5B862-D5FB-4019-8647-C43E5E7EF97D} = {F47D1283-0EE9-4728-8026-58405C29B786}
{E4D77737-4C73-4520-99E8-8A9E586C69A1} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{867317E3-364E-4F4D-9D6D-A206E1F72B9F} = {D544447C-D701-46BB-9A5B-C76C612A596B}
{24DEAE71-7BED-4A2A-B10D-085A1EF5B4B2} = {4CF2AFFA-A31B-4925-ADF4-062E9BDD1381}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A710059F-0466-4D48-9B3A-0EF4F840B616}

8
Packages.props

@ -15,6 +15,8 @@
<PackageReference Update="Microsoft.AspNet.Web.Optimization" Version="1.1.3" />
<PackageReference Update="Microsoft.AspNet.WebApi.Owin" Version="5.2.8" />
<PackageReference Update="Microsoft.Bcl.HashCode" Version="1.1.1" />
<PackageReference Update="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" />
<PackageReference Update="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
<PackageReference Update="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" Version="3.6.0" />
<PackageReference Update="Microsoft.IdentityModel.JsonWebTokens" Version="6.18.0" />
<PackageReference Update="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="6.18.0" />
@ -37,6 +39,8 @@
<PackageReference Update="Quartz.Extensions.DependencyInjection" Version="3.4.0" />
<PackageReference Update="Quartz.Extensions.Hosting" Version="3.4.0" />
<PackageReference Update="WebGrease" Version="1.6.0" />
<PackageReference Update="Scriban" Version="5.4.1" />
<PackageReference Update="SmartFormat" Version="3.0.0" />
</ItemGroup>
<ItemGroup
@ -62,6 +66,7 @@
<PackageReference Update="Microsoft.Extensions.WebEncoders" Version="2.1.1" />
<PackageReference Update="System.Collections.Immutable" Version="1.7.1" />
<PackageReference Update="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Update="System.Interactive" Version="4.1.1" />
<PackageReference Update="System.Linq.Async" Version="4.1.1" />
<PackageReference Update="System.Net.Http.Json" Version="3.2.1" />
<PackageReference Update="System.Text.Encodings.Web" Version="4.7.2" />
@ -88,6 +93,7 @@
<PackageReference Update="Microsoft.Extensions.WebEncoders" Version="3.1.25" />
<PackageReference Update="System.Collections.Immutable" Version="1.7.1" />
<PackageReference Update="System.ComponentModel.Annotations" Version="4.7.0" />
<PackageReference Update="System.Interactive" Version="4.1.1" />
<PackageReference Update="System.Linq.Async" Version="4.1.1" />
<PackageReference Update="System.Net.Http.Json" Version="3.2.1" />
<PackageReference Update="System.Text.Encodings.Web" Version="4.7.2" />
@ -113,6 +119,7 @@
<PackageReference Update="Microsoft.Extensions.WebEncoders" Version="5.0.17" />
<PackageReference Update="System.Collections.Immutable" Version="5.0.0" />
<PackageReference Update="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Update="System.Interactive" Version="4.1.1" />
<PackageReference Update="System.Linq.Async" Version="5.0.0" />
<PackageReference Update="System.Net.Http.Json" Version="5.0.0" />
<PackageReference Update="System.Text.Encodings.Web" Version="5.0.1" />
@ -138,6 +145,7 @@
<PackageReference Update="Microsoft.Extensions.WebEncoders" Version="6.0.5" />
<PackageReference Update="System.Collections.Immutable" Version="6.0.0" />
<PackageReference Update="System.ComponentModel.Annotations" Version="6.0.0" />
<PackageReference Update="System.Interactive" Version="4.1.1" />
<PackageReference Update="System.Linq.Async" Version="6.0.1" />
<PackageReference Update="System.Net.Http.Json" Version="6.0.0" />
<PackageReference Update="System.Text.Encodings.Web" Version="6.0.0" />

39
gen/OpenIddict.Client.WebIntegration.Generators/OpenIddict.Client.WebIntegration.Generators.csproj

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<SignAssembly>false</SignAssembly>
<PublicSign>false</PublicSign>
<IsPackable>false</IsPackable>
<IsShipping>false</IsShipping>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" PrivateAssets="all" />
<PackageReference Include="Scriban" PrivateAssets="all" GeneratePathProperty="true" />
<PackageReference Include="System.Interactive" PrivateAssets="all" GeneratePathProperty="true" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
</ItemGroup>
<PropertyGroup>
<GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
</PropertyGroup>
<Target Name="GetDependencyTargetPaths">
<ItemGroup>
<TargetPathWithTargetPlatformMoniker Include="$(PKGScriban)\lib\netstandard2.0\Scriban.dll" IncludeRuntimeDependency="false" />
<TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Interactive)\lib\netstandard2.0\System.Interactive.dll" IncludeRuntimeDependency="false" />
</ItemGroup>
</Target>
</Project>

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

@ -0,0 +1,621 @@
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using Scriban;
namespace OpenIddict.Client.WebIntegration.Generators
{
[Generator]
public class OpenIddictClientWebIntegrationGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
var file = context.AdditionalFiles.Select(file => file.Path)
.Where(path => string.Equals(Path.GetFileName(path), "OpenIddictClientWebIntegrationProviders.xml"))
.SingleOrDefault();
if (string.IsNullOrEmpty(file))
{
return;
}
var document = XDocument.Load(file, LoadOptions.None);
context.AddSource(
"OpenIddictClientWebIntegrationBuilder.generated.cs",
SourceText.From(GenerateBuilderMethods(document), Encoding.UTF8));
context.AddSource(
"OpenIddictClientWebIntegrationConfiguration.generated.cs",
SourceText.From(GenerateConfigurationClasses(document), Encoding.UTF8));
context.AddSource(
"OpenIddictClientWebIntegrationConstants.generated.cs",
SourceText.From(GenerateConstants(document), Encoding.UTF8));
context.AddSource(
"OpenIddictClientWebIntegrationEnvironments.generated.cs",
SourceText.From(GenerateEnvironments(document), Encoding.UTF8));
context.AddSource(
"OpenIddictClientWebIntegrationHelpers.generated.cs",
SourceText.From(GenerateHelpers(document), Encoding.UTF8));
context.AddSource(
"OpenIddictClientWebIntegrationScopes.generated.cs",
SourceText.From(GenerateScopes(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
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Client;
using OpenIddict.Client.WebIntegration;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace Microsoft.Extensions.DependencyInjection;
public partial class OpenIddictClientWebIntegrationBuilder
{
{{~ for provider in providers ~}}
/// <summary>
/// Enables {{ provider.name }} integration using the specified settings.
/// </summary>
{{~ if provider.documentation ~}}
/// <remarks>
/// For more information about {{ provider.name }} integration, visit <see href=""{{ provider.documentation }}"">the official website</see>.
/// </remarks>
{{~ end ~}}
/// <param name=""settings"">The provider settings.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder""/>.</returns>
public OpenIddictClientWebIntegrationBuilder Add{{ provider.name }}(OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings)
{
if (settings is null)
{
throw new ArgumentNullException(nameof(settings));
}
// Note: TryAddEnumerable() is used here to ensure the initializer is registered only once.
Services.TryAddEnumerable(ServiceDescriptor.Singleton<
IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}>());
return Configure(options => options.Providers.Add(new OpenIddictClientWebIntegrationProvider(Providers.{{ provider.name }}, settings)));
}
{{~ end ~}}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new
{
Name = (string) provider.Attribute("Name"),
Documentation = (string?) provider.Attribute("Documentation")
})
.ToList()
});
}
static string GenerateConstants(XDocument document)
{
var template = Template.Parse(@"#nullable enable
namespace OpenIddict.Client.WebIntegration;
public static partial class OpenIddictClientWebIntegrationConstants
{
public static class Providers
{
{{~ for provider in providers ~}}
public const string {{ provider.name }} = ""{{ provider.name }}"";
{{~ end ~}}
}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new { Name = (string) provider.Attribute("Name") })
.ToList()
});
}
static string GenerateEnvironments(XDocument document)
{
var template = Template.Parse(@"#nullable enable
namespace OpenIddict.Client.WebIntegration;
public partial class OpenIddictClientWebIntegrationEnvironments
{
{{~ for provider in providers ~}}
/// <summary>
/// Exposes the environments supported by the {{ provider.name }} provider.
/// </summary>
public enum {{ provider.name }}
{
{{~ for environment in provider.environments ~}}
{{ environment.name }},
{{~ end ~}}
}
{{~ end ~}}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new
{
Name = (string) provider.Attribute("Name"),
Environments = provider.Elements("Environment").Select(environment => new
{
Name = (string?) environment.Attribute("Name") ?? "Production"
})
.ToList()
})
.ToList()
});
}
static string GenerateConfigurationClasses(XDocument document)
{
var template = Template.Parse(@"#nullable enable
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Client;
using SmartFormat;
using SmartFormat.Core.Settings;
using Properties = OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants.Properties;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
public partial class OpenIddictClientWebIntegrationConfiguration
{
{{~ for provider in providers ~}}
/// <summary>
/// Contains the methods required to register the {{ provider.name }} integration in the OpenIddict client options.
/// </summary>
public class {{ provider.name }} : IConfigureOptions<OpenIddictClientOptions>
{
private readonly IOptions<OpenIddictClientWebIntegrationOptions> _options;
/// <summary>
/// Creates a new instance of the <see cref=""OpenIddictClientWebIntegrationConfiguration.{{ provider.name }}"" /> class.
/// </summary>
/// <param name=""options"">The OpenIddict client web integration options.</param>
/// <exception cref=""ArgumentException""><paramref name=""options""/> is null.</exception>
public {{ provider.name }}(IOptions<OpenIddictClientWebIntegrationOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Ensures the {{ provider.name }} configuration is in a consistent and valid state
/// and registers the {{ provider.name }} integration in the OpenIddict client options.
/// </summary>
/// <param name=""options"">The options instance to initialize.</param>
public void Configure(OpenIddictClientOptions options)
{
foreach (var provider in _options.Value.Providers)
{
if (provider.Name is not Providers.{{ provider.name }})
{
continue;
}
if (provider.Settings is not OpenIddictClientWebIntegrationSettings.{{ provider.name }} settings)
{
throw new InvalidOperationException(SR.FormatID0331(Providers.{{ provider.name }}));
}
if (string.IsNullOrEmpty(settings.ClientId))
{
throw new InvalidOperationException(SR.FormatID0332(nameof(settings.ClientId), Providers.{{ provider.name }}));
}
if (settings.RedirectUri is null)
{
throw new InvalidOperationException(SR.FormatID0332(nameof(settings.RedirectUri), Providers.{{ provider.name }}));
}
{{~ for setting in provider.settings ~}}
{{~ if setting.required ~}}
{{~ if setting.type == 'string' ~}}
if (string.IsNullOrEmpty(settings.{{ setting.name }}))
{{~ else ~}}
if (settings.{{ setting.name }} is null)
{{~ end ~}}
{
throw new InvalidOperationException(SR.FormatID0332(nameof(settings.{{ setting.name }}), Providers.{{ provider.name }}));
}
{{~ end ~}}
{{~ end ~}}
var formatter = Smart.CreateDefaultSmartFormat(new SmartSettings
{
CaseSensitivity = CaseSensitivityType.CaseInsensitive
});
var registration = new OpenIddictClientRegistration
{
Issuer = settings.Environment switch
{
{{~ for environment in provider.environments ~}}
OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }}
=> new Uri(formatter.Format(""{{ environment.issuer }}"", settings), UriKind.Absolute),
{{~ end ~}}
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
},
ClientId = settings.ClientId,
ClientSecret = settings.ClientSecret,
RedirectUri = settings.RedirectUri,
Configuration = settings.Environment switch
{
{{~ for environment in provider.environments ~}}
{{~ if environment.configuration ~}}
OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }} => new OpenIddictConfiguration
{
{{~ if environment.configuration.authorization_endpoint ~}}
AuthorizationEndpoint = new Uri(formatter.Format(""{{ environment.configuration.authorization_endpoint }}"", settings), UriKind.Absolute),
{{~ end ~}}
{{~ if environment.configuration.token_endpoint ~}}
TokenEndpoint = new Uri(formatter.Format(""{{ environment.configuration.token_endpoint }}"", settings), UriKind.Absolute),
{{~ end ~}}
{{~ if environment.configuration.userinfo_endpoint ~}}
UserinfoEndpoint = new Uri(formatter.Format(""{{ environment.configuration.userinfo_endpoint }}"", settings), UriKind.Absolute),
{{~ 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 ~}}
},
TokenEndpointAuthMethodsSupported =
{
{{~ for method in environment.configuration.token_endpoint_auth_methods_supported ~}}
""{{ method }}"",
{{~ end ~}}
}
},
{{~ else ~}}
OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }} => null,
{{~ end ~}}
{{~ end ~}}
_ => throw new InvalidOperationException(SR.FormatID0194(nameof(settings.Environment)))
},
EncryptionCredentials =
{
{{~ for setting in provider.settings ~}}
{{~ if setting.encryption_algorithm ~}}
new EncryptingCredentials(settings.{{ setting.name }}, ""{{ setting.encryption_algorithm }}"", SecurityAlgorithms.Aes256CbcHmacSha512),
{{~ end ~}}
{{~ end ~}}
},
SigningCredentials =
{
{{~ for setting in provider.settings ~}}
{{~ if setting.signing_algorithm ~}}
new SigningCredentials(settings.{{ setting.name }}, ""{{ setting.signing_algorithm }}""),
{{~ end ~}}
{{~ end ~}}
},
Properties =
{
[Properties.ProviderName] = Providers.{{ provider.name }},
[Properties.ProviderSettings] = settings
}
};
registration.Scopes.UnionWith(settings.Scopes);
{{~ for environment in provider.environments ~}}
if (settings.Environment is OpenIddictClientWebIntegrationEnvironments.{{ provider.name }}.{{ environment.name }})
{
{{~ for scope in environment.scopes ~}}
{{~ if scope.required ~}}
registration.Scopes.Add(""{{ scope.name }}"");
{{~ end ~}}
{{~ if scope.default ~}}
if (registration.Scopes.Count is 0)
{
registration.Scopes.Add(""{{ scope.name }}"");
}
{{~ end ~}}
{{~ end ~}}
}
{{~ end ~}}
options.Registrations.Add(registration);
}
}
}
{{~ end ~}}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new
{
Name = (string) provider.Attribute("Name"),
Environments = provider.Elements("Environment").Select(environment => new
{
Name = (string?) environment.Attribute("Name") ?? "Production",
Issuer = (string) environment.Attribute("Issuer"),
Configuration = environment.Element("Configuration") switch
{
XElement configuration => new
{
AuthorizationEndpoint = (string?) configuration.Attribute("AuthorizationEndpoint"),
TokenEndpoint = (string?) configuration.Attribute("TokenEndpoint"),
UserinfoEndpoint = (string?) configuration.Attribute("UserinfoEndpoint"),
CodeChallengeMethodsSupported = configuration.Elements("CodeChallengeMethod").ToList() switch
{
{ Count: > 0 } methods => methods.Select(type => (string?) type.Attribute("Value")).ToList(),
_ => (IList<string>) Array.Empty<string>()
},
GrantTypesSupported = configuration.Elements("GrantType").ToList() switch
{
{ Count: > 0 } types => types.Select(type => (string?) type.Attribute("Value")).ToList(),
// If no explicit grant type was set, assume the provider only supports the code flow.
_ => (IList<string>) new[] { GrantTypes.AuthorizationCode }
},
ResponseModesSupported = configuration.Elements("ResponseMode").ToList() switch
{
{ Count: > 0 } modes => modes.Select(type => (string?) type.Attribute("Value")).ToList(),
// If no explicit response mode was set, assume the provider only supports the query response mode.
_ => (IList<string>) new[] { ResponseModes.Query }
},
ResponseTypesSupported = configuration.Elements("ResponseType").ToList() switch
{
{ Count: > 0 } types => types.Select(type => (string?) type.Attribute("Value")).ToList(),
// If no explicit response type was set, assume the provider only supports the code flow.
_ => (IList<string>) new[] { ResponseTypes.Code }
},
ScopesSupported = configuration.Elements("Scope").ToList() switch
{
{ Count: > 0 } types => types.Select(type => (string?) type.Attribute("Value")).ToList(),
_ => (IList<string>) Array.Empty<string>()
},
TokenEndpointAuthMethodsSupported = configuration.Elements("TokenEndpointAuthMethod").ToList() switch
{
{ Count: > 0 } methods => methods.Select(type => (string?) type.Attribute("Value")).ToList(),
// If no explicit response type was set, assume the provider only supports
// flowing the client credentials as part of the token request payload.
_ => (IList<string>) new[] { ClientAuthenticationMethods.ClientSecretPost }
}
},
_ => null
},
Scopes = environment.Elements("Scope").Select(setting => new
{
Name = (string) setting.Attribute("Name"),
Default = (bool?) setting.Attribute("Default") ?? false,
Required = (bool?) setting.Attribute("Required") ?? false
})
})
.ToList(),
Settings = provider.Elements("Setting").Select(setting => new
{
Name = (string) setting.Attribute("Name"),
Type = (string) setting.Attribute("Type"),
Required = (bool?) setting.Attribute("Required") ?? false,
EncryptionAlgorithm = (string?) setting.Attribute("EncryptionAlgorithm"),
SigningAlgorithm = (string?) setting.Attribute("SigningAlgorithm")
})
.ToList()
})
.ToList()
});
}
static string GenerateHelpers(XDocument document)
{
var template = Template.Parse(@"#nullable enable
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Client;
using OpenIddict.Client.WebIntegration;
using SmartFormat;
using SmartFormat.Core.Settings;
using Properties = OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants.Properties;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
public partial class OpenIddictClientWebIntegrationHelpers
{
{{~ for provider in providers ~}}
/// <summary>
/// Resolves the {{ provider.name }} provider settings from the specified registration.
/// </summary>
/// <param name=""registration"">The client registration.</param>
/// <returns>The {{ provider.name }} provider settings.</returns>
/// <exception cref=""InvalidOperationException"">The provider settings cannot be resolved.</exception>
public static OpenIddictClientWebIntegrationSettings.{{ provider.name }} Get{{ provider.name }}Settings(this OpenIddictClientRegistration registration)
=> registration.GetProviderSettings<OpenIddictClientWebIntegrationSettings.{{ provider.name }}>() ??
throw new InvalidOperationException(SR.FormatID0333(Providers.{{ provider.name }}));
{{~ end ~}}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new { Name = (string) provider.Attribute("Name") })
.ToList()
});
}
static string GenerateScopes(XDocument document)
{
var template = Template.Parse(@"#nullable enable
namespace OpenIddict.Client.WebIntegration;
public static partial class OpenIddictClientWebIntegrationScopes
{
{{~ for provider in providers ~}}
/// <summary>
/// Exposes the scopes supported by the {{ provider.name }} provider.
/// </summary>
public static class {{ provider.name }}
{
{{~ for scope in provider.scopes ~}}
{{~ if scope.description ~}}
/// <summary>
/// {{ scope.description }}
/// </summary>
{{~ end ~}}
public const string {{ scope.clr_name }} = ""{{ scope.name }}"";
{{~ end ~}}
}
{{~ end ~}}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new
{
Name = (string) provider.Attribute("Name"),
Scopes = provider.Elements("Environment")
.SelectMany(environment => environment.Elements("Scope"))
.Select(scope => new
{
Name = (string) scope.Attribute("Name"),
ClrName = Regex.Replace((string) scope.Attribute("Name"), "(?:^|_| +)(.)",
match => match.Groups[1].Value.ToUpper(CultureInfo.InvariantCulture)),
Description = (string?) scope.Attribute("Description")
})
.Distinct(scope => scope.ClrName)
.ToList()
})
.ToList()
});
}
static string GenerateSettings(XDocument document)
{
var template = Template.Parse(@"#nullable enable
using Microsoft.IdentityModel.Tokens;
namespace OpenIddict.Client.WebIntegration;
public partial class OpenIddictClientWebIntegrationSettings
{
{{~ for provider in providers ~}}
/// <summary>
/// Provides various settings needed to configure the {{ provider.name }} integration.
/// </summary>
public class {{ provider.name }} : OpenIddictClientWebIntegrationSettings
{
{{~ for setting in provider.settings ~}}
{{~ if setting.description ~}}
/// <summary>
/// {{ setting.description }}
/// </summary>
{{~ end ~}}
public {{ setting.type }}? {{ setting.name }} { get; set; }
{{~ end ~}}
/// <summary>
/// Gets or sets the environment that determines the endpoints to use.
/// </summary>
public OpenIddictClientWebIntegrationEnvironments.{{ provider.name }} Environment { get; set; }
}
{{~ end ~}}
}
");
return template.Render(new
{
Providers = document.Root.Elements("Provider")
.Select(provider => new
{
Name = (string) provider.Attribute("Name"),
Settings = provider.Elements("Setting").Select(setting => new
{
Type = (string) setting.Attribute("Type"),
Name = (string) setting.Attribute("Name"),
Description = (string) setting.Attribute("Description")
})
.ToList()
})
.ToList()
});
}
}
public void Initialize(GeneratorInitializationContext context)
{
}
}
}

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

@ -16,15 +16,29 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
public class AuthenticationController : Controller
{
[HttpGet, Route("~/login")]
public ActionResult LogIn(string returnUrl)
public ActionResult LogIn(string provider, string returnUrl)
{
var context = HttpContext.GetOwinContext();
var issuer = provider switch
{
"local" => "https://localhost:44349/",
"github" => "https://github.com/",
"google" => "https://accounts.google.com/",
_ => null
};
if (string.IsNullOrEmpty(issuer))
{
return new HttpStatusCodeResult(400);
}
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] = "https://localhost:44349/"
[OpenIddictClientOwinConstants.Properties.Issuer] = issuer
})
{
// Only allow local return URLs to prevent open redirect attacks.
@ -36,7 +50,10 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
return new EmptyResult();
}
[AcceptVerbs("GET", "POST"), Route("~/signin-oidc")]
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[AcceptVerbs("GET", "POST"), Route("~/signin-{provider}")]
public async Task<ActionResult> Callback()
{
var context = HttpContext.GetOwinContext();
@ -93,7 +110,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
=> new Claim(ClaimTypes.Name, claim.Value, claim.ValueType, claim.Issuer),
// Applications can map non-standard claims issued by specific issuers to a standard equivalent.
{ Type: "non_standard_user_id", Issuer: "https://example.com/" }
{ Type: "id", Issuer: "https://github.com/" }
=> new Claim(Claims.Subject, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
@ -104,7 +121,7 @@ namespace OpenIddict.Sandbox.AspNet.Client.Controllers
{ Type: ClaimTypes.NameIdentifier or ClaimTypes.Name } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "custom_claim", Issuer: "https://example.com/" } => true,
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false

1
sandbox/OpenIddict.Sandbox.AspNet.Client/OpenIddict.Sandbox.AspNet.Client.csproj

@ -17,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.Owin\OpenIddict.Client.Owin.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Quartz\OpenIddict.Quartz.csproj" />
</ItemGroup>

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

@ -67,7 +67,15 @@ namespace OpenIddict.Sandbox.AspNet.Client
.AddClient(options =>
{
// Enable the redirection endpoint needed to handle the callback stage.
options.SetRedirectionEndpointUris("/signin-oidc");
//
// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
// 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.SetRedirectionEndpointUris(
"/signin-local",
"/signin-github",
"/signin-google");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
@ -88,9 +96,25 @@ namespace OpenIddict.Sandbox.AspNet.Client
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
RedirectUri = new Uri("https://localhost:44378/signin-oidc", UriKind.Absolute),
RedirectUri = new Uri("https://localhost:44378/signin-local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
});
// Register the Web providers integrations.
options.UseWebProviders()
.AddGitHub(new()
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44378/signin-github", UriKind.Absolute)
})
.AddGoogle(new()
{
ClientId = "1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com",
ClientSecret = "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",
RedirectUri = new Uri("https://localhost:44378/signin-google", UriKind.Absolute),
Scopes = { Scopes.Profile }
});
});
// Create a new Autofac container and import the OpenIddict services.

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

@ -1,4 +1,5 @@
@using System.Security.Claims
@using OpenIddict.Abstractions
@model string
<div class="jumbotron">
@ -18,11 +19,15 @@
<h3>Message received from the resource controller: @Model</h3>
}
<form action="~/" method="post">
@Html.AntiForgeryToken()
if (User is ClaimsPrincipal principal &&
principal.FindFirst(OpenIddictConstants.Claims.Subject)?.Issuer is "https://localhost:44395/")
{
<form action="~/" method="post">
@Html.AntiForgeryToken()
<button class="btn btn-lg btn-warning" type="submit">Query the resource controller</button>
</form>
<button class="btn btn-lg btn-warning" type="submit">Query the resource controller</button>
</form>
}
<a class="btn btn-lg btn-danger" href="/logout">Sign out</a>
}
@ -30,6 +35,11 @@
else
{
<h1>Welcome, anonymous</h1>
<a class="btn btn-lg btn-success" href="/login">Sign in</a>
@Html.ActionLink("Sign in using the local OIDC server", "Login", "Authentication",
new { provider = "local" }, new { @class = "btn btn-lg btn-success" })
@Html.ActionLink("Sign in using GitHub", "Login", "Authentication",
new { provider = "github" }, new { @class = "btn btn-lg btn-success" })
@Html.ActionLink("Sign in using Google", "Login", "Authentication",
new { provider = "google" }, new { @class = "btn btn-lg btn-success" })
}
</div>

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

@ -70,7 +70,7 @@ namespace OpenIddict.Sandbox.AspNet.Server
DisplayName = "MVC client application",
RedirectUris =
{
new Uri("https://localhost:44378/signin-oidc")
new Uri("https://localhost:44378/signin-local")
},
Permissions =
{

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

@ -10,13 +10,28 @@ namespace OpenIddict.Sandbox.AspNetCore.Client.Controllers;
public class AuthenticationController : Controller
{
[HttpGet("~/login")]
public ActionResult LogIn(string returnUrl)
public ActionResult LogIn(string provider, string returnUrl)
{
var issuer = provider switch
{
"local" => "https://localhost:44395/",
"github" => "https://github.com/",
"google" => "https://accounts.google.com/",
"reddit" => "https://www.reddit.com/",
_ => null
};
if (string.IsNullOrEmpty(issuer))
{
return BadRequest();
}
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] = "https://localhost:44395/"
[OpenIddictClientAspNetCoreConstants.Properties.Issuer] = issuer
})
{
// Only allow local return URLs to prevent open redirect attacks.
@ -27,7 +42,10 @@ public class AuthenticationController : Controller
return Challenge(properties, OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
}
[HttpGet("~/signin-oidc"), HttpPost("~/signin-oidc")]
// Note: this controller uses the same callback action for all providers
// but for users who prefer using a different action per provider,
// the following action can be split into separate actions.
[HttpGet("~/signin-{provider}"), HttpPost("~/signin-{provider}")]
public async Task<ActionResult> Callback()
{
// Retrieve the authorization data validated by OpenIddict as part of the callback handling.
@ -73,7 +91,7 @@ public class AuthenticationController : Controller
.Select(claim => claim switch
{
// Applications can map non-standard claims issued by specific issuers to a standard equivalent.
{ Type: "non_standard_user_id", Issuer: "https://example.com/" }
{ Type: "id", Issuer: "https://github.com/" }
=> new Claim(Claims.Subject, claim.Value, claim.ValueType, claim.Issuer),
_ => claim
@ -84,7 +102,7 @@ public class AuthenticationController : Controller
{ Type: Claims.Name or Claims.Subject } => true,
// Applications that use multiple client registrations can filter claims based on the issuer.
{ Type: "custom_claim", Issuer: "https://example.com/" } => true,
{ Type: "bio", Issuer: "https://github.com/" } => true,
// Don't preserve the other claims.
_ => false

1
sandbox/OpenIddict.Sandbox.AspNetCore.Client/OpenIddict.Sandbox.AspNetCore.Client.csproj

@ -10,6 +10,7 @@
<ProjectReference Include="..\..\src\OpenIddict.Client.AspNetCore\OpenIddict.Client.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.DataProtection\OpenIddict.Client.DataProtection.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Quartz\OpenIddict.Quartz.csproj" />
</ItemGroup>

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

@ -75,7 +75,16 @@ public class Startup
.AddClient(options =>
{
// Enable the redirection endpoint needed to handle the callback stage.
options.SetRedirectionEndpointUris("/signin-oidc");
//
// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
// 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.SetRedirectionEndpointUris(
"/signin-local",
"/signin-github",
"/signin-google",
"/signin-reddit");
// Register the signing and encryption credentials used to protect
// sensitive data like the state tokens produced by OpenIddict.
@ -97,9 +106,33 @@ public class Startup
ClientId = "mvc",
ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654",
RedirectUri = new Uri("https://localhost:44381/signin-oidc", UriKind.Absolute),
RedirectUri = new Uri("https://localhost:44381/signin-local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
});
// Register the Web providers integrations.
options.UseWebProviders()
.AddGitHub(new()
{
ClientId = "c4ade52327b01ddacff3",
ClientSecret = "da6bed851b75e317bf6b2cb67013679d9467c122",
RedirectUri = new Uri("https://localhost:44381/signin-github", UriKind.Absolute)
})
.AddGoogle(new()
{
ClientId = "1016114395689-kgtgq2p6dj27d7v6e2kjkoj54dgrrckh.apps.googleusercontent.com",
ClientSecret = "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",
RedirectUri = new Uri("https://localhost:44381/signin-google", UriKind.Absolute),
Scopes = { Scopes.Profile }
})
.AddReddit(new()
{
ClientId = "vDLNqhrkwrvqHgnoBWF3og",
ClientSecret = "Tpab28Dz0upyZLqn7AN3GFD1O-zaAw",
RedirectUri = new Uri("https://localhost:44381/signin-reddit", UriKind.Absolute),
ProductName = "DemoApp",
ProductVersion = "1.0.0"
});
});
services.AddHttpClient();

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

@ -1,4 +1,5 @@
@model string
@using static OpenIddict.Abstractions.OpenIddictConstants
@model string
<div class="jumbotron">
@if (User?.Identity is { IsAuthenticated: true })
@ -17,9 +18,12 @@
<h3>Message received from the resource controller: @Model</h3>
}
<form action="~/" method="post">
<button class="btn btn-lg btn-warning" type="submit">Query the resource controller</button>
</form>
if (User.FindFirst(Claims.Subject)?.Issuer is "https://localhost:44349/")
{
<form action="/" method="post">
<button class="btn btn-lg btn-warning" type="submit">Query the resource controller</button>
</form>
}
<a class="btn btn-lg btn-danger" href="/logout">Sign out</a>
}
@ -27,6 +31,13 @@
else
{
<h1>Welcome, anonymous</h1>
<a class="btn btn-lg btn-success" href="/login">Sign in</a>
<a class="btn btn-lg btn-success" asp-controller="Authentication"
asp-action="Login" asp-route-provider="local">Sign in using the local OIDC server</a>
<a class="btn btn-lg btn-success" asp-controller="Authentication"
asp-action="Login" asp-route-provider="github">Sign in using GitHub</a>
<a class="btn btn-lg btn-success" asp-controller="Authentication"
asp-action="Login" asp-route-provider="google">Sign in using Google</a>
<a class="btn btn-lg btn-success" asp-controller="Authentication"
asp-action="Login" asp-route-provider="reddit">Sign in using Reddit</a>
}
</div>

6
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -38,13 +38,9 @@ public class Worker : IHostedService
{
[CultureInfo.GetCultureInfo("fr-FR")] = "Application cliente MVC"
},
PostLogoutRedirectUris =
{
new Uri("https://localhost:44381/signout-callback-oidc")
},
RedirectUris =
{
new Uri("https://localhost:44381/signin-oidc")
new Uri("https://localhost:44381/signin-local")
},
Permissions =
{

14
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -800,7 +800,7 @@ To register the validation services, use 'services.AddOpenIddict().AddValidation
<value>The item index cannot be negative.</value>
</data>
<data name="ID0194" xml:space="preserve">
<value>The type of the parameter value is not supported.</value>
<value>The specified '{0}' setting is not valid.</value>
</data>
<data name="ID0195" xml:space="preserve">
<value>The identifier cannot be null or empty.</value>
@ -1286,6 +1286,18 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad
<data name="ID0329" xml:space="preserve">
<value>An unsupported response was returned by the remote authorization server.</value>
</data>
<data name="ID0330" xml:space="preserve">
<value>The provider name cannot be null or empty.</value>
</data>
<data name="ID0331" xml:space="preserve">
<value>The type of the settings instance attached to the '{0}' provider doesn't match the expected type.</value>
</data>
<data name="ID0332" xml:space="preserve">
<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>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

40
src/OpenIddict.Client.WebIntegration/OpenIddict.Client.WebIntegration.csproj

@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp3.1;net5.0;net6.0;netstandard2.0;netstandard2.1</TargetFrameworks>
</PropertyGroup>
<PropertyGroup>
<Description>OpenIddict client integration package for various Web services.</Description>
<PackageTags>$(PackageTags);providers;social;web</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Client\OpenIddict.Client.csproj" />
<ProjectReference Include="..\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\gen\OpenIddict.Client.WebIntegration.Generators\OpenIddict.Client.WebIntegration.Generators.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SmartFormat" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="**\*.xml" />
</ItemGroup>
<ItemGroup>
<Using Include="OpenIddict.Abstractions" />
<Using Include="OpenIddict.Abstractions.OpenIddictConstants" Static="true" />
<Using Include="OpenIddict.Abstractions.OpenIddictResources" Alias="SR" />
<Using Include="OpenIddict.Client.OpenIddictClientEvents" Static="true" />
<Using Include="OpenIddict.Client.OpenIddictClientHandlers" Static="true" />
<Using Include="OpenIddict.Client.OpenIddictClientHandlerFilters" Static="true" />
<Using Include="OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationHandlers" Static="true" />
</ItemGroup>
</Project>

61
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationBuilder.cs

@ -0,0 +1,61 @@
/*
* 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 OpenIddict.Client.WebIntegration;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Exposes the necessary methods required to configure the OpenIddict client services.
/// </summary>
public partial class OpenIddictClientWebIntegrationBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictClientWebIntegrationBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictClientWebIntegrationBuilder(IServiceCollection services)
=> Services = services ?? throw new ArgumentNullException(nameof(services));
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict client Web integration configuration.
/// </summary>
/// <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"/>.</returns>
public OpenIddictClientWebIntegrationBuilder Configure(Action<OpenIddictClientWebIntegrationOptions> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
// Note: provider registration methods are automatically generated by the source generator.
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => base.GetHashCode();
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public override string? ToString() => base.ToString();
}

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

@ -0,0 +1,31 @@
/*
* 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 Microsoft.Extensions.Options;
namespace OpenIddict.Client.WebIntegration;
/// <summary>
/// Contains the methods required to ensure that the OpenIddict client Web integration configuration is valid.
/// </summary>
public partial class OpenIddictClientWebIntegrationConfiguration : IConfigureOptions<OpenIddictClientOptions>
{
/// <summary>
/// Populates the default OpenIddict client Web integration options
/// and ensures that the configuration is in a consistent and valid state.
/// </summary>
/// <param name="options">The options instance to initialize.</param>
public void Configure(OpenIddictClientOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Register the built-in event handlers used by the OpenIddict client Web components.
options.Handlers.AddRange(OpenIddictClientWebIntegrationHandlers.DefaultHandlers);
}
}

21
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationConstants.cs

@ -0,0 +1,21 @@
/*
* 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>
/// Exposes common constants used by the OpenIddict client Web integration services.
/// </summary>
public static partial class OpenIddictClientWebIntegrationConstants
{
// Note: provider name constants are automatically generated by the source generator.
public static class Properties
{
public const string ProviderName = ".provider_name";
public const string ProviderSettings = ".provider_settings";
}
}

15
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationEnvironments.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>
/// Exposes the provider-specific environments supported by the OpenIddict client Web integration services.
/// </summary>
public static partial class OpenIddictClientWebIntegrationEnvironments
{
// Note: environments are automatically generated by the source generator.
}

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

@ -0,0 +1,70 @@
/*
* 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 Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using OpenIddict.Client;
using OpenIddict.Client.WebIntegration;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Exposes extensions allowing to register the OpenIddict client Web integration services.
/// </summary>
public static class OpenIddictClientWebIntegrationExtensions
{
/// <summary>
/// Registers the OpenIddict client Web integration services in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientWebIntegrationBuilder"/>.</returns>
public static OpenIddictClientWebIntegrationBuilder UseWebProviders(this OpenIddictClientBuilder builder)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
// Register the built-in event handlers used by the OpenIddict client Web components.
// Note: the order used here is not important, as the actual order is set in the options.
builder.Services.TryAdd(OpenIddictClientWebIntegrationHandlers.DefaultHandlers
.Select(descriptor => descriptor.ServiceDescriptor));
// Note: TryAddEnumerable() is used here to ensure the initializers are registered only once.
builder.Services.TryAddEnumerable(new[]
{
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWebIntegrationConfiguration>()
});
return new OpenIddictClientWebIntegrationBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict client Web integration services in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the validation services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
public static OpenIddictClientBuilder UseWebProviders(
this OpenIddictClientBuilder builder, Action<OpenIddictClientWebIntegrationBuilder> configuration)
{
if (builder is null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
configuration(builder.UseWebProviders());
return builder;
}
}

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

@ -0,0 +1,64 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using static OpenIddict.Client.OpenIddictClientHandlers.Discovery;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
public static partial class OpenIddictClientWebIntegrationHandlers
{
public static class Discovery
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Configuration response handling:
*/
AmendClientAuthenticationMethods.Descriptor);
/// <summary>
/// Contains the logic responsible for amending the client
/// authentication methods for the providers that require it.
/// </summary>
public class AmendClientAuthenticationMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<AmendClientAuthenticationMethods>()
.SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Apple implements a non-standard client authentication method for the token endpoint
// that is inspired by the standard private_key_jwt method but doesn't use the standard
// client_assertion/client_assertion_type parameters. Instead, the client assertion
// must be sent as a "dynamic" client secret using client_secret_post. Since the logic
// 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.GetProviderName() is Providers.Apple)
{
context.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.PrivateKeyJwt);
}
return default;
}
}
}
}

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

@ -0,0 +1,21 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
namespace OpenIddict.Client.WebIntegration;
public static partial class OpenIddictClientWebIntegrationHandlers
{
public static class Exchange
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Token request preparation:
*/
UseProductNameAsUserAgent<PrepareTokenRequestContext>.Descriptor);
}
}

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

@ -0,0 +1,21 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
namespace OpenIddict.Client.WebIntegration;
public static partial class OpenIddictClientWebIntegrationHandlers
{
public static class Userinfo
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Userinfo request preparation:
*/
UseProductNameAsUserAgent<PrepareUserinfoRequestContext>.Descriptor);
}
}

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

@ -0,0 +1,249 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.Net.Http.Headers;
using System.Security.Claims;
using static OpenIddict.Client.SystemNetHttp.OpenIddictClientSystemNetHttpHandlerFilters;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
namespace OpenIddict.Client.WebIntegration;
[EditorBrowsable(EditorBrowsableState.Never)]
public static partial class OpenIddictClientWebIntegrationHandlers
{
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authentication processing:
*/
AttachNonStandardClientAssertionTokenClaims.Descriptor,
AttachTokenRequestNonStandardClientCredentials.Descriptor,
/*
* Challenge processing:
*/
AttachNonDefaultResponseMode.Descriptor,
FormatNonStandardScopeParameter.Descriptor)
.AddRange(Discovery.DefaultHandlers)
.AddRange(Exchange.DefaultHandlers)
.AddRange(Userinfo.DefaultHandlers);
/// <summary>
/// Contains the logic responsible for amending the client
/// assertion methods for the providers that require it.
/// </summary>
public class AttachNonStandardClientAssertionTokenClaims : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireClientAssertionTokenGenerated>()
.UseSingletonHandler<AttachNonStandardClientAssertionTokenClaims>()
.SetOrder(PrepareClientAssertionTokenPrincipal.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.ClientAssertionTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006));
// For client assertions to be considered valid by the Apple ID authentication service,
// the team identifier associated with the developer account MUST be used as the issuer
// and the static "https://appleid.apple.com" URL MUST be used as the token audience.
//
// 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.GetProviderName() is Providers.Apple)
{
var settings = context.Registration.GetAppleSettings();
context.ClientAssertionTokenPrincipal.SetClaim(Claims.Private.Issuer, settings.TeamId);
context.ClientAssertionTokenPrincipal.SetAudiences("https://appleid.apple.com");
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching custom client credentials
/// parameters to the token request for the providers that require it.
/// </summary>
public class AttachTokenRequestNonStandardClientCredentials : IOpenIddictClientHandler<ProcessAuthenticationContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireTokenRequest>()
.UseSingletonHandler<AttachTokenRequestNonStandardClientCredentials>()
.SetOrder(AttachTokenRequestClientCredentials.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.TokenRequest is not null, SR.GetResourceString(SR.ID4008));
// Apple implements a non-standard client authentication method for the token endpoint
// that is inspired by the standard private_key_jwt method but doesn't use the standard
// client_assertion/client_assertion_type parameters. Instead, the client assertion
// must be sent as a "dynamic" client secret using client_secret_post. Since the logic
// 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.GetProviderName() is Providers.Apple)
{
context.TokenRequest.ClientSecret = context.TokenRequest.ClientAssertion;
context.TokenRequest.ClientAssertion = null;
context.TokenRequest.ClientAssertionType = null;
}
return default;
}
}
/// <summary>
/// Contains the logic responsible for attaching a specific response mode for providers that require it.
/// </summary>
public class AttachNonDefaultResponseMode : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<AttachNonDefaultResponseMode>()
// Note: this handler MUST be invoked after the scopes have been attached to the
// context to support overriding the response mode based on the requested scopes.
.SetOrder(AttachScopes.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.ResponseMode = context.Registration.GetProviderName() 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")
=> ResponseModes.FormPost,
_ => context.ResponseMode
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the standard "scope"
/// parameter for providers that are known to use a non-standard format.
/// </summary>
public class FormatNonStandardScopeParameter : IOpenIddictClientHandler<ProcessChallengeContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessChallengeContext>()
.UseSingletonHandler<FormatNonStandardScopeParameter>()
.SetOrder(AttachChallengeParameters.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessChallengeContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
context.Request.Scope = context.Registration.GetProviderName() 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.Reddit
when context.GrantType is GrantTypes.AuthorizationCode or GrantTypes.Implicit
=> string.Join(",", context.Scopes),
_ => context.Request.Scope
};
return default;
}
}
/// <summary>
/// Contains the logic responsible for overriding the user agent for providers
/// that are known to require or encourage using custom values (e.g Reddit).
/// </summary>
public class UseProductNameAsUserAgent<TContext> : IOpenIddictClientHandler<TContext>
where TContext : BaseExternalContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireHttpMetadataAddress>()
.UseSingletonHandler<UseProductNameAsUserAgent<TContext>>()
.SetOrder(int.MaxValue - 200_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// This handler only applies to System.Net.Http requests. If the HTTP request cannot be resolved,
// this may indicate that the request was incorrectly processed by another client stack.
var request = context.Transaction.GetHttpRequestMessage() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0173));
// A few providers (like Reddit) are known to aggressively check user agents and encourage
// developers to use unique user agents. While OpenIddict itself always adds a user agent,
// the default value doesn't differ accross applications. To reduce the risks of seeing
// requests blocked by these providers, the user agent is replaced by a custom value
// containing the product name and version set by the user or by the client identifier.
if (context.Registration.GetProviderName() is Providers.Reddit)
{
var settings = context.Registration.GetRedditSettings();
request.Headers.UserAgent.Add(new ProductInfoHeaderValue(
productName: settings.ProductName ?? context.Registration.ClientId!,
productVersion: settings.ProductVersion));
}
return default;
}
}
}

66
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHelpers.cs

@ -0,0 +1,66 @@
/*
* 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 Properties = OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants.Properties;
namespace OpenIddict.Client.WebIntegration;
/// <summary>
/// Exposes companion extensions for the OpenIddict client Web integration.
/// </summary>
public static partial class OpenIddictClientWebIntegrationHelpers
{
/// <summary>
/// Resolves the name of the provider associated with the client registration or
/// <see langword="null" /> if no provider information is attached to the registration.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <returns>The provider name, if applicable.</returns>
/// <exception cref="ArgumentNullException"><paramref name="registration"/> is null.</exception>
public static string? GetProviderName(this OpenIddictClientRegistration registration)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
return registration.Properties.TryGetValue(Properties.ProviderName, out var provider)
&& provider is string value ? value : null;
}
/// <summary>
/// Resolves the provider settings associated with the client registration or
/// <see langword="null" /> if no provider information is attached to the registration.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <returns>The provider settings, if applicable.</returns>
/// <exception cref="ArgumentNullException"><paramref name="registration"/> is null.</exception>
public static OpenIddictClientWebIntegrationSettings? GetProviderSettings(this OpenIddictClientRegistration registration)
{
if (registration is null)
{
throw new ArgumentNullException(nameof(registration));
}
return registration.Properties.TryGetValue(Properties.ProviderSettings, out var value)
&& value is OpenIddictClientWebIntegrationSettings settings ? settings : null;
}
/// <summary>
/// Resolves the provider settings associated with the client registration or
/// <see langword="null" /> if no provider information is attached to the registration or if
/// the actual setting information doesn't match the specified <typeparamref name="TSettings"/>.
/// </summary>
/// <typeparam name="TSettings">The type of the provider settings.</typeparam>
/// <param name="registration">The client registration.</param>
/// <returns>The provider settings, if applicable.</returns>
/// <exception cref="ArgumentNullException"><paramref name="registration"/> is null.</exception>
public static TSettings? GetProviderSettings<TSettings>(this OpenIddictClientRegistration registration)
where TSettings : OpenIddictClientWebIntegrationSettings
=> registration.GetProviderSettings() is TSettings settings ? settings : null;
// Note: provider-specific helpers are automatically generated by the source generator.
}

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

@ -0,0 +1,18 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
namespace OpenIddict.Client.WebIntegration;
/// <summary>
/// Provides various settings needed to configure the OpenIddict client Web integration.
/// </summary>
public class OpenIddictClientWebIntegrationOptions
{
/// <summary>
/// Gets the list of provider integrations enabled for this application.
/// </summary>
public List<OpenIddictClientWebIntegrationProvider> Providers { get; } = new();
}

46
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProvider.cs

@ -0,0 +1,46 @@
/*
* 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.Diagnostics;
namespace OpenIddict.Client.WebIntegration;
/// <summary>
/// Represents an OpenIddict client web integration provider.
/// </summary>
[DebuggerDisplay("{Name,nq}")]
public class OpenIddictClientWebIntegrationProvider
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWebIntegrationProvider"/> class.
/// </summary>
/// <param name="name">The provider name.</param>
/// <param name="settings">The provider settings.</param>
/// <exception cref="ArgumentException"><paramref name="name"/> is null or empty.</exception>
/// <exception cref="ArgumentNullException"><paramref name="settings"/> are null.</exception>
public OpenIddictClientWebIntegrationProvider(
string name,
OpenIddictClientWebIntegrationSettings settings)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(SR.GetResourceString(SR.ID0330), nameof(name));
}
Name = name;
Settings = settings ?? throw new ArgumentNullException(nameof(settings));
}
/// <summary>
/// Gets the provider name associated with the current instance.
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the provider settings associated with the current instance.
/// </summary>
public OpenIddictClientWebIntegrationSettings Settings { get; }
}

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

@ -0,0 +1,51 @@
<Providers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="OpenIddictClientWebIntegrationProviders.xsd">
<Provider Name="Apple" Documentation="https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api">
<Environment Issuer="https://appleid.apple.com/" />
<Setting Name="SigningKey" Type="ECDsaSecurityKey" Required="true" SigningAlgorithm="ES256"
Description="Gets or sets the ECDSA signing key associated with the developer account." />
<Setting Name="TeamId" Type="string" Required="true"
Description="Gets or sets the Team ID associated with the developer account." />
</Provider>
<Provider Name="GitHub" 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"
TokenEndpoint="https://github.com/login/oauth/access_token"
UserinfoEndpoint="https://api.github.com/user" />
</Environment>
</Provider>
<Provider Name="Google" Documentation="https://developers.google.com/identity/protocols/oauth2/openid-connect">
<Environment Issuer="https://accounts.google.com/" />
</Provider>
<Provider Name="Reddit" 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"
UserinfoEndpoint="https://oauth.reddit.com/api/v1/me">
<TokenEndpointAuthMethod Value="client_secret_basic" />
</Configuration>
<!--
Note: Reddit requires sending at least one scope element. If no scope parameter
is set, a misleading "invalid client identifier" error is returned to the caller.
To prevent that, the "identity" scope (that is required by the userinfo endpoint)
is always added even if another scope was explicitly registered by the user.
-->
<Scope Name="identity" Required="true" Description="Access my reddit username and signup date." />
</Environment>
<Setting Name="ProductName" Type="string" Required="false"
Description="Gets or sets the product name used in the user agent header." />
<Setting Name="ProductVersion" Type="string" Required="false"
Description="Gets or sets the product version used in the user agent header." />
</Provider>
</Providers>

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

@ -0,0 +1,349 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="Providers">
<xs:annotation>
<xs:documentation>The list of providers generated by the OpenIddict generator.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="Provider" minOccurs="1" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>A provider definition used by the OpenIddict generator.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="Environment" minOccurs="0" maxOccurs="10">
<xs:annotation>
<xs:documentation>An environment supported by the provider.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="Configuration" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The static configuration used for the environment, if applicable.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="CodeChallengeMethod" minOccurs="0" maxOccurs="10">
<xs:annotation>
<xs:documentation>The code challenge methods supported by the environment.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="Value" use="required">
<xs:annotation>
<xs:documentation>The code challenge method name (e.g S256).</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="plain" />
<xs:enumeration value="S256" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="GrantType" minOccurs="0" maxOccurs="10">
<xs:annotation>
<xs:documentation>The grant types supported by the environment.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="Value" use="required">
<xs:annotation>
<xs:documentation>The grant type name (e.g authorization_code).</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="authorization_code" />
<xs:enumeration value="implicit" />
<xs:enumeration value="refresh_token" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="ResponseMode" minOccurs="0" maxOccurs="10">
<xs:annotation>
<xs:documentation>The response modes supported by the environment.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="Value" use="required">
<xs:annotation>
<xs:documentation>The response mode name (e.g form_post).</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="form_post" />
<xs:enumeration value="fragment" />
<xs:enumeration value="query" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="ResponseType" minOccurs="0" maxOccurs="10">
<xs:annotation>
<xs:documentation>The response types supported by the environment.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="Value" use="required">
<xs:annotation>
<xs:documentation>The response type name (e.g code id_token).</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="code" />
<xs:enumeration value="code id_token" />
<xs:enumeration value="code id_token token" />
<xs:enumeration value="code token" />
<xs:enumeration value="id_token" />
<xs:enumeration value="id_token token" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="Scope" minOccurs="0" maxOccurs="50">
<xs:annotation>
<xs:documentation>The scopes supported by the environment.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:annotation>
<xs:documentation>The scope name (e.g openid).</xs:documentation>
</xs:annotation>
<xs:attribute name="Value" type="xs:string" use="required" />
</xs:complexType>
</xs:element>
<xs:element name="TokenEndpointAuthMethod" minOccurs="0" maxOccurs="10">
<xs:annotation>
<xs:documentation>The token endpoint authentication methods supported by the environment.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="Value" use="required">
<xs:annotation>
<xs:documentation>The token endpoint authentication method name (e.g client_secret_basic).</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="client_secret_basic" />
<xs:enumeration value="client_secret_post" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="AuthorizationEndpoint" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The authorization endpoint offered by the environment.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="TokenEndpoint" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The token endpoint offered by the environment.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="UserinfoEndpoint" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>The userinfo endpoint offered by the environment.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="Scope" minOccurs="0" maxOccurs="50">
<xs:annotation>
<xs:documentation>A scope supported by the environment.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="Name" use="required">
<xs:annotation>
<xs:documentation>The scope value (e.g address or phone).</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string" />
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Default" use="optional">
<xs:annotation>
<xs:documentation>A boolean indicating whether the scope is automatically added if no other scope is added by the user.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:boolean" />
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Required" use="optional">
<xs:annotation>
<xs:documentation>A boolean indicating whether the scope is always added even if another scope is already added by the user.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:boolean" />
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Description" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The scope description.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="Issuer" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The issuer URL corresponding to the environment.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="Name" use="optional">
<xs:annotation>
<xs:documentation>The environment name (by default, Production).</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="^[A-Z][a-zA-Z]*$" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:complexType>
</xs:element>
<xs:element name="Setting" minOccurs="0" maxOccurs="10">
<xs:annotation>
<xs:documentation>A custom setting exposed by the provider integration.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attribute name="Name" use="required">
<xs:annotation>
<xs:documentation>The setting name.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="^[A-Z][a-zA-Z0-9]*$" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Type" use="required">
<xs:annotation>
<xs:documentation>The setting type.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="ECDsaSecurityKey" />
<xs:enumeration value="string" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Required" use="required">
<xs:annotation>
<xs:documentation>A boolean indicating whether the setting is required.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:boolean" />
</xs:simpleType>
</xs:attribute>
<xs:attribute name="EncryptionAlgorithm" use="optional">
<xs:annotation>
<xs:documentation>The encryption algorithm, if applicable.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="A256KW" />
<xs:enumeration value="RSA-OAEP" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="SigningAlgorithm" use="optional">
<xs:annotation>
<xs:documentation>The signing algorithm, if applicable.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:enumeration value="ES256" />
<xs:enumeration value="ES384" />
<xs:enumeration value="ES512" />
<xs:enumeration value="PS256" />
<xs:enumeration value="PS384" />
<xs:enumeration value="PS512" />
<xs:enumeration value="RS256" />
<xs:enumeration value="RS384" />
<xs:enumeration value="RS512" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Description" type="xs:string" use="required">
<xs:annotation>
<xs:documentation>The setting description.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:attribute name="Name" use="required">
<xs:annotation>
<xs:documentation>The provider name.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:string">
<xs:pattern value="^[A-Z][a-zA-Z0-9]*$" />
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="Documentation" type="xs:anyURI" use="optional">
<xs:annotation>
<xs:documentation>The documentation URL, if applicable.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

15
src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationScopes.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>
/// Exposes the provider-specific scopes supported by the OpenIddict client Web integration services.
/// </summary>
public static partial class OpenIddictClientWebIntegrationScopes
{
// Note: scopes are automatically generated by the source generator.
}

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

@ -0,0 +1,33 @@
/*
* 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 abstract partial class OpenIddictClientWebIntegrationSettings
{
/// <summary>
/// Gets or sets the client identifier.
/// </summary>
public virtual string? ClientId { get; set; }
/// <summary>
/// Gets or sets the client secret, if applicable.
/// </summary>
public virtual string? ClientSecret { get; set; }
/// <summary>
/// Gets or sets the redirection URL.
/// </summary>
public virtual Uri? RedirectUri { get; set; }
/// <summary>
/// Gets the scopes requested to the authorization server.
/// </summary>
public virtual HashSet<string> Scopes { get; } = new(StringComparer.Ordinal);
}

14
src/OpenIddict.Client/OpenIddictClientBuilder.cs

@ -973,8 +973,13 @@ public class OpenIddictClientBuilder
/// <summary>
/// Sets the relative or absolute URLs associated to the redirection endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first address will be returned as part of the discovery document.
/// </summary>
/// <remarks>
/// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
/// 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.
/// </remarks>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
public OpenIddictClientBuilder SetRedirectionEndpointUris(params string[] addresses)
@ -990,8 +995,13 @@ public class OpenIddictClientBuilder
/// <summary>
/// Sets the relative or absolute URLs associated to the redirection endpoint.
/// If an empty array is specified, the endpoint will be considered disabled.
/// Note: only the first address will be returned as part of the discovery document.
/// </summary>
/// <remarks>
/// Note: to prevent mix-up attacks, it's recommended to use a unique redirection endpoint
/// 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.
/// </remarks>
/// <param name="addresses">The addresses associated to the endpoint.</param>
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
public OpenIddictClientBuilder SetRedirectionEndpointUris(params Uri[] addresses)

5
src/OpenIddict.Client/OpenIddictClientRegistration.cs

@ -132,4 +132,9 @@ public class OpenIddictClientRegistration
/// Gets the list of scopes sent by default as part of authorization requests.
/// </summary>
public HashSet<string> Scopes { get; } = new(StringComparer.Ordinal);
/// <summary>
/// Gets the bag used to store additional provider-specific properties.
/// </summary>
public Dictionary<string, object?> Properties { get; } = new(StringComparer.OrdinalIgnoreCase);
}

1
src/OpenIddict/OpenIddict.csproj

@ -17,6 +17,7 @@ To use these features on ASP.NET Core or OWIN/Katana/ASP.NET 4.x, reference the
<ProjectReference Include="..\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj" />
<ProjectReference Include="..\OpenIddict.Client\OpenIddict.Client.csproj" />
<ProjectReference Include="..\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />
<ProjectReference Include="..\OpenIddict.Server\OpenIddict.Server.csproj" />
<ProjectReference Include="..\OpenIddict.Validation\OpenIddict.Validation.csproj" />

Loading…
Cancel
Save