adamkrogh / goodreads-dotnet

:books: Goodreads .NET API Client Library
MIT License
51 stars 15 forks source link

OAuth Question #5

Closed hfloyd closed 6 years ago

hfloyd commented 7 years ago

Hello, I am a bit new to OAuth, but am struggling to understand how this library can be used to get access to an authenticated user's data.

The example includes these two lines:

// Create an authenticated API client
var authClient = new GoodreadsClient("apiKey", "apiSecret", "oauthToken", "oauthTokenSecret");

// Get a user's OAuth access token and secret after they have granted access
var credentials = client.Connection.Credentials.GetAccessToken(apiKey, apiSecret, requestToken, requestSecret);

I have my API Key & Secret... my confusion is related to the other 4 items : oauthToken, oauthTokenSecret, requestToken, requestSecret. If I do the following:

// Create an unauthenticated API client [because right now I have no tokens, right?]
var client = new GoodreadsClient("apiKey", "apiSecret");

// Get the Goodreads URL to redirect to for authorization
var authorizeUrl = client.Connection.Credentials.GetRequestTokenAndBuildAuthorizeUrl(callbackUrl);

//Incidentally, if I also do this:
var requestToken=client.Connection.Credentials.GetRequestToken().OauthToken;
//requestToken is different from the value in the authorizeUrl... why is that? 

Redirect user to authorizeUrl for "Allow" permission to be granted. Goodreads redirects back to the callBack url including a query string like: "?oauth_token=xxxxxxxxx&authorize=1"

What does "oauth_token" represent? Is this the "requestSecret"?

Example code from callback, which includes the query string data:


 string requestSecret = oauth_token;

            var client = new GoodreadsClient(apiKey, apiSecret);

            var requestToken = client.Connection.Credentials.GetRequestToken().OauthToken;

            //If I then use that as the "requestSecret" with the "requestToken" value (which didn't match the url) like this:
            var credentials = client.Connection.Credentials.GetAccessToken(apiKey, apiSecret, requestToken, requestSecret);

//credentials.OauthToken    is null AND credentials.OauthTokenSecret is null... so... something has gone wrong here.

            // Create an authenticated API client [This won't work because of nulls]...
            var authClient = new GoodreadsClient(apiKey, apiSecret, credentials.OauthToken, credentials.OauthTokenSecret);

I am missing something significant here... Can you point out where I am going wrong? Thanks

VladimirRybalko commented 7 years ago

Hi @hfloyd. Thanks for your question. I will check that and send you more detailed example. Seems that the current documentation would be misunderstand. If Goodreads behaviour is different from the documentation, I will update it too.

hfloyd commented 7 years ago

Thanks for your assistance, @VladimirRybalko and for your work on this library :-)

The Goodreads documentation about OAuth is here: https://www.goodreads.com/api/documentation#oauth

And their sample code (only in ruby & python): https://www.goodreads.com/api/oauth_example

hfloyd commented 7 years ago

I spent some more time with this, trying different things and stepping through the code - including your library code and have some other observations.

Using this code...

         // Create an unauthenticated API client [because right now I have no tokens, right?]
            var client = new GoodreadsClient(apiKey, apiSecret);
            var testData = client.Authors.GetAuthorIdByName("John Milton").Result; //This does return data, so I know the API key is working....

            // Get the Goodreads URL to redirect to for authorization
            var callbackUrl = HttpContext.Current.Request.Url.ToString();
            var authorizeUrl = client.Connection.Credentials.GetRequestTokenAndBuildAuthorizeUrl(callbackUrl);

            //The unauthenticated API client has data in the "credentials" property....
            var requestToken = client.Connection.Credentials.OauthToken; //this matches the string included in 'authorizeUrl'
            var requestSecret = client.Connection.Credentials.OauthTokenSecret; //I wonder where this comes from???

            if (string.IsNullOrEmpty(requestToken))
            {
                HttpContext.Current.Response.Redirect(authorizeUrl);
                return string.Format("Click here to Authenticate: {0}", authorizeUrl);
            }
            else
            {
                // Get the access token from the unauthenticated client?
                var credentials = client.Connection.Credentials.GetAccessToken(apiKey, apiSecret, requestToken, requestSecret);
                //At this point "credentials.OauthToken" and "credentials.OauthTokenSecret" are NULL, so the next part doesn't work...

                // Create an authenticated API client
                var authClient = new GoodreadsClient(apiKey, apiSecret, credentials.OauthToken, credentials.OauthTokenSecret);
                var test = authClient.Connection.IsAuthenticated;
                var userId = authClient.Users.GetAuthenticatedUserId().Result;

                return string.Format("Current User: {0}", userId);
            }

