---
title: "Keep and use multiple OAuth authorizations"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Keep and use multiple OAuth authorizations}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

shinyOAuth allows you to keep multiple OAuth authorizations in one Shiny app.
Users can connect several services, choose which saved authorization to use for
a request, and disconnect each one separately.

This is useful for a dashboard that combines data from several APIs, or an app
where users need access to several accounts at the same service.

This vignette builds an app that connects to two services and reads data from
either one. It uses browser retention to keep both authorizations available
when the user returns from a provider's login page.

## Client configuration and connections

Create an `OAuthClient` for each service with its registration details, scopes
and API addresses. Put the clients in a named list and pass it to
`oauth_connections()`. Your app uses those names to choose which service to
connect when a user clicks a Connect button.

Each successful authorization creates a separate connection. Inside `server()`,
retrieve that `OAuthConnection` to make requests with its current credentials.

One client can have several connections. Connecting A again creates another
authorization alongside the first. Account selection happens at the provider;
it may reuse the account already signed in there.

A provider may also return the same refresh credential for both connections.
The manager queues overlapping asynchronous refreshes for that credential.
If it rotates or its refresh outcome is uncertain, other connections holding
the old credential become uncertain and require authorization again. The manager
keeps their account identity and permissions separate. Successful revocation
also stops use of known copies of the revoked credential within that manager.

For apps that need one authorization per Shiny session, see the `oauth_ui()`
and `oauth_module_server()` example in [Usage](usage.html).

## Register two clients

This template uses two fictional services. Replace their endpoints, credentials,
scope names and API path with values from your providers' documentation. It
assumes authorization code flow with S256 PKCE and confidential clients using
HTTP Basic client authentication. The providers must support that registration.

Register these exact redirect URIs with the respective services:

| Service | Redirect URI |
| --- | --- |
| A | `http://127.0.0.1:8100/callback/a` |
| B | `http://127.0.0.1:8100/callback/b` |

These loopback HTTP addresses are for local development, if your providers
permit them. Use your public HTTPS origin and corresponding registered
callbacks when deploying, and omit the owner's `allow_http_loopback` exception.
Store the two client IDs and secrets in the environment
variables used below, outside source control.

Copy the next three code blocks, in order, into `app.R`.

### 1. Configure clients outside `server()`

```{r multiple-clients, eval = FALSE}
library(shiny)
library(shinyOAuth)

app_origin <- "http://127.0.0.1:8100"
callbacks <- paste0(app_origin, c("/callback/a", "/callback/b"))

provider_a <- oauth_provider(
  name = "Service A",
  auth_url = "https://login.a.example/authorize",
  token_url = "https://login.a.example/token",
  token_auth_style = "header",
  use_pkce = TRUE,
  pkce_method = "S256"
)
provider_b <- oauth_provider(
  name = "Service B",
  auth_url = "https://login.b.example/authorize",
  token_url = "https://login.b.example/token",
  token_auth_style = "header",
  use_pkce = TRUE,
  pkce_method = "S256"
)

client_a <- oauth_client(
  provider_a,
  client_id = Sys.getenv("SERVICE_A_CLIENT_ID"),
  client_secret = Sys.getenv("SERVICE_A_CLIENT_SECRET"),
  redirect_uri = callbacks[[1]],
  scopes = "records.read",
  resource_bases = c(api = "https://api.a.example/v1"),
  required_scopes = "records.read",
  label = "Service A",
  authorization_server_mode = "multi_redirect_uri",
  authorization_server_redirect_uris = callbacks
)
client_b <- oauth_client(
  provider_b,
  client_id = Sys.getenv("SERVICE_B_CLIENT_ID"),
  client_secret = Sys.getenv("SERVICE_B_CLIENT_SECRET"),
  redirect_uri = callbacks[[2]],
  scopes = "records.read",
  resource_bases = c(api = "https://api.b.example/v1"),
  required_scopes = "records.read",
  label = "Service B",
  authorization_server_mode = "multi_redirect_uri",
  authorization_server_redirect_uris = callbacks
)

clients <- list(a = client_a, b = client_b)
```

`a` and `b` are app-local selectors, independent of the registered `client_id`.
`api` names an approved resource within each client. For connection A,
`[["request"]]("api", "records")` means `https://api.a.example/v1/records`.

The default callback policy gives each client a distinct route. Supply the
**complete callback vector to each client**, even though each uses its own
`redirect_uri`. The manager verifies that each client's declared set includes
all of its callback routes, comparing canonical origins and paths. Additional
routes used elsewhere in the application may also be included. Shared
callback routes are an advanced alternative documented in `?oauth_connections`.

