titarenko / OAuth2

OAuth2 client implementation for .NET
412 stars 154 forks source link

Exception of type 'OAuth2.Client.UnexpectedResponseException' was thrown. when retrieving User info from Linked In #140

Closed sivagithub-pk closed 3 years ago

sivagithub-pk commented 3 years ago

Hi, I am using OAuth2 to implement the SSO for Google, Facebook, and LinkedIn. Google and Facebook I am able to log in and read the user profile info but for LinkedIn, I am able to get the authorization code but when I call method to get the User info I am getting the exception as Exception of type 'OAuth2.Client.UnexpectedResponseException' was thrown. below is my code snippet ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; var userInfo = await oauth.GetUserInfoAsync(new NameValueCollection { { "code", code } });

Here is the exception of staktrace. at OAuth2.Infrastructure.RestClientExtensions.VerifyResponse(IRestResponse response) at OAuth2.Infrastructure.RestClientExtensions.d1.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult() at OAuth2.Client.OAuth2Client.d52.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.ConfiguredTaskAwaitable1.ConfiguredTaskAwaiter.GetResult() at OAuth2.Client.OAuth2Client.<GetUserInfoAsync>d__53.MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at Apex.Web.Authentication.SingleSignOnService.d__6.MoveNext() in C:\Users\ksivasankar\Source\Repos\Apex\Source\Backend\Shared\Web\Authentication\SingleSignOnService.cs:line 75 Can any one please suggest where I am doing wrong.

niemyjski commented 3 years ago

Could you step into this and see if you can debug this call. I don't use linked in, but form what you posted it sounds like the json response is different than what we are expecting.

sivagithub-pk commented 3 years ago

Hi @niemyjski ,

I have fixed this issue. the reason why it's throwing the exception is,

  1. The Linked in is not supporting API version V1.
  2. The response JSON is different.

I have fixed this issue by customizing the LinkedInclient File that will point to the API version V2. Existing LinkedIn client file. that points to API version V1

using System; using System.Threading; using System.Threading.Tasks; using OAuth2.Configuration; using OAuth2.Infrastructure; using OAuth2.Models; using RestSharp; using System.Xml.Linq; using System.Xml.XPath;

namespace OAuth2.Client.Impl { ///

/// LinkedIn authentication client. /// public class LinkedInClient : OAuth2Client { /// /// Initializes a new instance of the class. /// /// The factory. /// The configuration. public LinkedInClient(IRequestFactory factory, IClientConfiguration configuration) : base(factory, configuration) { }

    /// <summary>
    /// Defines URI of service which issues access code.
    /// </summary>
    protected override Endpoint AccessCodeServiceEndpoint
    {
        get
        {
            return new Endpoint
            {
                BaseUri  = "https://www.linkedin.com",
                Resource = "/uas/oauth2/authorization"
            };
        }
    }

    /// <summary>
    /// Defines URI of service which issues access token.
    /// </summary>
    protected override Endpoint AccessTokenServiceEndpoint
    {
        get
        {
            return new Endpoint
            {
                BaseUri  = "https://www.linkedin.com",
                Resource = "/uas/oauth2/accessToken"
            };
        }
    }

    /// <summary>
    /// Defines URI of service which allows to obtain information about user which is currently logged in.
    /// </summary>
    protected override Endpoint UserInfoServiceEndpoint
    {
        get
        {
            return new Endpoint
            {
                BaseUri  = "https://api.linkedin.com",
                Resource = "/v1/people/~:(id,email-address,first-name,last-name,picture-url)"
            };
        }
    }

    public override Task<string> GetLoginLinkUriAsync(string state = null, CancellationToken cancellationToken = default)
    {
        return base.GetLoginLinkUriAsync(state ?? Guid.NewGuid().ToString("N"), cancellationToken);
    }

    protected override void BeforeGetUserInfo(BeforeAfterRequestArgs args)
    {
        args.Client.Authenticator = null;
        args.Request.Parameters.Add(new Parameter("oauth2_access_token", AccessToken, ParameterType.GetOrPost));
    }

