diff --git a/DESCRIPTION b/DESCRIPTION index a0484041..f56876e9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -28,7 +28,6 @@ Imports: vctrs (>= 0.6.3), withr Suggests: - askpass, bench, clipr, covr, @@ -38,6 +37,7 @@ Suggests: jsonlite, knitr, rmarkdown, + rstudioapi, testthat (>= 3.1.8), tibble, webfakes, diff --git a/NEWS.md b/NEWS.md index 6c8540b2..0410f930 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/R/oauth-flow-auth-code.R b/R/oauth-flow-auth-code.R index aa6e6f58..635d33a2 100644 --- a/R/oauth-flow-auth-code.R +++ b/R/oauth-flow-auth-code.R @@ -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() { - 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 @@ -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)) { @@ -486,6 +473,3 @@ oauth_flow_auth_code_fetch <- function(state) { body <- resp_body_json(resp) body$code } - -# Make base::readline() mockable -readline <- NULL diff --git a/R/oauth-flow-password.R b/R/oauth-flow-password.R index 88bf2f65..60d421f4 100644 --- a/R/oauth-flow-password.R +++ b/R/oauth-flow-password.R @@ -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 } + diff --git a/R/utils.R b/R/utils.R index ce27537a..46a8c282 100644 --- a/R/utils.R +++ b/R/utils.R @@ -281,3 +281,37 @@ 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 if (rlang::is_interactive()) { + # 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)) + } else { + cli::cli_abort("Unable to obtain user input in a non-interactive session.") + } + + 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) +} diff --git a/tests/testthat/test-oauth-flow-auth-code.R b/tests/testthat/test-oauth-flow-auth-code.R index 0ffe0e6e..514f7fa6 100644 --- a/tests/testthat/test-oauth-flow-auth-code.R +++ b/tests/testthat/test-oauth-flow-auth-code.R @@ -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") @@ -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") @@ -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 { diff --git a/vignettes/articles/wrapping-apis.Rmd b/vignettes/articles/wrapping-apis.Rmd index 36338878..ddb7b309 100644 --- a/vignettes/articles/wrapping-apis.Rmd +++ b/vignettes/articles/wrapping-apis.Rmd @@ -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. It's a good idea to extend `get_api_key()` to automatically use your encrypted key to make it easier to write tests: