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

clear token on oauth2.0 error #315

Merged
merged 8 commits into from
Jan 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# httr 1.0.0.9000

* `refresh_oauth2.0()` now checks for known OAuth2.0 errors and clears the
locally cached token in the presense of any (@nathangoulding, #315)

* Tweak regexp in `parse_url()` so urls like `file:///a/b/c` work (#309).

* `oauth2.0_token()` accepts the optional named list parameter `user_params`
Expand Down
8 changes: 8 additions & 0 deletions R/oauth-cache.R
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ fetch_cached_token <- function(hash, cache_path) {
load_cache(cache_path)[[hash]]
}

remove_cached_token <- function(token) {
if (is.null(token$cache_path)) return()

tokens <- load_cache(token$cache_path)
tokens[[token$hash()]] <- NULL
saveRDS(tokens, token$cache_path)
}

load_cache <- function(cache_path) {
if (!file.exists(cache_path)) {
list()
Expand Down
25 changes: 25 additions & 0 deletions R/oauth-error.r
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
oauth2.0_error_codes <- c(
400,
401
)

oauth2.0_errors <- c(
"invalid_request",
"invalid_client",
"invalid_grant",
"unauthorized_client",
"unsupported_grant_type",
"invalid_scope"
)

# This implements error checking according to the OAuth2.0
# specification: https://tools.ietf.org/html/rfc6749#section-5.2
known_oauth2.0_error <- function(response) {
if (status_code(response) %in% oauth2.0_error_codes) {
content <- content(response)
if (content$error %in% oauth2.0_errors) {
return(TRUE)
}
}
FALSE
}
4 changes: 4 additions & 0 deletions R/oauth-refresh.R
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ refresh_oauth2.0 <- function(endpoint, app, credentials, user_params = NULL) {
}

response <- POST(refresh_url, body = body, encode = "form")
if (known_oauth2.0_error(response)) {
warning("Unable to refresh token", call. = FALSE)
return(NULL)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather this threw an error, to be consistent with the stop_for_status() below

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But maybe you're trying to separate this into known errors and unknown errors? I'd like to hear your thinking about this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was that here in refresh_oauth2.0 our goal is to get new, valid credentials using our refresh token (obviously), and OAuth2 provides a defined mechanism to inform us of that this can't happen - by returning invalid_grant in the error response.

Separating this out from stop_for_status allows us to still catch any errors that occur using stop_for_status - 404s, redirects, internal server errors, etc. All of these are errors that prevent us from getting refreshed credentials, but there isn't any specific information in them that tells us what to do.

Contrast that with the OAuth2 error(s) that tell us we have an invalid token, which is why our logic is to clear the token from our cache.

Taking it a step further, the OAuth2 spec is pretty clear that invalid_grant is the error we should look for in the case of the refresh token. I've currently written this in such a way that has_oauth2.0_error will return TRUE even in the case of OAuth2 errors that aren't specific to the refresh token.

I did opt to cast a broader net, but there's an argument to be made that this function should be called has_oauth2.0_refresh_token_error(response) and only return TRUE when invalid_grant is present.

Thoughts?

}
stop_for_status(response)

refresh_data <- content(response)
Expand Down
9 changes: 7 additions & 2 deletions R/oauth-token.r
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,14 @@ Token2.0 <- R6::R6Class("Token2.0", inherit = Token, list(
!is.null(self$credentials$refresh_token)
},
refresh = function() {
self$credentials <- refresh_oauth2.0(self$endpoint, self$app,
cred <- refresh_oauth2.0(self$endpoint, self$app,
self$credentials, self$params$user_params)
self$cache()
if (is.null(cred)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic here could be made more clear. Maybe:

creds <- refresh_oauth2.0(self$endpoint, self$app,
  self$credentials, self$params$user_params)
if (is.null(creds) {
  remove_cached_token(self)
  warning("Unable refresh token", call. = FALSE)
} else {
  self$credentials <- creds
  self$cache()
}
self

?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe the warning should occur in refresh_oauth2.0()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the warning should probably occur in refresh_oauth2.0 in thinking about it.

remove_cached_token(self)
} else {
self$credentials <- cred
self$cache()
}
self
},
sign = function(method, url) {
Expand Down