In "GetAccessToken()" I am seeing a response of "Invalid OAuth Request" when passing in those two OAuth values, so that still isn't right...

hfloyd commented 7 years ago

Okay! I've figured it out!

So, the "client.Connection.Credentials.OauthToken" & "client.Connection.Credentials.OauthTokenSecret" ARE the request values, so if I stored them for later retrieval (on the callback url)...

//The unauthenticated API client has data in the "credentials" property.... these are the request values
var requestToken = client.Connection.Credentials.OauthToken; 
var requestSecret = client.Connection.Credentials.OauthTokenSecret;

//store these for later in cookies
var cookieResponse = SaveRequestTokens(requestToken, requestSecret); //returns an HttpResponseMessage with the cookies added

var returnVal = string.Format("<p>Click here to Authenticate: <a href=\"{0}\">{0}</a></p>", authorizeUrl);
            HttpContext.Current.Response.ContentType = "html";
            HttpContext.Current.Response.Write(returnVal);
            return cookieResponse;

Then on the callback...

//retrieve original request tokens from cookies
            var requestToken = GetRequestToken();
            var requestSecret = GetRequestSecret();

            //Pass in original request tokens...
            var credentials = client.Connection.Credentials.GetAccessToken(apiKey, apiSecret, requestToken, requestSecret);

            //store the access token info for later use
            var cookieResponse = SaveAccessTokens(credentials.OauthToken, credentials.OauthTokenSecret);

            // Create an authenticated API client now works
            var authClient = new GoodreadsClient(apiKey, apiSecret, credentials.OauthToken, credentials.OauthTokenSecret);

I wonder if it would make sense to rename the properties: client.Connection.Credentials.OauthToken --> "OauthRequestToken" & client.Connection.Credentials.OauthTokenSecret --> "OauthRequestSecret" to make it clearer?

Also, I'd be happy to add a Wiki page with my entire code sample.

adamkrogh commented 7 years ago

Hey!

Sorry I've been a bit busy lately but I'm glad you got things figured out, good job! I was just working on a solution here and confusing myself in the process as well. The properties could definitely be renamed a bit clearer and I wonder if the whole process could be streamlined.

You definitely hit on the right solution though. Storing the request token and request secret for future requests is how I would do it too.

I just created a goodreads-dotnet-samples repo where we can maybe create a few simple applications illustrating this library. An example piece of code that you can run and debug through is always a huge help. I'll see if I can get a basic structure out today and we can go from there.

hfloyd commented 7 years ago

Hi @adamkrogh. Attached is a slightly stripped-down version of my code (which is a WebApi) feel free to incorporate this as you see fit. Sample Goodreads WebApi Controller.txt

Or, let me know where you'd like this pasted in your skeleton project and I'll be happy to add it for you.

VladimirRybalko commented 7 years ago

Ough. Sorry for delay. Now I'm changing my place of residence. So I'm a bit busy. @hfloyd @adamkrogh, it's a really cool job! I think we may include samples into this repository or add link to read.me at least.

VladimirRybalko commented 6 years ago

@hfloyd thank you for your MVC example. I think I will include it into our new Demo application. It will be soon..

VladimirRybalko commented 6 years ago

@hfloyd Thank you for your question. It inspired me to redesign our auth infrastructure and add many examples. Please check links below: Demo application Wiki page

hfloyd commented 6 years ago

@VladimirRybalko Looks great :-)

If I ever pick up my tentative project again, I'll be sure to test it out. (I've been finding the GoodReads API to be irritatingly limited in many ways...)