--- title: "Use your Shiny login with other R packages" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Use your Shiny login with other R packages} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## 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()`: ```{r, eval = FALSE} 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](usage.html). ## 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. ```{r, eval = FALSE} 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](usage.html#token-expiry-and-session-length). ## 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: ```{r, eval = FALSE} 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](multiple-authorizations.html). For one login with separate tokens for several APIs, see [Token targets](token-targets.html). 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](token-targets.html#advanced-microsoft-permissions-and-consent) 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](smart-on-fhir.html). ## Advanced: asynchronous token acquisition Use `async = TRUE` when refreshing a token should not block the app. Set up workers first, as shown in [Usage](usage.html#asynchronous-execution): ```{r, eval = FALSE} # 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): ```{r, eval=FALSE} # 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](https://shiny.posit.co/r/articles/improve/nonblocking/) for scheduling, and [OAuthConnection](https://lukakoning.github.io/shinyOAuth/reference/OAuthConnection.html) for connection lifetime and permission-reduction details.