rznlfg
Last Updated: February 25, 2016
·
6.518K
· analogj
57a90b685ef5eb6b17edf33fa91c4ea8

DotNetOpenAuth.AspNet 401 Unauthorized Error and Persistent Access Token Secret Fix

We know that everyone hates creating new accounts just to try out a new service. So when designing QuietThyme, our Cloud Ebook Manager, we started looking for OAuth and OpenId libraries that we could leverage to allow for social login. We ended up using the DotNetOpenAuth.AspNet library for user authentication, because it supports Microsoft, Twitter, Facebook, LinkedIn, Yahoo, and many others right out of the box. While we had some issues setting it all up, in the end we only needed to do a few small customizations to get most of it working (described in a previous coderwall post). We noticed that, unlike all the others, the LinkedIn client would not authenticate, returning a 401 Unauthorized Error from DotNetOpenAuth. It quickly became apparent that this was due to a signature issue, and after looking at the source we were able to determine that the retrieved AccessToken secret is not being used with the authenticated profile info request.

It actually makes sense, the reason that OAuthClient class doesn't include the retrieved access token secret is that it's normally not needed for authentication purposes, which is the primary purpose of the ASP.NET OAuth library.

We needed to make authenticated requests against the api, after the user has logged in, to retrieve some standard profile information, including email address and full name. We were able to solve this issue by making use of an InMemoryOAuthTokenManager temporarily.

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?scope=r_basicprofile+r_emailaddress",
                        HttpDeliveryMethods.PostRequest),
                UserAuthorizationEndpoint =
                        new MessageReceivingEndpoint("https://www.linkedin.com/uas/oauth/authorize",
                        HttpDeliveryMethods.PostRequest),
                TamperProtectionElements =
                        new ITamperProtectionChannelBindingElement[] { new HmacSha1SigningBindingElement() },
                //ProtocolVersion = ProtocolVersion.V10a
            };

    private string ConsumerKey { get; set; }
    private string ConsumerSecret { get; set; }

    public LinkedInCustomClient(string consumerKey, string consumerSecret)
        : this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager()) { }

    public LinkedInCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
        : base("linkedIn", LinkedInServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
    {
        ConsumerKey = consumerKey;
        ConsumerSecret = consumerSecret;
    }

    //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,email-address)";

        string accessToken = response.AccessToken;

        var profileEndpoint =
            new MessageReceivingEndpoint(profileRequestUrl, HttpDeliveryMethods.GetRequest);

        try
        {
            InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
            imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
            WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);

            HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);

            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;

                    string email = String.Empty;
                    try
                    {
                        email = document.Root.Element("email-address").Value;
                    }
                    catch(Exception)
                    {
                    }

                    var extraData = new Dictionary<string, string>();
                    extraData.Add("accesstoken", accessToken);
                    extraData.Add("name", userName);
                    extraData.AddDataIfNotEmpty(document, "headline");
                    extraData.AddDataIfNotEmpty(document, "summary");
                    extraData.AddDataIfNotEmpty(document, "industry");

                    if(!String.IsNullOrEmpty(email))
                    {
                        extraData.Add("email",email);
                    }

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

Here's the section that has changed from the base LinkedIn client written by Microsoft.

InMemoryOAuthTokenManager imoatm = new InMemoryOAuthTokenManager(ConsumerKey, ConsumerSecret);
imoatm.ExpireRequestTokenAndStoreNewAccessToken(String.Empty, String.Empty, accessToken, (response as ITokenSecretContainingMessage).TokenSecret);
WebConsumer w = new WebConsumer(LinkedInServiceDescription, imoatm);

HttpWebRequest request = w.PrepareAuthorizedRequest(profileEndpoint, accessToken);

Unfortunately, the IOAuthTOkenManger.ReplaceRequestTokenWithAccessToken(..) method does not get executed until after the VerifyAuthentication() method returns, so we instead have to create a new TokenManager and and create a WebConsumer and HttpWebRequest using the AccessToken credentials we just retrieved.

This solves our simple 401 Unauthorized issue.

