Use your Shiny login with other R packages
Source:vignettes/external-integrations.Rmd
external-integrations.RmdReuse a login in another package
An R package or database driver may need an access token, a credential for calling a service on the user’s behalf. Reuse the login managed by shinyOAuth so the user does not need to sign in a second time.
A connection gives your server code access to that login. Ask it for
a current token just before calling the other package; shinyOAuth can
refresh it when needed. In reactive server code, with auth
from oauth_module_server():
connection <- shiny::req(auth$connection())
token <- connection$access_token(required_scopes = "records.read")
existing_sdk_read_records(access_token = token)existing_sdk_read_records() stands for your package’s
function. A scope is a permission name defined by the service;
required_scopes checks the permissions needed for this
operation. The other package sends its own request.
The examples build on the login setup in Usage.
A complete Shiny example
This template uses httr2 so you can see where the token
goes. Replace the placeholder URLs, scopes and credentials with your
provider’s settings. The read button loads records; the output checks
that the saved result still belongs to the current login before
displaying it.
library(shiny)
library(shinyOAuth)
provider <- oauth_provider(
name = "records",
auth_url = "https://login.example/authorize",
token_url = "https://login.example/token",
userinfo_required = FALSE
)
client <- oauth_client(
provider,
client_id = Sys.getenv("OAUTH_CLIENT_ID"),
client_secret = Sys.getenv("OAUTH_CLIENT_SECRET"),
redirect_uri = "https://app.example/callback",
scopes = c("records.read", "records.write"),
required_scopes = "records.read"
)
ui <- oauth_ui(fluidPage(
actionButton("login", "Sign in"),
actionButton("again", "Sign in again"),
actionButton("logout", "Sign out"),
actionButton("load", "Load records"),
textOutput("permissions"),
verbatimTextOutput("records")
), id = "auth", client = client)
server <- function(input, output, session) {
auth <- oauth_module_server(
"auth", client, auto_redirect = FALSE, refresh_proactively = TRUE
)
observeEvent(input$login, {
auth$request_login()
invisible(NULL)
})
observeEvent(input$again, {
auth$reauthorize()
invisible(NULL)
})
observeEvent(input$logout, auth$logout())
output$permissions <- renderText({
connection <- req(auth$connection())
if (connection$has_scopes("records.write")) "Editing available" else "Read only"
})
records <- eventReactive(input$load, {
connection <- req(auth$connection())
data <- tryCatch({
response <- httr2::request("https://api.example/records") |>
httr2::req_auth_bearer_token(
connection$access_token(required_scopes = "records.read")
) |>
httr2::req_perform()
httr2::resp_body_json(response)
}, shinyOAuth_access_error = function(error) {
showNotification("Authorization unavailable. Try again or sign in again.")
req(FALSE)
})
list(authorization = connection$id, data = data)
})
output$records <- renderPrint({
connection <- req(auth$connection())
result <- records()
req(identical(result$authorization, connection$id))
req(connection$has_scopes("records.read"))
result$data
})
}
shinyApp(ui, server, uiPattern = ".*")uiPattern = ".*" allows the example’s
/callback route. req(auth$connection()) waits
for a login, and $has_scopes() checks whether an optional
feature is available. The call to
$access_token(required_scopes = ...) checks the operation’s
permissions before returning credentials.
The output compares the saved connection ID and current permissions before displaying records, so results from an earlier login disappear after logout or a change of account.
Refresh and signing in again
$access_token() checks the token’s remaining lifetime
and refreshes when needed. For a slow operation, use
min_valid_for_seconds to require more remaining token
lifetime. If no usable token is available, the call raises an error; it
does not open a login page or repeat the other package’s operation.
Offer auth$reauthorize() when the user needs to sign in
again. It preserves locally reduced permissions and ends the previous
local login. Cancelling does not restore it. An ordinary new login
requests the client’s original scopes.
Token retrieval follows the module’s expiry and maximum-login-age settings.
SDK token callbacks and database drivers
Some client libraries, often called SDKs, accept a function that supplies a token when needed. Build that client inside the user’s Shiny session:
sdk <- reactive({
connection <- req(auth$connection())
example_sdk_client(
token = function() connection$access_token(required_scopes = "records.read")
)
})example_sdk_client() stands for the package’s
constructor. The reactive expression builds a client for the current
connection and rebuilds it when login changes; its callback remains
bound to that connection.
If a database driver only accepts a token string, acquire it just before opening or updating the database connection.
Exported bearer tokens carry the provider’s full grant, even when the
app applies narrower local permission checks.
$access_token() rejects DPoP and certificate-bound tokens;
$request() supplies the matching proof or certificate for
those requests.
Several accounts and optional profile retrieval
With a connection manager, choose a saved login with
auth$connection(connection_id). Use
auth$reauthorize(connection_id) to replace it; the
replacement gets a new connection ID. See Multiple authorizations.
For one login with separate tokens for several APIs, see Token targets.
For Microsoft APIs other than Graph, configure
userinfo_required = FALSE on the provider to skip the Graph
profile request. connection$identity() reads selected
previously validated identity fields.
Advanced: connection lifetime and permissions
A connection belongs to one Shiny session, and its methods check that ownership before resolving credentials. Refresh preserves its ID; logout, replacement and session closure invalidate it, even if the same account signs in again.
Reads of connection(), $access_token() and
$has_scopes() do not rerun on refresh alone when login and
permissions stay the same. Use $summary() for status, and
auth$touch() to record explicit user activity for a
retained login.
Reducing permissions and replacing a login
$refresh(scopes = ...) can reduce permissions while
preserving required scopes. Later refreshes and reauthorization keep
that reduction. required_scopes on
$access_token() specifies the permissions needed for that
operation. A provider can reject narrowing; Microsoft
targets do not support explicit API narrowing.
For OIDC, include offline_access when explicitly
narrowing if later reauthorization should still request refresh consent.
Automatic refresh does not remove that request just because the response
omits it.
Standalone SMART replacements keep requested patient and encounter context. Narrowing can remove optional encounter context, while retained patient permissions still require patient context. See SMART on FHIR.
Advanced: asynchronous token acquisition
Use async = TRUE when refreshing a token should not
block the app. Set up workers first, as shown in Usage:
# Configure mirai daemons or a non-sequential future plan before starting Shiny.
# Inside the owning Shiny session:
connection <- req(auth$connection())
pending <- connection$access_token(required_scopes = "records.read", async = TRUE)
promises::then(pending, function(token) {
# Recheck before starting work after an asynchronous boundary.
current <- req(auth$connection())
req(identical(current$id, connection$id))
req(current$has_scopes("records.read"))
existing_sdk_read_records(access_token = token)
})The callback compares the current connection and permissions because logout or a permission change can occur while token acquisition runs. The other package still needs its own background-request mechanism if its calls block R.
Handling token errors
Token-acquisition errors inherit
shinyOAuth_access_error. Inspect
error$context$reason when you want to offer a specific
recovery action:
| Reason | Suggested response |
|---|---|
authorization_unavailable |
Obtain a current connection after login. |
insufficient_scope |
Explain which permission is missing. |
refresh_pending |
Await async work or let the user retry later. |
refresh_unavailable |
Try later or offer a new login. |
interaction_required |
Offer a new login. |
lifetime_unavailable |
The replacement token is too short-lived for the operation. |
unsupported_token_binding |
Use $request() with the configured DPoP or mTLS
transport. |
Argument and configuration errors are separate. Token methods never open a login page or repeat an SDK or database operation after an error.
Advanced: background tasks and saved results
Acquire the token in the owning session before starting background
work. Pass plain inputs and that short-lived token to the worker,
keeping the connection and refresh credential in the session. This
example uses Shiny’s ExtendedTask (Shiny 1.8.1 or
newer):
# Configure future::plan(future::multisession) before starting the app.
# Inside server(), with auth already created:
load_records <- shiny::ExtendedTask$new(function(token, authorization_id) {
promises::future_promise({
list(
authorization_id = authorization_id,
data = existing_sdk_read_records(access_token = token)
)
})
})
shiny::observeEvent(input$load, {
shiny::req(load_records$status() != "running")
connection <- shiny::req(auth$connection())
token <- connection$access_token("records.read", min_valid_for_seconds = 120)
load_records$invoke(token, connection$id)
})
output$records <- shiny::renderTable({
connection <- shiny::req(auth$connection())
shiny::req(connection$has_scopes("records.read"))
result <- load_records$result()
shiny::req(identical(result$authorization_id, connection$id))
result$data
})The requested token lifetime covers worker startup and the operation.
Before displaying results, the output checks connection ID and
permissions; the default req() behavior clears output when
those checks fail.
See the Shiny background-task guide for scheduling, and OAuthConnection for connection lifetime and permission-reduction details.