Custom OAuth client in MVC4 / DotNetOpenAuth - missing access token secret

后端 未结 4 2077
花落未央
花落未央 2020-12-30 16:31

I\'m currently working on implementing a Dropbox OAuth client for my application. It\'s been a fairly painless process until I hit the end. Once I\'ve authorized, when I att

相关标签:
4条回答
  • 2020-12-30 16:56

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

    That said, if you want to retrieve the access token secret in your case, you can override the VerifyAuthentication() method, instead of VerifyAuthenticationCore() like you are doing above. Inside VerifyAuthentication(), you can call WebWorker.ProcessUserAuthorization() to validation the login and from the returned AuthorizedTokenResponse object, you have access to the token secret.

    0 讨论(0)
  • 2020-12-30 17:00

    I found your question when I was searching for solution to a similar problem. I solved it by making 2 new classes, which you can read about in this coderwall post.

    I'll also copy and paste the full post here:


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

    When designing QuietThyme, our Cloud Ebook Manager, we knew that everyone hates creating new accounts just as much as we do. 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 and Yahoo, and many others right out of the bow. 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 acutally 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))
        {}
    
    0 讨论(0)
  • 2020-12-30 17:00

    After doing some digging, I was able to solve this by changing my constructor logic as follows:

    public DropboxClient(string consumerKey, string consumerSecret) : 
        this(consumerKey, consumerSecret, new AuthenticationOnlyCookieOAuthTokenManager())
    {
    }
    
    public DropboxClient(string consumerKey, string consumerSecret, IOAuthTokenManager tokenManager) : 
        base("dropbox", DropboxServiceDescription, new SimpleConsumerTokenManager(consumerKey, consumerSecret, tokenManager))
    {
    }
    

    becomes

    public DropboxClient(string consumerKey, string consumerSecret) : 
            base("dropbox", DropboxServiceDescription, consumerKey, consumerSecret)
        {
        }
    

    Digging through the DNOA source shows that if you construct an OAuthClient (my base class) with just the consumer key and secret, it uses the InMemoryOAuthTokenManager instead of the SimpleConsumerTokenManager. I don't know why, but now my access token secret is properly appended to my signature in the authorized request and everything works. Hopefully this helps someone else. In the meantime, I'll likely clean this up for a blog post since there is zero guidance on the net (that I can find) for doing this.

    EDIT: I'm going to undo my answer since, as a colleague pointed out, this will take care of one request, but now that I'm using the in-memory manager, that will flush once I round trip fully back to the browser (I'm assuming). So I think the root issue here is that I need to get the access token secret, which I still haven't seen how to do.

    0 讨论(0)
  • 2020-12-30 17:00

    As for your original question that the secret is not provided in response--the secret is right there when you get the response in the verifyAuthenticationCore function. You get both of them like this:

      string token = response.AccessToken; ;
      string secret = (response as ITokenSecretContainingMessage).TokenSecret; 
    
    0 讨论(0)
提交回复
热议问题