### 2. Keep authorizations when the browser returns

```{r multiple-manager, eval = FALSE}
# Temporary keys for this local demo, generated once per R process.
# In a deployment, load two independent 32-byte raw keys from secret storage.
keys <- list(
  credentials = openssl::rand_bytes(32),
  owner = openssl::rand_bytes(32)
)

manager <- oauth_connections(
  clients,
  app_origin = app_origin,
  retention = "browser",
  owner_policy = oauth_browser_owner(allow_http_loopback = TRUE),
  store = oauth_connection_store_memory(),
  keys = keys
)
```

Browser retention uses an HttpOnly cookie containing an opaque local owner ID.
Tokens stay encrypted in the R process. The cookie associates saved connections
with this browser; it does not identify a person or log them into either service.

Without `retention = "browser"`, the default is `"shiny"`: existing connections
are discarded when that Shiny session ends. Merely configuring multiple clients
does not preserve A while the browser goes away to authorize B.

Keep the manager, store and keys outside `server()` so returning Shiny sessions
use the same configuration. This memory store supports **one R process** and
does not survive an R restart. Stable keys alone do not make it persistent or
share records between workers. Browser ownership also has idle and absolute
expiry limits. Automatic refresh and API reads do not reset inactivity. Call
`auth[["touch"]]()` from a user input event handler, as the Read handler below does,
to count an application action as activity. Explicit refresh also counts as
activity; the absolute expiry remains fixed. For an app with its own verified
account login, see `?oauth_account_owner` and `retention = "account"`.

Size the owner registry and credential store separately. Both owner factories
accept `max_entries` (default 1,000) per manager. Retained browser entries count
browsers that successfully authorize a service. Recent visitors occupy a separate
provisional pool with the same limit; entries without a pending authorization may
be replaced and expire after at most five minutes. Pending authorizations protect
their browser entries until cancellation or transaction expiry, subject to the
owner's idle and absolute limits. If all provisional entries are protected, new
visitors are rejected. Account entries include logged-out
login generations until their reauthentication deadline. A full retained registry
rejects new retained owners while preserving existing retained sessions and
retirement records.
`oauth_connection_store_memory(max_entries = ...)` controls a different limit:
successful authorization transactions occupy capacity for the store's full
`max_age`, including after a connection is disconnected. Neither limit measures
only the number of currently open Shiny sessions.

### 3. Select a connection and make a request

```{r multiple-app, eval = FALSE}
base_ui <- fluidPage(
  use_shinyOAuth(),
  h2("My connected services"),
  actionButton("connect_a", "Connect Service A"),
  actionButton("connect_b", "Connect Service B"),
  selectInput("connection_id", "Saved authorization", choices = character()),
  actionButton("read", "Read records"),
  actionButton("disconnect", "Disconnect selected"),
  actionButton("logout", "Disconnect all and leave"),
  textOutput("result"),
  textOutput("auth_error")
)
ui <- oauth_connections_ui(base_ui, "services", manager)

server <- function(input, output, session) {
  auth <- oauth_connections_server("services", manager)
  observeEvent(input[["connect_a"]], auth[["connect"]]("a"))
  observeEvent(input[["connect_b"]], auth[["connect"]]("b"))

  observe({
    rows <- auth[["connections"]]()
    ids <- vapply(rows, function(x) x[["connection_id"]], character(1))
    labels <- vapply(seq_along(rows), function(i) {
      paste(rows[[i]][["client_label"]], i, paste0("(", rows[[i]][["status"]], ")"))
    }, character(1))
    selected <- isolate(input[["connection_id"]])
    if (!length(selected) || !selected %in% ids) selected <- head(ids, 1)
    updateSelectInput(session, "connection_id",
      choices = setNames(ids, labels), selected = selected)
  })

  selected_connection <- reactive({
    rows <- auth[["connections"]]()
    ids <- vapply(rows, function(x) x[["connection_id"]], character(1))
    req(input[["connection_id"]], input[["connection_id"]] %in% ids)
    auth[["connection"]](input[["connection_id"]])
  })

  result <- eventReactive(input[["read"]], {
    auth[["touch"]]()
    connection <- selected_connection()
    req(connection[["is_usable"]]())
    message <- tryCatch({
      response <- connection[["request"]](
        "api", "records", required_scopes = "records.read"
      )
      httr2::resp_check_status(response)
      # Parse the body here according to your API's schema.
      paste("Records request succeeded; HTTP", httr2::resp_status(response))
    }, error = function(e) "Could not read records. Check the connection and try again.")
    list(connection_id = connection[["id"]], message = message)
  })

  output[["result"]] <- renderText({
    connection <- selected_connection()
    req(connection[["is_usable"]]())
    value <- result()
    req(identical(value[["connection_id"]], connection[["id"]]))
    value[["message"]]
  })
  observeEvent(input[["disconnect"]], {
    auth[["disconnect"]](selected_connection()[["id"]])
  })
  observeEvent(input[["logout"]], auth[["logout"]]())
  output[["auth_error"]] <- renderText({
    if (length(auth[["errors"]]())) "Authorization is unavailable. Try connecting again."
  })
}

runApp(shinyApp(ui, server, uiPattern = ".*"),
  host = "127.0.0.1", port = 8100, launch.browser = FALSE)
```

