Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refreshing expired token with OAuth 2.0 authentication #31

Closed
jdeboer opened this issue May 5, 2013 · 11 comments
Closed

Refreshing expired token with OAuth 2.0 authentication #31

jdeboer opened this issue May 5, 2013 · 11 comments

Comments

@jdeboer
Copy link

jdeboer commented May 5, 2013

How can the authentication token be refreshed using the refresh_token instead of the user needing to grant access again? Is it possible for httr to automatically refresh using the refresh_token when the access_token ised but has expired?

Problem described as follows:

With the OAuth 2.0 client secret for my app held in an environment variable named "GANALYTICS_CONSUMER_SECRET"

Note: The client_id below has been replaced with a dummy value.

Using Windows 7 64-bit and R version 3.0.0, the below code in the R GUI will successfully request access from the user to Google Analytics, then pass the resulting authentication token back to R via a redirect to the localhost. (Note this does not currently work in RStudio.)

library(httr)
authorize.urlPath <- "auth"
  access.urlPath <- "token"
  base_url <- "https://accounts.google.com/o/oauth2"
  appname <- "GANALYTICS"
  client_id <- "123456789012.apps.googleusercontent.com"
  scope_url <- "https://www.googleapis.com/auth/analytics.readonly"
  endpoint <- oauth_endpoint(
    request = NULL,
    authorize = authorize.urlPath,
    access = access.urlPath,
    base_url = base_url
  )
  app <- oauth_app(
    appname = appname,
    key = client_id
  )
  access_token <- oauth2.0_token(
    endpoint = endpoint,
    app = app,
    scope = scope_url
  )

Passing access_token to sign_oauth2.0 results in the a malformed configuration query if passed to GET, i.e. the list object is flattened to a string starting as follows "access_token=list%28access%5Ftoken%20%3D%20%22 and ending %22%29"

However, passing access_token$access_token to sign_oauth2.0 works.

Therefore the following is executed to create the configure and send the request to Google Analytics (note the Google Analytics profile ID has been replaced with a dummy value):

