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

Enable oauth_flow_auth_code to parse URL for component code and state #326

Merged
merged 10 commits into from
Oct 11, 2023
5 changes: 4 additions & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# httr2 (development version)

* `oauth_flow_auth_code()` allows the user to enter a URL that contains
authorization `code` and `state` parameters (@fh-mthomson, #326).

* `req_oauth_device()` now takes a `auth_url` parameter making it usable
(#331, @taerwin).

Expand Down Expand Up @@ -44,7 +47,7 @@
* `oauth_flow_auth_code()` deprecates `host_name` and `port` arguments in favour
of using `redirect_uri`. It also deprecates `host_ip` since it seems unlikely
that changing this is ever useful.

* New `oauth_cache_path()` returns the path that httr2 uses for caching OAuth
tokens. Additionally, you can now change the cache location by setting the
`HTTR2_OAUTH_CACHE` env var.
Expand Down
58 changes: 39 additions & 19 deletions R/oauth-flow-auth-code.R
Original file line number Diff line number Diff line change
Expand Up @@ -437,29 +437,49 @@ is_hosted_session <- function() {
}

oauth_flow_auth_code_read <- function(state) {
code <- trimws(readline("Enter authorization code: "))
# We support two options here:
#
# 1) The original {gargle} style, where the user copy & pastes a
# base64-encoded JSON object with both the code and state. This is used on
# https://www.tidyverse.org/google-callback/; and
#
# 2) The full manual approach, where the code and state are entered
# independently.
result <- tryCatch(
jsonlite::fromJSON(rawToChar(openssl::base64_decode(code))),
error = function(e) {
list(
code = code,
state = trimws(readline("Enter state parameter: "))
)
})
if (!identical(result$state, state)) {
code <- trimws(readline("Enter authorization code or URL: "))

if (is_string_url(code)) {
# minimal setup where user copy & pastes a URL
parsed <- url_parse(code)

code <- parsed$query$code
new_state <- parsed$query$state
} else if (is_base64_json(code)) {
# {gargle} style, where the user copy & pastes a base64-encoded JSON
# object with both the code and state. This is used on
# https://www.tidyverse.org/google-callback/
json <- jsonlite::fromJSON(rawToChar(openssl::base64_decode(code)))

code <- json$code
new_state <- json$state
} else {
# Full manual approach, where the code and state are entered
# independently.

new_state <- trimws(readline("Enter state parameter: "))
}

if (!identical(state, new_state)) {
abort("Authentication failure: state does not match")
}
result$code

code
}

is_string_url <- function(x) grepl("^https?://", x)

is_base64_json <- function(x) {
tryCatch(
{
jsonlite::fromJSON(rawToChar(openssl::base64_decode(x)))
TRUE
},
error = function(err) FALSE
)
}


# Determine whether we can fetch the OAuth authorization code from an external
# source without user interaction.
can_fetch_oauth_code <- function(redirect_url) {
Expand Down
13 changes: 10 additions & 3 deletions tests/testthat/test-oauth-flow-auth-code.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,21 @@ test_that("so-called 'hosted' sessions are detected correctly", {
})
})

test_that("URL embedding authorisation code and state can be input manually", {
local_mocked_bindings(
readline = function(prompt = "") "https://x.com?code=code&state=state"
)
expect_equal(oauth_flow_auth_code_read("state"), "code")
expect_error(oauth_flow_auth_code_read("invalid"), "state does not match")
})

test_that("JSON-encoded authorisation codes can be input manually", {
state <- base64_url_rand(32)
input <- list(state = state, code = "abc123")
input <- list(state = "state", code = "code")
encoded <- openssl::base64_encode(jsonlite::toJSON(input))
local_mocked_bindings(
readline = function(prompt = "") encoded
)
expect_equal(oauth_flow_auth_code_read(state), "abc123")
expect_equal(oauth_flow_auth_code_read("state"), "code")
expect_error(oauth_flow_auth_code_read("invalid"), "state does not match")
})

Expand Down
Loading