From 23b29689b0b4a8101e986d89e3585cf75ad73885 Mon Sep 17 00:00:00 2001 From: lukakoning Date: Wed, 16 Sep 2026 21:56:20 +0200 Subject: [PATCH 1/3] Use the authenticated user ID for Shiny OAuth identity --- NEWS.md | 2 ++ R/auth-shiny.R | 3 ++- man/auth_shiny.Rd | 1 + tests/testthat/test-auth-shiny.R | 12 ++++++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 tests/testthat/test-auth-shiny.R diff --git a/NEWS.md b/NEWS.md index 398afdf..02a8305 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # arcgisutils 0.6.1 (development) +- Shiny authentication now identifies the signed-in user by their user ID instead of the organization ID. + - `arc_gp_job` gains `submit_path` and `results_path`, so services that are not `GPServer` endpoints can use it. A tile export submits to `exportTiles` and reads its download URL off the job resource, and no longer needs a parallel job class of its own. - `arc_gp_job$await()` gains a `timeout` argument, defaulting to `Inf`. diff --git a/R/auth-shiny.R b/R/auth-shiny.R index 6a467e2..81c3afd 100644 --- a/R/auth-shiny.R +++ b/R/auth-shiny.R @@ -25,6 +25,7 @@ #' #' The user info returned from `auth$token@userinfo` has the same structure #' as [arc_portal_self()]. See that function's documentation for available fields. +#' The authenticated user is identified by `user$id` in that response. #' #' The below example is derived from the shinyOAuth documentation. #' @@ -94,7 +95,7 @@ oauth_provider_arcgis <- function( token_url = paste0(host, "/sharing/rest/oauth2/token"), token_auth_style = "body", userinfo_url = paste0(host, "/sharing/rest/portals/self?f=json"), - userinfo_id_selector = function(x) x$id + userinfo_id_selector = function(x) x$user$id ) } diff --git a/man/auth_shiny.Rd b/man/auth_shiny.Rd index 6104544..1ce4fdd 100644 --- a/man/auth_shiny.Rd +++ b/man/auth_shiny.Rd @@ -51,6 +51,7 @@ Creating the client inside the server function will result in authentication fai The user info returned from \code{auth$token@userinfo} has the same structure as \code{\link[=arc_portal_self]{arc_portal_self()}}. See that function's documentation for available fields. +The authenticated user is identified by \code{user$id} in that response. The below example is derived from the shinyOAuth documentation. diff --git a/tests/testthat/test-auth-shiny.R b/tests/testthat/test-auth-shiny.R new file mode 100644 index 0000000..370002b --- /dev/null +++ b/tests/testthat/test-auth-shiny.R @@ -0,0 +1,12 @@ +test_that("Shiny OAuth identifies users within the same organization", { + skip_if_not_installed("shinyOAuth") + provider <- oauth_provider_arcgis() + select_id <- provider@userinfo_id_selector + + alice <- list(id = "organization-id", user = list(id = "alice-id")) + bob <- list(id = "organization-id", user = list(id = "bob-id")) + expect_identical(select_id(alice), "alice-id") + expect_identical(select_id(bob), "bob-id") + expect_null(select_id(list(id = "organization-id"))) + expect_null(select_id(list(id = "organization-id", user = list()))) +}) From 261bd436cb3df9d3e71f6fe1cd6ecb68c9b03a31 Mon Sep 17 00:00:00 2001 From: lukakoning Date: Wed, 16 Sep 2026 21:59:04 +0200 Subject: [PATCH 2/3] Opt in to missing token type compatibility for ArcGIS --- DESCRIPTION | 2 +- NEWS.md | 1 + R/auth-shiny.R | 8 ++-- man/auth_shiny.Rd | 3 +- tests/testthat/test-auth-shiny.R | 66 +++++++++++++++++++++++++++++++- 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 2a00e99..34bf766 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -52,7 +52,7 @@ Suggests: jsonify, testthat (>= 3.0.0), vctrs, - shinyOAuth, + shinyOAuth (>= 0.6.0), nanoparquet Config/rextendr/version: 0.4.2 Config/testthat/edition: 3 diff --git a/NEWS.md b/NEWS.md index 02a8305..4a53c2e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # arcgisutils 0.6.1 (development) - Shiny authentication now identifies the signed-in user by their user ID instead of the organization ID. +- Shiny login and refresh accept ArcGIS token responses that omit `token_type`. This integration now requires shinyOAuth 0.6.0 or later. - `arc_gp_job` gains `submit_path` and `results_path`, so services that are not `GPServer` endpoints can use it. A tile export submits to `exportTiles` and reads its download URL off the job resource, and no longer needs a parallel job class of its own. diff --git a/R/auth-shiny.R b/R/auth-shiny.R index 81c3afd..f5398ee 100644 --- a/R/auth-shiny.R +++ b/R/auth-shiny.R @@ -9,7 +9,8 @@ #' #' @details #' -#' Authentication with `auth_shiny()` requires the package `shinyOAuth`. +#' Authentication with `auth_shiny()` requires `shinyOAuth` 0.6.0 or later. +#' ArcGIS token responses that omit the token type are treated as Bearer tokens. #' When creating an OAuth app in ArcGIS Online / Enterprise, ensure that #' the app has a valid redirect URI. This **must be** the same redirect URI #' that is used by `shinyOAuth` and **must be** the same port as the @@ -88,12 +89,13 @@ oauth_provider_arcgis <- function( ver <- utils::packageVersion("arcgisutils") arc_ver <- paste0("arcgisutils v", ver) - rlang::check_installed("shinyOAuth") + rlang::check_installed("shinyOAuth", version = "0.6.0") shinyOAuth::oauth_provider( name = arc_ver, auth_url = paste0(host, "/sharing/rest/oauth2/authorize"), token_url = paste0(host, "/sharing/rest/oauth2/token"), token_auth_style = "body", + allow_missing_token_type = TRUE, userinfo_url = paste0(host, "/sharing/rest/portals/self?f=json"), userinfo_id_selector = function(x) x$user$id ) @@ -111,7 +113,7 @@ auth_shiny <- function( host = arc_host(), ... ) { - rlang::check_installed("shinyOAuth") + rlang::check_installed("shinyOAuth", version = "0.6.0") dots <- rlang::list2(...) check_dots_named(dots) diff --git a/man/auth_shiny.Rd b/man/auth_shiny.Rd index 1ce4fdd..08d6021 100644 --- a/man/auth_shiny.Rd +++ b/man/auth_shiny.Rd @@ -35,7 +35,8 @@ authentication experience for \code{{shiny}} applications via the package use \code{auth_shiny()} will enable user-specific behavior and authentications. } \details{ -Authentication with \code{auth_shiny()} requires the package \code{shinyOAuth}. +Authentication with \code{auth_shiny()} requires \code{shinyOAuth} 0.6.0 or later. +ArcGIS token responses that omit the token type are treated as Bearer tokens. When creating an OAuth app in ArcGIS Online / Enterprise, ensure that the app has a valid redirect URI. This \strong{must be} the same redirect URI that is used by \code{shinyOAuth} and \strong{must be} the same port as the diff --git a/tests/testthat/test-auth-shiny.R b/tests/testthat/test-auth-shiny.R index 370002b..8a53775 100644 --- a/tests/testthat/test-auth-shiny.R +++ b/tests/testthat/test-auth-shiny.R @@ -1,5 +1,5 @@ test_that("Shiny OAuth identifies users within the same organization", { - skip_if_not_installed("shinyOAuth") + skip_if_not_installed("shinyOAuth", minimum_version = "0.6.0") provider <- oauth_provider_arcgis() select_id <- provider@userinfo_id_selector @@ -10,3 +10,67 @@ test_that("Shiny OAuth identifies users within the same organization", { expect_null(select_id(list(id = "organization-id"))) expect_null(select_id(list(id = "organization-id", user = list()))) }) + +test_that("Shiny login and refresh accept ArcGIS responses without token_type", { + skip_if_not_installed("shinyOAuth", minimum_version = "0.6.0") + client <- auth_shiny( + client = "test-client", + secret = "test-secret", + redirect_uri = "http://localhost:8100" + ) + expect_true(client@provider@allow_missing_token_type) + expect_identical(client@provider@allowed_token_types, "Bearer") + token_requests <- 0L + userinfo_requests <- 0L + authorization <- NULL + httr2::local_mocked_responses(function(req) { + if (identical(req$url, client@provider@token_url)) { + token_requests <<- token_requests + 1L + body <- if (token_requests == 1L) { + '{"access_token":"first-access","expires_in":1800,"username":"alice","ssl":true,"refresh_token":"refresh","refresh_token_expires_in":604799}' + } else { + '{"access_token":"refreshed-access","expires_in":1800,"username":"alice","ssl":true}' + } + } else { + userinfo_requests <<- userinfo_requests + 1L + authorization <<- httr2::req_dry_run( + req, + quiet = TRUE, + redact_headers = FALSE + )$headers[["authorization"]] + body <- '{"id":"organization-id","user":{"id":"alice-id","username":"alice"}}' + } + httr2::response( + url = req$url, + status_code = 200L, + headers = list("Content-Type" = "application/json"), + body = charToRaw(body) + ) + }) + + browser <- paste(rep("ab", 64), collapse = "") + url <- shinyOAuth::prepare_call(client, browser_token = browser) + state <- httr2::url_parse(url)$query$state + token <- shinyOAuth::handle_callback( + client, + code = "test-code", + state = state, + browser_token = browser + ) + expect_identical(token@access_token, "first-access") + expect_identical(token@token_type, "Bearer") + expect_identical( + client@provider@userinfo_id_selector(token@userinfo), + "alice-id" + ) + expect_identical(authorization, "Bearer first-access") + + refreshed <- shinyOAuth::refresh_token(client, token) + expect_identical(refreshed@access_token, "refreshed-access") + expect_identical(refreshed@token_type, "Bearer") + expect_identical(refreshed@refresh_token, "refresh") + expect_identical(refreshed@userinfo, token@userinfo) + expect_identical(authorization, "Bearer refreshed-access") + expect_identical(token_requests, 2L) + expect_identical(userinfo_requests, 2L) +}) From 33777bb90c71f5cc5892dd991083f1bf57cce265 Mon Sep 17 00:00:00 2001 From: Josiah Parry Date: Wed, 16 Sep 2026 13:42:18 -0700 Subject: [PATCH 3/3] Update R/auth-shiny.R --- R/auth-shiny.R | 1 - 1 file changed, 1 deletion(-) diff --git a/R/auth-shiny.R b/R/auth-shiny.R index f5398ee..601b99d 100644 --- a/R/auth-shiny.R +++ b/R/auth-shiny.R @@ -10,7 +10,6 @@ #' @details #' #' Authentication with `auth_shiny()` requires `shinyOAuth` 0.6.0 or later. -#' ArcGIS token responses that omit the token type are treated as Bearer tokens. #' When creating an OAuth app in ArcGIS Online / Enterprise, ensure that #' the app has a valid redirect URI. This **must be** the same redirect URI #' that is used by `shinyOAuth` and **must be** the same port as the