--- title: "Use one login for several APIs" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Use one login for several APIs} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(shinyOAuth) ``` ## One login, different API tokens A calendar service and a contacts service can require different access tokens, even when the same person uses both. A token target names an API and the permissions the app needs there. The app chooses the target when requesting a token, without requiring a separate login flow for each API. Targets rely on provider support for RFC 8707 resource indicators or Microsoft's API-specific scopes. The provider's registration and consent determine which APIs the login can access. For independent logins or different accounts, use [Multiple authorizations](multiple-authorizations.html). For initial login setup, start with [Usage](usage.html). ## Configure two APIs This example uses a provider with RFC 8707 support. A resource identifier names an API to the login provider; a scope names a permission. `resource_bases` lists the actual HTTP addresses the app may call, which can differ from the resource identifiers. Replace all example settings with your provider's registered values. Microsoft uses the alternative setup shown [below](#microsoft-example). ```{r, eval=FALSE} # Provider supports RFC 8707 resource indicators. provider <- oauth_provider( "workplace", "https://login.example/authorize", "https://login.example/token", token_target_mode = "rfc8707", token_auth_style = "body", 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", token_targets = list( calendar = oauth_token_target( resource = "https://calendar.example/", scopes = c("calendar.read", "calendar.write"), required_scopes = "calendar.read", resource_ids = "calendar_api" ), contacts = oauth_token_target( resource = "https://contacts.example/", scopes = "contacts.read", resource_ids = "contacts_api" ) ), token_target_default = "calendar", resource_bases = c( calendar_api = "https://calendar.example/v1", contacts_api = "https://contacts.example/v1" ) ) ``` `token_target_default` chooses the API when a call omits `target`. `oauth_token_target()` checks each declaration's fields; plain named lists with the same fields remain supported. The client checks provider-specific rules and how declarations relate to one another. Omitting client `scopes` uses the union of API permissions declared by the targets. If you supply `scopes`, include every target's permissions: that explicit list remains the limit and is never expanded automatically. Identity and refresh consent, such as `profile` or `offline_access`, still need an explicit request; OIDC continues to add `openid` when needed. `resource_ids` connects each target to its named API address in `resource_bases`. You can omit those addresses when only exporting tokens to another package. ## Choose a token when making a call Get a connection from the module and pass `target` to select the API. Replace `existing_contacts_sdk()` with your contacts package's client constructor: ```{r, eval=FALSE} server <- function(input, output, session) { auth <- oauth_module_server( "auth", client, reauth_after_seconds = 60 * 60 # One-hour maximum login age ) # Alternatively: auth <- oauth_connections_server("auth", manager) # and select auth$connection(connection_id), or auth$connection() when unique. contacts <- shiny::reactive({ connection <- shiny::req(auth$connection()) token <- connection$access_token("contacts.read", target = "contacts") existing_contacts_sdk(access_token = token)$list_contacts() }) output$can_edit_calendar <- shiny::renderText({ connection <- shiny::req(auth$connection()) connection$has_scopes("calendar.write", target = "calendar") }) shiny::observeEvent(input$save_event, { connection <- shiny::req(auth$connection()) connection$request( "calendar_api", "events", method = "POST", required_scopes = "calendar.write", target = "calendar", refresh = TRUE, configure = function(req) httr2::req_body_json(req, list(title = input$title)) ) }) } ``` `$access_token()` obtains or refreshes the chosen token when needed. `$has_scopes()` checks permissions already recorded; it can return `FALSE` before that target's first token acquisition. `$targets()` lists target status without exposing credentials. Use `$is_usable(target = "contacts")` to check whether that API's current token is usable, or `$summary(target = "contacts")` to inspect its status and expiry. These reads never acquire or refresh a token; an unacquired target has status `"not_acquired"` and is not usable. The example limits login to one hour with `reauth_after_seconds`. Without a limit, local authentication can outlast every API token; `indefinite_session = TRUE` disables that limit. ## Refresh and sign in again For RFC 8707 providers, reduce one target's permissions with, for example, `connection$refresh(scopes = "calendar.read", target = "calendar")`. The selected scopes must include the target's required permissions. `auth$reauthorize()` starts a replacement login with the reduced permissions. For managed connections, pass the connection ID. Once replacement starts, cancelling does not restore the old login. An ordinary new login asks for the client's original permissions again. ## Microsoft example Microsoft uses API-qualified scope names to select the token's intended API. Use the exact names from your registration. This example exports tokens to another package: ```{r, eval=FALSE} provider <- oauth_provider_microsoft( tenant = Sys.getenv("MICROSOFT_TENANT_ID"), userinfo_required = FALSE ) client <- oauth_client( provider, Sys.getenv("OAUTH_CLIENT_ID"), Sys.getenv("OAUTH_CLIENT_SECRET"), redirect_uri = "https://app.example/callback", scopes = c( "openid", "offline_access", "https://graph.microsoft.com/Calendars.Read", "api://your-contacts-api/Contacts.Read" ), token_targets = list( calendar = oauth_token_target( resource = "https://graph.microsoft.com", scopes = "https://graph.microsoft.com/Calendars.Read" ), contacts = oauth_token_target( resource = "api://your-contacts-api", scopes = "api://your-contacts-api/Contacts.Read" ) ), token_target_default = "calendar" ) ``` `userinfo_required = FALSE` skips the Graph profile request, which would be inappropriate for a token intended for another API. Add `resource_bases` and each target's `resource_ids` if you also want to call `$request()`. ## Advanced: provider support and declaration limits Targets share a login and refresh credential but keep separate API tokens and permission limits. Enable `token_target_mode = "rfc8707"` or `"microsoft"` only after confirming provider support; discovery does not infer it. Targets require `userinfo_required = FALSE` and cannot be combined with SMART configuration or the client's `resource` argument. Use the module's `connection()` method, since the older `oauth_connection()` wrapper does not support targets. Up to 16 targets are supported. ### Identity and refresh-consent scopes For OIDC, put identity scopes such as `openid`, `profile` and `email` at client level. `offline_access` requests refresh consent through client `scopes`; it is not an API permission and is rejected in `required_scopes`. The provider still decides whether to issue a refresh token. When narrowing a target, retain any identity and refresh-consent scopes you still need. Later refreshes preserve reductions; a replacement OIDC login still requests `openid` for its new identity check. `connection$identity()` returns the last validated identity, which can outlast an API token. Each target's token and request methods check current API access. ## Advanced: Microsoft permissions and consent Microsoft can return previously consented permissions beyond those needed for the current operation. Connection methods enforce the app's local limits, but an exported token carries the provider's full grant. Explicit API narrowing with `$refresh(scopes = ...)` fails with `unsupported_scope_narrowing` and leaves the login usable. Change provider consent or use a separate registration if the token itself needs fewer permissions. ### Resource identifiers and scope names Use the exact registered resource identifier, including any trailing slash, and fully qualified scope names. Resources can be URIs or GUIDs. The package rejects distinct resources with overlapping prefixes because scope names would be ambiguous. Microsoft permission checks ignore ASCII case; resource identifiers are exact. ### Static permissions and unusual short names Use `resource/.default` as a target's sole API scope for static consent, and include it in client `scopes`. Put actual permission names in `required_scopes`; `.default` itself is not evidence that a permission was granted. If the provider returns unusual short permission names containing `:` or `/`, declare them in `scope_aliases` so the package can interpret the response. See Microsoft's [scope and consent guidance](https://learn.microsoft.com/en-us/entra/identity-platform/scopes-oidc) and [refresh-token guidance](https://learn.microsoft.com/en-us/entra/identity-platform/refresh-tokens). ## Advanced: concurrent refreshes and failures Asynchronous acquisitions coordinate use of the shared refresh credential. A synchronous call reports `refresh_pending` while that work is in progress. A failure that may have consumed the credential can require a new login for the whole connection, even if another API token still works. With `indefinite_session = TRUE`, the single module can retain usable cached tokens after such a failure, but retires the refresh credential. It cannot refresh or acquire another target without a new login. The connection manager does not have this exception. Local `authenticated` state alone does not prove that a selected API token is usable. ### Request customization and background work `$request(refresh = TRUE)` runs synchronously, checks destination and permissions, and obtains usable credentials before sending. It does not replay a failed API operation or start a new login. Use `$request()` for DPoP or mTLS-bound tokens; `$access_token()` exports bearer tokens only. Logout, replacement and session end invalidate old connections and late results. For SDK callbacks and background-result checks, see [External integrations](external-integrations.html).