request.config <- sign_oauth2.0(access_token = access_token$access_token)
query.url <- "https://www.googleapis.com/analytics/v3/data/ga?ids=ga%3A12345678&dimensions=ga%3Adate&metrics=ga%3Avisits&start-date=2013-04-21&end-date=2013-05-05&max-results=50"`
response <- GET(
  url = query.url,
  request.config
)

The above successfully retrieves the requested data from Google Analytics.

However, the token expires after 3600 seconds and needs to be refreshed using the refresh_token item from the access_token list object:

> print(access_token)
$access_token
[1] "########"

$token_type
[1] "Bearer"

$expires_in
[1] 3600

$refresh_token
[1] "########"

Note: the $access_token and $refresh_token have been masked from view.

Once the token has expired, if used as above, it results in Google Analytics returning a 401 HTTP status code with the following authentication error message "Login Required"

How can the authentication token be refreshed using the refresh_token instead of the user needing to grant access again? Is it possible for httr to automatically refresh using the refresh_token when the access_token ised but has expired?

Thank you.

@jdeboer
Copy link
Author

jdeboer commented May 6, 2013

I've attempted to solve this by creating a new function oauth2.0_refresh that refreshes an access token using its refresh token. I've based this off the existing oauth2.0_token function from httr and by following the steps demonstrated by R code available from the following links:

  1. https://github.com/greentheo/ROAuthSamples/blob/master/gaGetCredentials_sample.R
  2. http://www.omegahat.org/RGoogleStorage/
oauth2.0_refresh <- function(endpoint, app, access_token, type = NULL) {
  req <- POST(
    url = endpoint$access,
    multipart = FALSE,
    body = list(
      client_id = app$key,
      client_secret = app$secret,
      grant_type = "refresh_token",
      refresh_token = access_token$refresh_token
    )
  )
  content_out <- content(req, type = type)
  content_out <- c(content_out, access_token['refresh_token'])
}

I can successfully refresh an access token using the above function.

Further suggestions

Using the $expires_in value (which is in number of seconds), it would be possible to determine when the access token is due to expire, and with that information automatically refresh it when used if it has expired.

Also, is there a way to configure httr to not use the callback method in order to support RStudio, by instead requiring users to manually copy and paste the access code from their browser to the RStudio console?

@hadley
Copy link
Member

hadley commented May 6, 2013

That looks reasonable. How about adding an has_expired function that determines if a token has expired? Doing it automatically will be harder unless we switch to a ref class for the token object.

The rstudio port problem will be fixed by using httpuv instead of the builtin server.

@jdeboer
Copy link
Author

jdeboer commented May 8, 2013

Yes, to do that I think the token list object would need an expiration date-time field added. The function would simply compare it to the date and time as of when the has_expired function is called.

@jdeboer jdeboer closed this as completed May 8, 2013
@jdeboer jdeboer reopened this May 8, 2013
@jdeboer
Copy link
Author

jdeboer commented May 11, 2013

Hi Hadley, I forked httr and made some amendments to handle the refreshing of OAuth 2.0 access tokens with httr. Would you mind please taking a look? I would also like to ask for some guidance around the rstudio port problem that you mentioned that will be fixed by using httpuv instead of the built-in server. Is there work already started on this? If not, I would like to try to figure this one out if I can - will probably need some help though :)

@jdeboer
Copy link
Author

jdeboer commented May 13, 2013

I've written the following R code that creates a reference class for automatically refreshing an expired oauth2.0 token created by httr. It also handles saving the token to the file system. Please let me know if you have any feedback on the code as this is my first attempt at creating reference classes. Feedback on exception handling would be particularly useful.

First, the below R code depends on the following forked version of httr which has functions for checking and refreshing expired access tokens - the forked version can be installed as follows:

library(devtools)
install_github(repo = "httr", username = "jdeboer")

The following code then creates a generator function, oauth2.0_generator, for an oauth reference class. The access token of an object created using this generator should be accessed via the getAccessToken method - that method will refresh and save the token if it has expired, before returning it.

library(httr)

setClassUnion(
  name = "characterOrNull",
  members = c("character", "NULL")
)

oauth2.0_generator <- setRefClass(
  Class = "oauth2.0",
  fields = list(
    file = "character",
    endpoint = "list",
    app = "list",
    scope = "character",
    type = "characterOrNull",
    access_token = "list",
    margin = "numeric"
  ),
  methods = list(
    initialize = function(file, endpoint, app, scope, type, margin = 5) {
      .self$file <- file
      .self$endpoint <- endpoint
      .self$app <- app
      .self$scope <- scope
      .self$type <- type
      .self$margin <- margin
      if(file.exists(file)) {
        .self$access_token <- readRDS(file)
        .self$access_token <- .self$getAccessToken()
      } else {
        .self$access_token <- oauth2.0_token(
          endpoint = endpoint,
          app = app,
          scope = scope_url,
          type = type
        )
        saveRDS(access_token, file = file)
      }
      return(.self)
    },
    getAccessToken = function() {
      if (
        oauth2.0_has_expired(
          access_token = access_token,
          margin = margin
        )
      ) {
        .self$access_token <- oauth2.0_refresh(
          endpoint = endpoint,
          app = app,
          access_token = access_token,
          type = type
        )
        saveRDS(access_token, file = file)
      }
      return(access_token)
    }
  )
)

The following function, new_oauth, incorporates the use of httr's oauth2.0 functions to demonstrate the creation of a new oauth2.0 object using the generator function:

new_oauth <- function(
  token_file,
  base_url,
  authorize_url,
  access_url,
  scope_url,
  appname,
  client_id,
  client_secret = NULL,
  type = NULL
) {
  endpoint <- oauth_endpoint(
    request = NULL,
    authorize = authorize_url,
    access = access_url,
    base_url = base_url
  )
  app <- oauth_app(
    appname = appname,
    key = client_id,
    secret = client_secret
  )
  oauth <- oauth2.0_generator$new(
    endpoint = endpoint,
    app = app,
    scope = scope_url,
    file = token_file,
    type = type
  )
  return(oauth)
}

A new oauth object can then be created to make a request to, for example, Google Analytics:

authorize_url <- "auth"
access_url <- "token"
base_url <- "https://accounts.google.com/o/oauth2"
appname <- "GANALYTICS"
client_id <- "123456789012.apps.googleusercontent.com"
scope_url <- "https://www.googleapis.com/auth/analytics.readonly"
token_file <- "~/ganalytics_token.RDS"

oauth <- new_oauth(
  token_file = token_file,
  base_url = base_url,
  authorize_url = authorize_url,
  access_url = access_url,
  scope_url = scope_url,
  appname = appname,
  client_id = client_id
)

The access_token string from the oauth object can be accessed and used to make the request as demonstrated below:

request.config <- sign_oauth2.0(
  access_token = oauth$access_token$access_token
)

query.url <- "https://www.googleapis.com/analytics/v3/data/ga?ids=ga%3A12345678&dimensions=ga%3Adate&metrics=ga%3Avisits&start-date=2013-04-21&end-date=2013-05-05&max-results=50"

response <- GET(
  url = query.url,
  request.config
)

print(response)

@jdeboer
Copy link
Author

jdeboer commented May 13, 2013

To utilise the automatic refreshing of the access token, the last code snippet above should be replaced with:

query.url <- "https://www.googleapis.com/analytics/v3/data/ga?ids=ga%3A12345678&dimensions=ga%3Adate&metrics=ga%3Avisits&start-date=2013-04-21&end-date=2013-05-05&max-results=50"

response <- GET(
  url = query.url,
  config = sign_oauth2.0(
    access_token = oauth$getAccessToken()$access_token
  )
)

print(response)

@hadley
Copy link
Member

hadley commented May 13, 2013

Could you please file a pull request? It's easier to comment there.

@hadley
Copy link
Member

hadley commented May 13, 2013

For your other question, see #32

@craigcitro
Copy link
Contributor

There must be OAuth2 in the air -- I've actually been implementing some similar functionality in the last week. I'm interested to see this pull request, but I had one high-level question:

I like the idea of using a reference class to handle the update of the credential behind the scenes, but I don't know that I'd want just the credential to be the mutable object. It feels like you want some sort of "connection" or "session" object (not an R connection, just a poor coincidence of naming) representing an "authorized HTTP instance", since that's what the user will ultimately want to use.

For instance, what you'll probably want to do in code using this library is to create a new authenticated connection object (either loading the credential from disk or re-doing the auth dance), and then make a series of requests. There's a handful of related data (endpoint info, API info, app info, etc) that needs to be threaded through -- it seems nice to package that all together, and have that top-level object be mutable and contain immutable pieces. Or am I overthinking it?

@jdeboer
Copy link
Author

jdeboer commented May 18, 2013

@craigcitro and @hadley Please check out the following pull request from a new branch within my httr repo regarding the use of a reference class for handling OAuth2.0 sessions. jdeboer#1
Thank you both for for feedback so far, please keep it coming.

@hadley
Copy link
Member

hadley commented Dec 2, 2013

Also in the OAuth branch.

@hadley hadley closed this as completed Dec 2, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants