aspnet-contrib / AspNet.Security.OAuth.Providers

OAuth 2.0 social authentication providers for ASP.NET Core
Apache License 2.0
2.35k stars 533 forks source link

Update AspNet.Security.OAuth.LinkedIn to work with Sign In with LinkedIn using OpenID Connect #796

Closed CosminVana closed 11 months ago

CosminVana commented 11 months ago

Starting with 1st of August 2023, new linkedin applications only have the following product: Sign In with LinkedIn using OpenID Connect

This product is not supported by AspNet.Security.OAuth.LinkedIn, as it uses different set of scopes, and the scopes requested by the current version of the package (r_liteprofile, r_emailaddress) are not supported.

In order to make it work, I had to overwrite the scopes and to implement the CreateTicketAsync method to use the new endpoint:

builder.Services.AddOAuth<LinkedInAuthenticationOptions, ApplicationLinkedInAuthenticationHandler>(LinkedInAuthenticationDefaults.AuthenticationScheme, LinkedInAuthenticationDefaults.DisplayName, linkedInOptions =>
    {
        linkedInOptions.ClientId = string.IsNullOrEmpty(linkedInAuthenticationSettings["ClientId"]) ? "Default" : linkedInAuthenticationSettings["ClientId"];
        linkedInOptions.ClientSecret = string.IsNullOrEmpty(linkedInAuthenticationSettings["ClientSecret"]) ? "Default" : linkedInAuthenticationSettings["ClientSecret"];
        linkedInOptions.SignInScheme = IdentityConstants.ExternalScheme;
        linkedInOptions.Scope.Clear();
        linkedInOptions.Scope.AddRange(new[] { "openid", "profile", "email" });
        linkedInOptions.UserInformationEndpoint = "https://api.linkedin.com/v2/userinfo";
        linkedInOptions.EmailAddressEndpoint = "https://api.linkedin.com/v2/userinfo";
        linkedInOptions.TokenEndpoint = "https://www.linkedin.com/oauth/v2/accessToken";
        linkedInOptions.AuthorizationEndpoint = "https://www.linkedin.com/oauth/v2/authorization";
    });

Here is the ApplicationLinkedInAuthenticationHandler class

public class ApplicationLinkedInAuthenticationHandler : LinkedInAuthenticationHandler
    {
        public ApplicationLinkedInAuthenticationHandler(IOptionsMonitor<LinkedInAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock)
        {
        }

        protected override async Task<AuthenticationTicket> CreateTicketAsync(
        [NotNull] ClaimsIdentity identity,
        [NotNull] AuthenticationProperties properties,
        [NotNull] OAuthTokenResponse tokens)
        {
             string requestUri = Options.UserInformationEndpoint;

            using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
            request.Headers.Add("x-li-format", "json");
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);

            using var response = await Backchannel.SendAsync(request, Context.RequestAborted);
            if (!response.IsSuccessStatusCode)
            {
                throw new HttpRequestException("An error occurred while retrieving the user profile.");
            }

            using var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync(Context.RequestAborted));
            var formattedPayload = new
            {
                id = payload.RootElement.GetString("sub"),
                firstName = new
                {
                    localized = new
                    {
                        en_US = payload.RootElement.GetString("given_name")
                    },
                },
                lastName = new
                {
                    localized = new
                    {
                        en_US = payload.RootElement.GetString("family_name")
                    },
                }
            };

            var principal = new ClaimsPrincipal(identity);
            var context = new OAuthCreatingTicketContext(principal, properties, Context, Scheme, Options, Backchannel, tokens, JsonDocument.Parse(JsonSerializer.Serialize(formattedPayload)).RootElement);
            context.RunClaimActions();

            identity.AddClaim(new Claim(ClaimTypes.Email, payload.RootElement.GetString("email"), ClaimValueTypes.String, Options.ClaimsIssuer));

            await Events.CreatingTicket(context);
            return new AuthenticationTicket(context.Principal!, context.Properties, Scheme.Name);
        }
    }
CosminVana commented 11 months ago

image

image

martincostello commented 11 months ago

Hey there - the changes needed to update the LinkedIn provider are being tracked by #781.