ASP.NET: Custom LinkedIn OAuth Provider

The release of Visual Studio 2012 brought us a very nice feature. They added OAuth/OpenID support to the default Web Application templates. Now by just adding the external API keys to a config file you can add external account support to your application.

You can watch a short introduction to the feature here or read about it here.

Very cool stuff. However software is never perfect and some of the included providers by defaulthave minor bugs; for instance, theres a little bug with the Google client regarding the way it retrieves user metadata. A quick fix for it, as recommended by Microsoft in that blog post, is creating a custom client and overriding the method that requests the extra data in order to retrieve what youre supposed to get.

So why are we here?

Well, while playing with the other providers, such as Facebook and Twitter I didnt have problems either logging in or retrieving user metadata. BUT! when it came to LinkedIn I faced an interesting issue: the log in didnt work. I thought it had to do with the keys I was using, but after checking and double-checking I came to the conclusion that it was broken.

I went to the community in search of answers to my issue and found out that others were having the same problems. Digging further into the issue log I found that theres actually an open issue ticket for this problem.

Before this became a feature I had created a LinkedIn provider with the DotNetOpenAuth library which is the core of the OAuth/OpenID in ASP.NET so I was aware of token related issues when it came to LinkedIn. Since I needed LinkedIn support now and couldnt wait for an update I went ahead and created a new custom client in order to fix the issue.

Show me tha code!

Going through the original library source code I noticed that the default constructor delegated the consumer token management to a SimpleConsumerTokenManager class.

public LinkedInClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) : 
    base("linkedIn", LinkedInClient.LinkedInServiceDescription, (IConsumerTokenManager) new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{ }

I deduced that the problem was here: In a previous effort to add LinkedIn support I had gone through this example and had issues with the tokens as well. The problem is that they have to be persisted. Apparently in a second request the object scope is exceeded, therefore the tokens passed are empty. The quick fix to this was adding the token manager to a session variable.

But the problem here was solved in a different and simpler manner. Watching the source code I noticed that there was a different overload that handled the consumerKey and the secretKey differently. Without the use of any external classes.

public LinkedClient(string consumerKey, string consumerSecret) : 
    base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret)
{ }

So I tested this one by setting it as my default constructor and voilá! Everything worked like a charm.

Heres the resulting code:

public class LinkedInCustomClient : OAuthClient
{
    private static XDocument LoadXDocumentFromStream(Stream stream)
    {
        var settings = new XmlReaderSettings
        {
            MaxCharactersInDocument = 65536L
        };

        return XDocument.Load(XmlReader.Create(stream, settings));
    }

    /// Describes the OAuth service provider endpoints for LinkedIn.
    private static readonly ServiceProviderDescription LinkedInServiceDescription = new ServiceProviderDescription
    {
        AccessTokenEndpoint = new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/accessToken", HttpDeliveryMethods.PostRequest),
        RequestTokenEndpoint = new MessageReceivingEndpoint("https://api.linkedin.com/uas/oauth/requestToken", HttpDeliveryMethods.PostRequest),
        UserAuthorizationEndpoint = new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authorize", HttpDeliveryMethods.PostRequest),
        TamperProtectionElements = new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
        ProtocolVersion = ProtocolVersion.V10a
    };

    public LinkedInCustomClient(string consumerKey, string consumerSecret) : 
        base("linkedIn", LinkedInServiceDescription, consumerKey, consumerSecret)
    { }

    /// Check if authentication succeeded after user is redirected back from the service provider.
    /// The response token returned from service provider authentication result. 
    [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We don't care if the request fails.")]
     protected override AuthenticationResult VerifyAuthenticationCore(AuthorizedTokenResponse response)
     {
         // See here for Field Selectors API http://developer.linkedin.com/docs/DOC-1014
        const string profileRequestUrl =  "https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,industry,summary)";

        string accessToken = response.AccessToken;

        var profileEndpoint = new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest);
        HttpWebRequest request =  WebWorker.PrepareAuthorizedRequest(profileEndpoint, accessToken);

        try
        {
            using (WebResponse profileResponse = request.GetResponse())
            {
                using (Stream responseStream = profileResponse.GetResponseStream())
                {
                    XDocument document = LoadXDocumentFromStream(responseStream);
                    string userId = document.Root.Element("id").Value;
                    string firstName = document.Root.Element("first-name").Value;
                    string lastName = document.Root.Element("last-name").Value;
                    string userName = firstName + " " + lastName;

                    var extraData = new Dictionary
                    {
                        { "accesstoken", accessToken }, 
                        { "name", userName }
                    };

                    return new AuthenticationResult( isSuccessful: true,
                        provider: ProviderName, 
                        providerUserId: userId,
                        userName: userName,
                        extraData: extraData );
                }
            }
        }
        catch (Exception exception)
        {
            return new AuthenticationResult(exception);
        }
    }
}

You could create a custom type using XSD to hold the users metadata in a more easy to read and maintain manner. But Ill let you decide on that, this is how the default client has the metadata retrieval implemented and I decided not to change it.

Now you just need to implement it like this:

OAuthWebSecurity.RegisterClient(new LinkedInCustomClient(consumerKey, secretKey), "LinkedIn", null);

I hope this helps while they fix the issues and update the package. Happy coding!

The library clients source code can be found here: https://github.com/AArnott/dotnetopenid/tree/master/src/DotNetOpenAuth.AspNet/Clients

Update (11/12/2013)

The source code repository was moved to: https://github.com/DotNetOpenAuth/DotNetOpenAuth