Now what happens if you would like to persist the AccessToken credentials after the authentication process? This could be useful for a DropBox client for instance, where you would like to sync files to a user's DropBox asyncronously. The issue goes back to the way the AspNet library was written, it was assumed that DotNetOpenAuth would only be used for user authethentication, not as a basis for futher OAuth api calls. Thankfully the fix was fairly simple, all I had to do was modify the base AuthetnicationOnlyCookieOAuthTokenManger so that the ReplaceRequestTokenWithAccessToken(..) method stored the new AccessToken key and secrets.

/// <summary>
/// Stores OAuth tokens in the current request's cookie
/// </summary>
public class PersistentCookieOAuthTokenManagerCustom : AuthenticationOnlyCookieOAuthTokenManager
{
    /// <summary>
    /// Key used for token cookie
    /// </summary>
    private const string TokenCookieKey = "OAuthTokenSecret";

    /// <summary>
    /// Primary request context.
    /// </summary>
    private readonly HttpContextBase primaryContext;

    /// <summary>
    /// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
    /// </summary>
    public PersistentCookieOAuthTokenManagerCustom() : base()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="AuthenticationOnlyCookieOAuthTokenManager"/> class.
    /// </summary>
    /// <param name="context">The current request context.</param>
    public PersistentCookieOAuthTokenManagerCustom(HttpContextBase context) : base(context)
    {
        this.primaryContext = context;
    }

    /// <summary>
    /// Gets the effective HttpContext object to use.
    /// </summary>
    private HttpContextBase Context
    {
        get
        {
            return this.primaryContext ?? new HttpContextWrapper(HttpContext.Current);
        }
    }


    /// <summary>
    /// Replaces the request token with access token.
    /// </summary>
    /// <param name="requestToken">The request token.</param>
    /// <param name="accessToken">The access token.</param>
    /// <param name="accessTokenSecret">The access token secret.</param>
    public new void ReplaceRequestTokenWithAccessToken(string requestToken, string accessToken, string accessTokenSecret)
    {
        //remove old requestToken Cookie
        //var cookie = new HttpCookie(TokenCookieKey)
        //{
        //    Value = string.Empty,
        //    Expires = DateTime.UtcNow.AddDays(-5)
        //};
        //this.Context.Response.Cookies.Set(cookie);

        //Add new AccessToken + secret Cookie
        StoreRequestToken(accessToken, accessTokenSecret);

    }

}

Then to use this PersistentCookieOAuthTokenManager all you need to do is modify your DropboxClient constructor, or any other client where you would like to persist the AccessToken Secret

public DropBoxCustomClient(string consumerKey, string consumerSecret)
    : this(consumerKey, consumerSecret, new PersistentCookieOAuthTokenManager()) { }

public DropBoxCustomClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager)
    : base("dropBox", DropBoxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
{}
Say Thanks
Respond

2 Responses
Add your response

4295

Hi Jason.
I’m a bit lost trying to find the best integration. I would like to extend the DotNetOAuth.aspnet and access more LinkedIn data as well so I ran into your nice implementation and started to imagine how to create mine...
My question is how do you integrate this in your project? Are you using the OAuthWebSecurity wrapper? Where do you hook/register your custom client?
By the way you did propose the email for the Facebook client in the github project (thanks) I believe it could be worth to propose this for the LinkedIn client as well because form a pure integration perspective I really think using a primary email is a better approach than a username… All clients should offer this Out Of the box. It would really help lazy integrators like me :-)
Anyway thanks a lot for your time!

over 1 year ago ·
9537

Hi and thank you for this wonderful pot, I had a problem I thought you maybe can help me to solve it. I am trying to implement DotNetOpenAuth to make request to Jira. on Jira server you have to have the Consumer Key and the Public Key which is generated using following commands:

openssl genrsa -out myjira.pem 1024 //private key

openssl rsa -in myjira.pem -pubout -out myjira.pub //public key

and the oAuth consumer must use Consumer key and the Private Key (not conumer secret). OBS: I have read dokumentation about Jira OAuth with a Java example and this is the only way that Jira accpets.
Now I don't know how to implement OAuth on Jira using asp.net. I have to create a custom OAuthClient but I don't know how, considering the Private and public key using RSA. Thank you a lot for helping me.

over 1 year ago ·