    /// <summary>
    /// Should return parsed <see cref="UserInfo"/> from content received from third-party service.
    /// </summary>
    /// <param name="content">The content which is received from third-party service.</param>
    protected override UserInfo ParseUserInfo(string content)
    {

        var document  = XDocument.Parse(content);
        var avatarUri = SafeGet(document, "/person/picture-url");
        var avatarSizeTemplate = "{0}_{0}";
        if (string.IsNullOrEmpty(avatarUri))
        {
            avatarUri = "https://www.linkedin.com/scds/common/u/images/themes/katy/ghosts/person/ghost_person_80x80_v1.png";
            avatarSizeTemplate = "{0}x{0}";
        }
        var avatarDefaultSize = string.Format(avatarSizeTemplate, 80);

        return new UserInfo
        {
            Id        = document.XPathSelectElement("/person/id").Value,
            Email     = SafeGet(document, "/person/email-address"),
            FirstName = document.XPathSelectElement("/person/first-name").Value,
            LastName  = document.XPathSelectElement("/person/last-name").Value,
            AvatarUri =
                {
                    Small  = avatarUri.Replace(avatarDefaultSize, string.Format(avatarSizeTemplate, AvatarInfo.SmallSize)),
                    Normal = avatarUri,
                    Large  = avatarUri.Replace(avatarDefaultSize, string.Format(avatarSizeTemplate, AvatarInfo.LargeSize))
                }
        };
    }

    private string SafeGet(XDocument document, string path)
    {
        var element = document.XPathSelectElement(path);
        if (element == null)
            return null;

        return element.Value;
    }

    /// <summary>
    /// Friendly name of provider (OAuth service).
    /// </summary>
    public override string Name
    {
        get { return "LinkedIn"; }
    }
}

}

My customized LinkedClient file that points to API Version V2

using System; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; using OAuth2.Client; using OAuth2.Configuration; using OAuth2.Infrastructure; using OAuth2.Models; using RestSharp;

namespace Apex.Web.Authentication { ///

/// LinkedIn authentication client. /// public class LinkedInClientV2 : OAuth2Client { /// /// Initializes a new instance of the class. /// /// The factory. /// The configuration. public LinkedInClientV2(IRequestFactory factory, IClientConfiguration configuration) : base(factory, configuration) { }

    /// <summary>
    /// Gets friendly name of provider (OAuth service).
    /// </summary>
    public override string Name => "LinkedIn";

    /// <summary>
    /// Gets defines URI of service which issues access code.
    /// </summary>
    protected override Endpoint AccessCodeServiceEndpoint
    {
        get
        {
            return new Endpoint
            {
                BaseUri = "https://www.linkedin.com",
                Resource = "/uas/oauth2/authorization"
            };
        }
    }

    /// <summary>
    /// Gets defines URI of service which issues access token.
    /// </summary>
    protected override Endpoint AccessTokenServiceEndpoint
    {
        get
        {
            return new Endpoint
            {
                BaseUri = "https://www.linkedin.com",
                Resource = "/uas/oauth2/accessToken"
            };
        }
    }

    /// <summary>
    /// Gets defines URI of service which allows to obtain information about user which is currently logged in.
    /// </summary>
    protected override Endpoint UserInfoServiceEndpoint
    {
        get
        {
            return new Endpoint
            {
                BaseUri = "https://api.linkedin.com",
                Resource = "/v2/clientAwareMemberHandles?q=members&projection=(elements*(primary,type,handle~))
            };
        }
    }

    public override Task<string> GetLoginLinkUriAsync(string state = null, CancellationToken cancellationToken = default)
    {
        return base.GetLoginLinkUriAsync(state ?? Guid.NewGuid().ToString("N"), cancellationToken);
    }

    protected override void BeforeGetUserInfo(BeforeAfterRequestArgs args)
    {
        args.Client.Authenticator = null;

pragma warning disable CS0618 // Type or member is obsolete

        args.Request.Parameters.Add(new Parameter("oauth2_access_token", AccessToken, ParameterType.GetOrPost));

pragma warning restore CS0618 // Type or member is obsolete

    }

    /// <summary>
    /// Should return parsed <see cref="UserInfo"/> from content received from third-party service.
    /// </summary>
    /// <param name="content">The content which is received from third-party service.</param>
    /// <returns>UserInfo.</returns>
    protected override UserInfo ParseUserInfo(string content)
    {
        var jsonContent = JObject.Parse(content);

        return new UserInfo
        {
            Email = jsonContent["elements"][0]["handle~"]["emailAddress"].ToString()
        };
    }
}

}

Now I am able to query and read the user email details successfully.