Open `http://127.0.0.1:8100` in a regular browser. Connect A, return, then connect
B. Both authorizations should appear in the selector. Select each and read its
records. Disconnect removes the selected local connection; provider revocation
may affect other authorizations as explained below. The numeric suffix only
distinguishes rows in this demo; use the opaque `connection_id` as the stable key.

The UI wrapper handles callback pages; `uiPattern = ".*"` lets Shiny serve those
paths. Use the same module ID (`"services"`) in UI and server. Deploying under a
subpath or behind a proxy can also require `app_base_path` and a trusted
`request_uri_resolver`; see `?oauth_connections_ui`.

## Requests, permissions and refresh

For optional writes, first configure the applicable client with
`scopes = c("records.read", "records.write")` and obtain a new authorization.
Replace these example permissions with the provider's actual scope names.
Add `actionButton("write", "Update selected record")` to the UI and place this
observer inside `server()`, after `selected_connection` is defined:

```{r multiple-request-body, eval = FALSE}
observeEvent(input[["write"]], {
  auth[["touch"]]()
  connection <- selected_connection()
  req(connection[["is_usable"]]())
  tryCatch({
    response <- connection[["request"]]("api", "records/example", method = "PUT",
      required_scopes = "records.write", configure = function(req) {
        req |>
          httr2::req_body_json(list(name = "Updated record")) |>
          httr2::req_headers(`If-Match` = 'W/"7"')
      })
    httr2::resp_check_status(response)
    showNotification("Record updated.")
  }, error = function(e) {
    showNotification("Could not update the record.", type = "error")
  })
})
```

`configure` adds a body or application headers to the unauthenticated request;
`httr2::req_body_form()` and `httr2::req_body_raw()` are also supported.
The connection retains control of its destination, authentication and redirect
policy. The write scope is optional for connecting, but required for this action.

`OAuthConnection[["request"]]()` uses the existing `perform_resource_req()` transport.
It adds selection of the current owned credentials and checks the approved API
base and required permissions. Existing code using `perform_resource_req(token,
url)` remains supported. For a single existing module, `oauth_connection(client,
reactive(auth[["token"]]))` supplies the optional wrapper without adding retention.

`resource_bases` limits destinations, including absolute pagination links and
their base paths. It does not automatically send OAuth's `resource` parameter
or prove the audience of an opaque token; configure resource indicators
explicitly when your provider requires them. See [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html).

`required_scopes` on the client is the minimum for usable access. Requesting
additional optional scopes can produce a `limited` connection if only some are
granted. Supply an operation's additional permissions in `[["request"]]()`; ordinary
OAuth scope names are compared literally, and permissions are not inferred from
the API path. A usable connection does not guarantee the API will accept every
request.

If the provider issues a refresh token, the manager attempts refresh at expiry;
`refresh_proactively = TRUE` on `oauth_connections_server()` starts earlier.
Explicit refresh is also available inside a server observer:

```{r multiple-refresh, eval = FALSE}
observeEvent(input[["refresh"]], {
  tryCatch(selected_connection()[["refresh"]](), error = function(e) {
    showNotification("Could not refresh. Connect again if access has ended.", type = "error")
  })
})
```

