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. For initial login setup, start with Usage.
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.
# 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.
Get a connection from the module and pass target to
select the API. Replace existing_contacts_sdk() with your
contacts package’s client constructor:
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.
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 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:
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().
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.
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.
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.
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.
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 and refresh-token guidance.
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(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.