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

use rstudioapi instead of readline if calling oauth_flow_auth_code() from RStudio IDE #410

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ Imports:
vctrs (>= 0.6.3),
withr
Suggests:
askpass,
bench,
clipr,
covr,
Expand All @@ -38,6 +37,7 @@ Suggests:
jsonlite,
knitr,
rmarkdown,
rstudioapi,
testthat (>= 3.1.8),
tibble,
webfakes,
Expand Down
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* `req_template()` now works when you have a bare `:` in a template that
uses "uri" style (#389).

* `oauth_flow_auth_code()` now uses `rstudioapi::askForPassword()`
(if in RStudio, `readline()` otherwise) when prompting the user to
input auth codes (@fh-mthomson, #406).

# httr2 1.0.0

## Function lifecycle
Expand Down
22 changes: 3 additions & 19 deletions R/oauth-flow-auth-code.R
Original file line number Diff line number Diff line change
Expand Up @@ -403,23 +403,10 @@ oauth_flow_auth_code_pkce <- function() {
)
}

# Try to determine whether we can redirect the user's browser to a server on
# localhost, which isn't possible if we are running on a hosted platform.
#
# Currently this detects RStudio Server, Posit Workbench, and Google Colab. It
# is based on the strategy pioneered by the {gargle} package.
is_hosted_session <- function() {
hadley marked this conversation as resolved.
Show resolved Hide resolved
if (nzchar(Sys.getenv("COLAB_RELEASE_TAG"))) {
return(TRUE)
}
# If RStudio Server or Posit Workbench is running locally (which is possible,
# though unusual), it's not acting as a hosted environment.
Sys.getenv("RSTUDIO_PROGRAM_MODE") == "server" &&
!grepl("localhost", Sys.getenv("RSTUDIO_HTTP_REFERER"), fixed = TRUE)
}


oauth_flow_auth_code_read <- function(state) {
code <- trimws(readline("Enter authorization code or URL: "))
code <- prompt_user("Enter authorization code or URL: ")

if (is_string_url(code)) {
# minimal setup where user copy & pastes a URL
Expand All @@ -439,7 +426,7 @@ oauth_flow_auth_code_read <- function(state) {
# Full manual approach, where the code and state are entered
# independently.

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

if (!identical(state, new_state)) {
Expand Down Expand Up @@ -486,6 +473,3 @@ oauth_flow_auth_code_fetch <- function(state) {
body <- resp_body_json(resp)
body$code
}

# Make base::readline() mockable
readline <- NULL
4 changes: 2 additions & 2 deletions R/oauth-flow-password.R
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ oauth_flow_password <- function(client,

check_password <- function(password, call = caller_env()) {
if (is.null(password)) {
check_installed("askpass", call = call)
password <- askpass::askpass()
password <- prompt_user()
}
check_string(password, call = call)
password
}

31 changes: 31 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,34 @@ create_progress_bar <- function(total,
done = function() cli::cli_progress_done(id = id)
)
}

prompt_user <- function(prompt = "Please enter your password: ") {
if (is_rstudio_session()) {
check_installed("rstudioapi", reason = "to ask user for inputs.")
result <- rstudioapi::askForPassword(prompt)
} else {
# use readline over askpass outside of RStudio IDE since it generalizes better to
# JupyterHub + Google Colab, see https://github.com/r-lib/httr2/pull/410#issuecomment-1852721581
result <- trimws(readline(prompt))
}
result
}

is_rstudio_session <- function() {
!is.na(Sys.getenv("RSTUDIO_PROGRAM_MODE", unset = NA))
}

# Try to determine whether we can redirect the user's browser to a server on
# localhost, which isn't possible if we are running on a hosted platform.
#
# Currently this detects RStudio Server, Posit Workbench, and Google Colab. It
# is based on the strategy pioneered by the {gargle} package.
is_hosted_session <- function() {
if (nzchar(Sys.getenv("COLAB_RELEASE_TAG"))) {
return(TRUE)
}
# If RStudio Server or Posit Workbench is running locally (which is possible,
# though unusual), it's not acting as a hosted environment.
Sys.getenv("RSTUDIO_PROGRAM_MODE") == "server" &&
!grepl("localhost", Sys.getenv("RSTUDIO_HTTP_REFERER"), fixed = TRUE)
}
6 changes: 3 additions & 3 deletions tests/testthat/test-oauth-flow-auth-code.R
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ 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"
prompt_user = 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")
Expand All @@ -32,7 +32,7 @@ test_that("JSON-encoded authorisation codes can be input manually", {
input <- list(state = "state", code = "code")
encoded <- openssl::base64_encode(jsonlite::toJSON(input))
local_mocked_bindings(
readline = function(prompt = "") encoded
prompt_user = function(prompt = "") encoded
)
expect_equal(oauth_flow_auth_code_read("state"), "code")
expect_error(oauth_flow_auth_code_read("invalid"), "state does not match")
Expand All @@ -42,7 +42,7 @@ test_that("bare authorisation codes can be input manually", {
state <- base64_url_rand(32)
sent_code <- FALSE
local_mocked_bindings(
readline = function(prompt = "") {
prompt_user = function(prompt = "") {
if (sent_code) {
state
} else {
Expand Down
4 changes: 2 additions & 2 deletions vignettes/articles/wrapping-apis.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -501,13 +501,13 @@ You can make this approach a little more user friendly by providing a helper tha
```{r}
set_api_key <- function(key = NULL) {
if (is.null(key)) {
key <- askpass::askpass("Please enter your API key")
key <- rstudioapi::askForPassword("Please enter your API key")
}
Sys.setenv("NYTIMES_KEY" = key)
}
```

Using askpass (or similar) here is good practice since you don't want to encourage the user to type their secret key into the console, as mentioned above.
Using `rstudioapi` (or similar) here is good practice since you don't want to encourage the user to type their secret key into the console, as mentioned above.
fh-mthomson marked this conversation as resolved.
Show resolved Hide resolved
fh-mthomson marked this conversation as resolved.
Show resolved Hide resolved

It's a good idea to extend `get_api_key()` to automatically use your encrypted key to make it easier to write tests:

Expand Down
Loading