Add `actionButton("refresh", "Refresh selected")` to the UI to use this snippet.
The example uses the default synchronous mode. `async = TRUE` needs worker setup
and promise handling for explicit refresh; see [Usage](usage.html#asynchronous-execution).

Retention itself does not request a refresh token. OAuth refresh cannot expand
the authorization's scope ([RFC 6749, section 6](https://www.rfc-editor.org/rfc/rfc6749.html#section-6)).
`[["refresh"]](scopes = ...)` can deliberately narrow a managed connection's accepted
permissions; getting broader access again requires a new authorization.

Disconnect immediately removes local usability. Remote token revocation is
best effort and reported separately in the return value. This template does not
configure revocation endpoints, so add each provider's documented
`revocation_url` if supported. `logout()` also ends this local owner session;
it does not sign the person out of the external services or your own account
login system.

Separate connection IDs represent separate local records. A provider can reuse
the same upstream grant for repeated authorizations and invalidate all related
credentials when one is revoked; OAuth explicitly permits this behavior
([RFC 7009, section 2.1](https://www.rfc-editor.org/rfc/rfc7009.html#section-2.1)).
For example, Google's documented revocation can affect all scopes granted to a
project and invalidate related tokens
([Google's revocation policy](https://developers.google.com/identity/protocols/oauth2/web-server#tokenrevoke)).
Repeated consent, distinct token strings, and local connection IDs do not prove
independent upstream grants.

The default `auth[["disconnect"]](id)` requests remote revocation. To remove only the
local connection, use `auth[["disconnect"]](id, revoke = FALSE)`; those removed
credentials remain valid remotely until expiry or provider-side revocation.
Choose this deliberately using the provider's documented behavior. Sibling
connections can still report `active` locally after remote revocation: always
handle API authorization failures and offer a new authorization.

## Keep an ordinary OIDC login alongside SMART connections

For an OIDC client inside the manager, select validated identity explicitly in
the owning server session:

```{r multiple-identity, eval = FALSE}
identity <- selected_connection()[["identity"]](userinfo = c("name", "email"))
# identity[["id_token_claims"]] contains only iss and sub by default.
# identity[["userinfo"]] contains only the selected, previously fetched fields.
```

This requires a usable connection with `openid` and a validated ID token.
UserInfo fields are returned only when its `sub` matches that token. Configure
the provider's UserInfo endpoint and required profile scopes if those fields
are needed; the accessor does not fetch them. Treat the returned data as
sensitive. Identity stays out of connection summaries and printing, and raw
credentials are never returned. These snapshots can predate the latest OAuth
refresh; they do not establish fresh login or an account-retention owner.

Use one callback wrapper with `additional_clients` for ordinary modules. Given
your existing `login_provider` and a reviewed SMART discovery snapshot `site`,
configure both registered callbacks with the same multi-server defense:

```{r mixed-login-configuration, eval = FALSE}
callbacks <- c("https://app.example/login/callback", "https://app.example/fhir/callback")
login_client <- oauth_client(login_provider, "registered-login-app",
  redirect_uri = callbacks[[1]], scopes = c("openid", "profile"),
  authorization_server_mode = "multi_redirect_uri",
  authorization_server_redirect_uris = callbacks)
fhir_client <- smart_client(site, "registered-fhir-app", callbacks[[2]],
  scopes = c("launch/patient", "patient/Patient.r"),
  authorization_server_mode = "multi_redirect_uri",
  authorization_server_redirect_uris = callbacks)
manager <- oauth_connections(list(fhir = fhir_client), "https://app.example",
  retention = "browser", owner_policy = oauth_browser_owner(),
  store = oauth_connection_store_memory(), keys = keys)
base_ui <- fluidPage(actionButton("sign_in", "Sign in"),
  actionButton("connect_fhir", "Connect FHIR"))
ui <- oauth_connections_ui(base_ui, "health", manager,
  additional_clients = list(login = login_client))
server <- function(input, output, session) {
  login <- oauth_module_server("login", login_client, auto_redirect = FALSE)
  health <- oauth_connections_server("health", manager)
  observeEvent(input[["sign_in"]], login[["request_login"]]())
  observeEvent(input[["connect_fhir"]], health[["connect"]]("fhir"))
}
shinyApp(ui, server, uiPattern = ".*")
```

Supply the existing registration's secret or key to `login_client` as required.
`keys` is the deployment key configuration described above. The additional
client names are ordinary module IDs, without the manager namespace. Both GET
and form-post callbacks are supported; select each registered response mode on
its client. Additional clients need distinct routes from managed clients.

This preserves the ordinary module's token and logout API. The manager neither
stores that login token nor changes its lifecycle. A full browser navigation
still ends the ordinary Shiny session; retain application login through your
existing application-session mechanism if needed. Disable automatic login
redirects while processing other authorizations, as in this example. Browser
ownership for retained SMART grants is independent of OIDC identity; use account
retention with a trusted local-account resolver when that binding is required.
