Skip to content

Commit

Permalink
Update the LinkedIn provider to use "Sign In with LinkedIn V2"
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinchalet committed May 20, 2023
1 parent 0cf3b87 commit 958f3e6
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public void Execute(GeneratorExecutionContext context)
static string GenerateBuilderMethods(XDocument document)
{
var template = Template.Parse(@"#nullable enable
#pragma warning disable 612, 618
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
Expand Down Expand Up @@ -284,6 +285,9 @@ public sealed partial class {{ provider.name }}
/// </summary>
/// <param name=""{{ setting.parameter_name }}"">{{ setting.description | string.capitalize }}.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Add{{ setting.property_name }}(params {{ setting.clr_type }}[] {{ setting.parameter_name }})
{
if ({{ setting.parameter_name }} is null)
Expand All @@ -299,6 +303,9 @@ public sealed partial class {{ provider.name }}
/// </summary>
/// <param name=""{{ setting.parameter_name }}"">{{ setting.description | string.capitalize }}.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(ECDsaSecurityKey {{ setting.parameter_name }})
{
if ({{ setting.parameter_name }} is null)
Expand All @@ -322,6 +329,9 @@ public sealed partial class {{ provider.name }}
/// The PEM-encoded Elliptic Curve Digital Signature Algorithm (ECDSA) signing key.
/// </param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(string key)
=> Set{{ setting.property_name }}(key.AsMemory());
Expand All @@ -332,6 +342,9 @@ public sealed partial class {{ provider.name }}
/// The PEM-encoded Elliptic Curve Digital Signature Algorithm (ECDSA) signing key.
/// </param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(ReadOnlyMemory<char> key)
=> Set{{ setting.property_name }}(key.Span);
Expand All @@ -342,6 +355,9 @@ public sealed partial class {{ provider.name }}
/// The PEM-encoded Elliptic Curve Digital Signature Algorithm (ECDSA) signing key.
/// </param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(ReadOnlySpan<char> key)
{
if (key.IsEmpty)
Expand Down Expand Up @@ -372,6 +388,9 @@ public sealed partial class {{ provider.name }}
/// </summary>
/// <param name=""{{ setting.parameter_name }}"">{{ setting.description | string.capitalize }}.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(Uri {{ setting.parameter_name }})
{
if ({{ setting.parameter_name }} is null)
Expand All @@ -392,6 +411,9 @@ public sealed partial class {{ provider.name }}
/// </summary>
/// <param name=""{{ setting.parameter_name }}"">{{ setting.description | string.capitalize }}.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(string {{ setting.parameter_name }})
{
if (string.IsNullOrEmpty({{ setting.parameter_name }}))
Expand All @@ -407,6 +429,9 @@ public sealed partial class {{ provider.name }}
/// </summary>
/// <param name=""{{ setting.parameter_name }}"">{{ setting.description | string.capitalize }}.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(X509Certificate2 {{ setting.parameter_name }})
{
if ({{ setting.parameter_name }} is null)
Expand All @@ -429,6 +454,9 @@ public sealed partial class {{ provider.name }}
/// <param name=""resource"">The name of the embedded resource.</param>
/// <param name=""password"">The password used to open the certificate.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(Assembly assembly, string resource, string? password)
#if SUPPORTS_EPHEMERAL_KEY_SETS
// Note: ephemeral key sets are currently not supported on macOS.
Expand All @@ -447,6 +475,9 @@ public sealed partial class {{ provider.name }}
/// <param name=""password"">The password used to open the certificate.</param>
/// <param name=""flags"">An enumeration of flags indicating how and where to store the private key of the certificate.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(
Assembly assembly, string resource,
string? password, X509KeyStorageFlags flags)
Expand All @@ -473,6 +504,9 @@ public sealed partial class {{ provider.name }}
/// <param name=""stream"">The stream containing the certificate.</param>
/// <param name=""password"">The password used to open the certificate.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(Stream stream, string? password)
#if SUPPORTS_EPHEMERAL_KEY_SETS
// Note: ephemeral key sets are currently not supported on macOS.
Expand All @@ -493,6 +527,9 @@ public sealed partial class {{ provider.name }}
/// to store the private key of the certificate.
/// </param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(Stream stream, string? password, X509KeyStorageFlags flags)
{
if (stream is null)
Expand All @@ -511,6 +548,9 @@ public sealed partial class {{ provider.name }}
/// </summary>
/// <param name=""thumbprint"">The thumbprint of the certificate used to identify it in the X.509 store.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(string thumbprint)
{
if (string.IsNullOrEmpty(thumbprint))
Expand Down Expand Up @@ -541,6 +581,9 @@ public sealed partial class {{ provider.name }}
/// <param name=""name"">The name of the X.509 store.</param>
/// <param name=""location"">The location of the X.509 store.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}(string thumbprint, StoreName name, StoreLocation location)
{
if (string.IsNullOrEmpty(thumbprint))
Expand All @@ -562,6 +605,9 @@ public sealed partial class {{ provider.name }}
/// </summary>
/// <param name=""{{ setting.parameter_name }}"">{{ setting.description | string.capitalize }}.</param>
/// <returns>The <see cref=""OpenIddictClientWebIntegrationBuilder.{{ provider.name }}""/> instance.</returns>
{{~ if setting.obsolete ~}}
[Obsolete(""This option is no longer supported and will be removed in a future version."")]
{{~ end ~}}
public {{ provider.name }} Set{{ setting.property_name }}({{ setting.clr_type }} {{ setting.parameter_name }})
{
if ({{ setting.parameter_name }} is null)
Expand Down Expand Up @@ -611,6 +657,8 @@ public sealed partial class {{ provider.name }}
ParameterName = (string) setting.Attribute("ParameterName"),
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
Expand Down Expand Up @@ -687,6 +735,7 @@ public static class Providers
static string GenerateConfigurationClasses(XDocument document)
{
var template = Template.Parse(@"#nullable enable
#pragma warning disable 612, 618
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -1155,6 +1204,9 @@ public sealed class {{ provider.name }}
/// <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 ~}}
Expand All @@ -1179,6 +1231,8 @@ public sealed class {{ provider.name }}
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public ValueTask HandleAsync(HandleConfigurationResponseContext context)
// 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.QuickBooksOnline)
if (context.Registration.ProviderName is Providers.Apple or Providers.LinkedIn or Providers.QuickBooksOnline)
{
context.Configuration.GrantTypesSupported.Add(GrantTypes.AuthorizationCode);
context.Configuration.GrantTypesSupported.Add(GrantTypes.RefreshToken);
Expand Down Expand Up @@ -278,6 +278,16 @@ public ValueTask HandleAsync(HandleConfigurationResponseContext context)
ClientAuthenticationMethods.PrivateKeyJwt);
}

// LinkedIn doesn't support sending the client credentials using basic authentication but
// 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)
{
context.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.ClientSecretPost);
}

return default;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public sealed class HandleNonStandardFrontchannelErrorResponse : IOpenIddictClie
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessAuthenticationContext>()
.AddFilter<RequireRedirectionRequest>()
.UseSingletonHandler<HandleNonStandardFrontchannelErrorResponse>()
.SetOrder(HandleFrontchannelErrorResponse.Descriptor.Order - 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
Expand Down Expand Up @@ -394,7 +395,7 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
context.DisableBackchannelIdentityTokenNonceValidation = context.Registration.ProviderName switch
{
// These providers don't include the nonce in their identity tokens:
Providers.Asana or Providers.Dropbox or Providers.QuickBooksOnline => true,
Providers.Asana or Providers.Dropbox or Providers.LinkedIn or Providers.QuickBooksOnline => true,

_ => context.DisableBackchannelIdentityTokenNonceValidation
};
Expand Down Expand Up @@ -582,16 +583,6 @@ public ValueTask HandleAsync(ProcessAuthenticationContext context)
context.UserinfoRequest["fields"] = string.Join(",", options.Fields);
}

// By default, LinkedIn returns all the basic fields except the profile image.
// To retrieve the profile image, a projection parameter must be sent with
// all the parameters that should be returned from the userinfo endpoint.
else if (context.Registration.ProviderName is Providers.LinkedIn)
{
var options = context.Registration.GetLinkedInOptions();

context.UserinfoRequest["projection"] = string.Concat("(", string.Join(",", options.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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,8 +414,7 @@
</Configuration>

<!--
Note: HubSpot requires sending the "profile" scope to
be able to use the dynamic access token info endpoint.
Note: HubSpot requires sending the "oauth" scope to be able to use the dynamic access token info endpoint.
-->

<Scope Name="oauth" Default="true" Required="true" />
Expand Down Expand Up @@ -450,25 +449,16 @@
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->

<Provider Name="LinkedIn" Documentation="https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin">
<Environment Issuer="https://www.linkedin.com/">
<Configuration AuthorizationEndpoint="https://www.linkedin.com/oauth/v2/authorization"
TokenEndpoint="https://www.linkedin.com/oauth/v2/accessToken"
UserinfoEndpoint="https://api.linkedin.com/v2/me">
<GrantType Value="authorization_code" />
<GrantType Value="refresh_token" />
</Configuration>

<Provider Name="LinkedIn" 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 at least one scope element. If no scope is set, an error is
returned to the caller. To prevent that, the "r_liteprofile" scope (that is required by the
userinfo endpoint) is always added even if another scope was explicitly registered by the user.
Note: LinkedIn requires sending the "profile" scope to be able to use the userinfo endpoint.
-->

<Scope Name="r_liteprofile" Default="true" Required="true" />
<Scope Name="profile" Default="true" Required="true" />
</Environment>

<Setting PropertyName="Fields" ParameterName="fields" Collection="true" Type="String"
<Setting PropertyName="Fields" ParameterName="fields" Collection="true" Obsolete="true" Type="String"
Description="The fields that should be retrieved from the userinfo endpoint (by default, all known basic fields are requested)">
<Item Value="firstName" Default="true" Required="false" />
<Item Value="id" Default="true" Required="false" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,16 @@
</xs:simpleType>
</xs:attribute>

<xs:attribute name="Obsolete" use="optional">
<xs:annotation>
<xs:documentation>A boolean indicating whether the setting is obsolete.</xs:documentation>
</xs:annotation>

<xs:simpleType>
<xs:restriction base="xs:boolean" />
</xs:simpleType>
</xs:attribute>

<xs:attribute name="Type" use="required">
<xs:annotation>
<xs:documentation>The setting type.</xs:documentation>
Expand Down

0 comments on commit 958f3e6

Please sign in to comment.