Skip to contents

Reuse 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.