| Title: | OIDC Authentication and OAuth Authorization for 'shiny' Applications |
|---|---|
| Description: | Provides a simple, configurable framework for 'OpenID Connect' (OIDC) authentication and 'OAuth 2.0' authorization in 'shiny' applications using 'S7' classes. Defines providers, clients, and tokens, as well as various supporting functions and a 'shiny' module. Features include cross-site request forgery (CSRF) protection, state encryption, 'Proof Key for Code Exchange' (PKCE) handling, validation of OIDC identity tokens (nonces, signatures, claims), automatic user info retrieval for OIDC and supported 'OAuth' providers, asynchronous flows, and hooks for audit logging. |
| Authors: | Luka Koning [aut, cre, cph] |
| Maintainer: | Luka Koning <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.6.1.9000 |
| Built: | 2026-10-20 22:38:26 UTC |
| Source: | https://github.com/lukakoning/shinyoauth |
This function inspects a configured OAuthClient and its OAuthProvider for
compliance with the OAuth 2.1 draft 16 (ruleset 1.1.0) specification. It
reports configuration gaps, unresolved external prerequisites, and recommendations
without changing the configuration or making requests.
check_oauth21(config, context = list(), draft = "draft-ietf-oauth-v2-1-16")check_oauth21(config, context = list(), draft = "draft-ietf-oauth-v2-1-16")
config |
An OAuthClient or OAuthProvider. Provider-only assessments are partial and cannot establish missing client settings. |
context |
Optional named list with |
draft |
Implemented target revision. Currently only
|
Ruleset 1.1.0 covers code/refresh, enabled PAR, required UserInfo and
introspection, and the additional operations selected in context. Signing
and encryption key retrieval is included when applicable. Future resource URLs, arbitrary
request customization, browser/proxy TLS, registered redirect matching, secret
custody, and authorization/resource server behavior require separate evidence.
Optional DPoP, mTLS, PAR, JAR and JARM are not required as a bundle.
configuration_compliant is FALSE if an applicable mandatory configuration
check fails; otherwise NA if a mandatory configuration check is unresolved;
otherwise TRUE when a nonempty set of applicable mandatory checks passes.
Recommendations and external unknowns do not change that verdict. In
particular, the legacy assertion type JWT is a recommendation finding;
the assertion audience is a separate mandatory check for JWT authentication.
requirement_source distinguishes OAuth core, OIDC, extension specifications,
security guidance and local package/application policy. Callback capacity
thresholds are package recommendations, not draft-defined numeric minima;
complete encoded requests still need deployment testing. OAuth 2.1 assessment
is opt-in and does not change existing OAuth 2.0 configuration or requests.
A positive verdict applies only to the recorded scope and ruleset, with the current configuration, options, runtime and declared context. It is not certification or a test of a live deployment. Rerun after policy changes. Reports contain no client/provider objects, credentials, keys or endpoint URLs. No caches or state stores are read or changed; their method contracts are inspected without invoking them.
A shinyOAuth_oauth21_assessment list with configuration_compliant,
checks, draft, ruleset_version, package_version, assessed_at,
assessment_scope, and operations. checks is a data frame with stable
id, scope, status (pass, fail, warn, unknown, not_applicable),
requirement (MUST, SHOULD, info), message, remediation, reference,
evidence_source, requirement_source, and logical affects_verdict columns. Only rows with
affects_verdict = TRUE enter aggregation; unknown external obligations
remain visible separately. warn records a compatibility warning, such as
accepting a missing token type, and does not by itself fail the verdict.
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-16
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-11
https://www.rfc-editor.org/rfc/rfc9207.html
provider <- oauth_provider( name = "Example", auth_url = "https://auth.example/authorize", token_url = "https://auth.example/token", token_auth_style = "public", use_pkce = TRUE, pkce_method = "S256" ) client <- oauth_client( provider, client_id = "example-client", redirect_uri = "https://app.example/callback" ) assessment <- check_oauth21(client) assessment[["checks"]][assessment[["checks"]][["status"]] != "pass", ]provider <- oauth_provider( name = "Example", auth_url = "https://auth.example/authorize", token_url = "https://auth.example/token", token_auth_style = "public", use_pkce = TRUE, pkce_method = "S256" ) client <- oauth_client( provider, client_id = "example-client", redirect_uri = "https://app.example/callback" ) assessment <- check_oauth21(client) assessment[["checks"]][assessment[["checks"]][["status"]] != "pass", ]
resource_req()
Deprecated alias for resource_req().
Use resource_req() for Bearer, DPoP, and mTLS-protected resource requests instead.
client_bearer_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, resource_hosts = NULL, oauth_client = NULL )client_bearer_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, resource_hosts = NULL, oauth_client = NULL )
token |
Either an OAuthToken object or a raw access token string. |
url |
The absolute URL to call. |
method |
Optional HTTP method (character). Defaults to "GET". When
the effective token type is |
headers |
Optional named list or named character vector of extra
headers to set on the request. Header names are case-insensitive.
Names must be unique HTTP field-name tokens, and values must not contain
prohibited control characters. DPoP requests reject |
query |
Optional named list of query parameters to append to the URL. |
follow_redirect |
Logical or |
check_url |
Logical. If |
client |
Optional OAuthClient. Required when the effective
token type is |
token_type |
Optional override for the access token type when |
dpop_nonce |
Optional DPoP nonce to embed in the proof for this
request. This is primarily useful after a resource server challenges with
|
resource_hosts |
Optional non-empty character vector of trusted resource
host patterns, using |
oauth_client |
Compatibility alias for |
Same value as resource_req().
Connect shinyOAuth to a shared storage backend, such as Redis or a database,
by wrapping your R functions in a cachem-like interface. Use the result as
state_store in oauth_client() to share pending login state, or as
jwks_cache in oauth_provider() to share provider signing keys (JWKS).
A shared state store is needed when a login can start on one R process and its callback can arrive at another, for example in a multi-worker deployment without sticky routing. A shared signing-key cache lets workers reuse keys fetched from the provider rather than maintaining separate caches.
custom_cache( get, set, remove, take = NULL, info = NULL, set_if_absent = NULL, worker_safe = FALSE )custom_cache( get, set, remove, take = NULL, info = NULL, set_if_absent = NULL, worker_safe = FALSE )
get |
A function(key, missing = NULL) -> value. Required.
Should return the stored value, or the |
set |
A function(key, value) -> invisible(NULL). Required. Should store the value under the given key. |
remove |
A function(key) -> any. Required. Deletes the entry for |
take |
A function(key, missing = NULL) -> value. Optional. An atomic get-and-delete operation. When provided, shinyOAuth uses
Should return the stored value and atomically remove the entry, or
return the If your backend supports atomic get-and-delete natively
(e.g., Redis When |
info |
Function() -> list(max_age = seconds, ...). Optional TTL information from |
set_if_absent |
A function(key, value, ttl = NULL) -> logical. Optional. An atomic set-if-missing operation for shared JWKS caches and callback
bridge slots. It must store
|
worker_safe |
Logical, default |
This helper adapts your storage functions; it does not create a database, open connections, or make process-local storage shared. Your backend must preserve stored R values and expire entries after their configured lifetime.
An R6 object exposing cachem-like [["get"]]/[["set"]]/[["remove"]]/[["info"]] methods
and the optional [["take"]] and [["set_if_absent"]] atomic methods.
The default cachem::cache_mem() state store belongs to one R process.
If a load balancer sends the returning callback to another worker, that
worker cannot find the pending login and validation fails. Configure all
workers with access to the same external store when routing does not keep
the authorization request and callback on the same process.
For a shared state_store, implement take: it must read and delete a pending
login as one indivisible operation, so two requests cannot use it. Redis
GETDEL and SQL DELETE ... RETURNING are examples of backend operations
that can do this. Use the same state_key and matching provider/client
settings on every worker. Separate reads and deletes, including those in
cachem::cache_disk(), cannot ensure single-use state under concurrent access.
See the deployment guidance.
For the form_post and form_post.jwt callback bridges, also implement
set_if_absent to reserve callback slots without replacing pending responses.
Store values are small R lists; preserve them without interpreting fields.
Pending login records in external stores are AES-GCM sealed with a distinct
key derived from state_key and bound to the client, provider, and state key.
The backend receives an opaque sealed_state_record string instead of the
browser binding, PKCE verifier, and nonce. Existing unsealed external records
are rejected; users with logins pending across an upgrade must restart login.
The default process-local memory store keeps records within the R trust boundary.
Encryption does not replace backend access controls, expiry, or atomic take:
a backend able to restore consumed entries can still violate single-use state.
With request_object_mode = "request_uri", hosted Request Objects are stored
as separate records containing the signed or encrypted JWT and its expiry.
These records do not use the pending-login sealed_state_record wrapper.
A signed, unencrypted JWT has readable claims, including authorization request
details. Configure Request Object JWE encryption when those claims need
confidentiality, and apply backend access controls and expiry to these records.
For a state store, returning max_age in seconds from info() also lets
oauth_module_server() align the browser cookie lifetime with the store.
Reporting this value does not expire entries; your backend must enforce it.
A shared jwks_cache can reduce repeated key downloads when several R
workers use the same provider. This is independent of sharing login state:
sharing signing keys alone does not let another worker resume a login.
Key caching uses get and set. Also implement set_if_absent to coordinate
rate-limited forced key refreshes across workers, for example when the provider
rotates its signing keys. Without that atomic operation, forced refresh is
disabled for custom/shared caches. Use separate stores or key namespaces for
login state and signing keys when they require different expiry policies.
For asynchronous login and token operations, a custom signing-key cache
defaults to synchronous fallback. Set worker_safe = TRUE only if its
functions and captured values
remain usable after serialization into a new R process. Open database/file
connections and other process-bound resources must not be captured; open and
close connections inside each operation instead. Serialization success alone
does not establish worker safety. Standard cachem::cache_mem() and
cachem::cache_disk() backends support worker dispatch without this opt-in.
Other cache implementations can expose a scalar logical worker_safe member
with the same contract. State stores are not used in callback workers.
# This in-memory example illustrates the cache interface in one R process. # It does not share entries across workers or implement timed expiry. # A production shared store must implement both itself. mem <- new.env(parent = emptyenv()) my_cache <- custom_cache( get = function(key, missing = NULL) { base::get0(key, envir = mem, ifnotfound = missing, inherits = FALSE) }, set = function(key, value) { assign(key, value, envir = mem) invisible(NULL) }, remove = function(key) { if (exists(key, envir = mem, inherits = FALSE)) { rm(list = key, envir = mem) } invisible(NULL) }, # In a shared store, replace this with the backend's atomic get-and-delete # operation, such as Redis GETDEL. This R environment is process-local. take = function(key, missing = NULL) { val <- base::get0(key, envir = mem, ifnotfound = missing, inherits = FALSE) if (exists(key, envir = mem, inherits = FALSE)) { rm(list = key, envir = mem) } val }, info = function() list(max_age = Inf) )# This in-memory example illustrates the cache interface in one R process. # It does not share entries across workers or implement timed expiry. # A production shared store must implement both itself. mem <- new.env(parent = emptyenv()) my_cache <- custom_cache( get = function(key, missing = NULL) { base::get0(key, envir = mem, ifnotfound = missing, inherits = FALSE) }, set = function(key, value) { assign(key, value, envir = mem) invisible(NULL) }, remove = function(key) { if (exists(key, envir = mem, inherits = FALSE)) { rm(list = key, envir = mem) } invisible(NULL) }, # In a shared store, replace this with the backend's atomic get-and-delete # operation, such as Redis GETDEL. This R environment is process-local. take = function(key, missing = NULL) { val <- base::get0(key, envir = mem, ifnotfound = missing, inherits = FALSE) if (exists(key, envir = mem, inherits = FALSE)) { rm(list = key, envir = mem) } val }, info = function() list(max_age = Inf) )
Retrieve profile information using the user's access token. Call this when
fetching a profile on demand, reloading profile fields, or managing tokens
outside the Shiny module. It returns the provider's profile as an R list.
The Shiny module fetches and stores this information during login when
userinfo_required = TRUE; that result is available as auth[["token"]]@userinfo.
get_userinfo( client, token, token_type = NULL, shiny_session = NULL, oauth_client = NULL )get_userinfo( client, token, token_type = NULL, shiny_session = NULL, oauth_client = NULL )
client |
OAuthClient object. The client must have a
|
token |
Either an OAuthToken object or a raw access token string. |
token_type |
Optional override for the access token type when |
shiny_session |
Optional captured Shiny session details for audit events.
Normally supplied by the module; leave |
oauth_client |
Compatibility alias for |
The provider must have a userinfo_url: the OpenID Connect (OIDC) UserInfo
endpoint, or an OAuth provider's profile API. With OIDC, this function checks that
userinfo belongs to the same user as a validated ID token. Pass the complete
OAuthToken, which carries the validated ID token needed for this check.
OIDC providers with a UserInfo endpoint require this baseline by default,
including when userinfo_required = FALSE disables automatic retrieval.
Raw access tokens and unvalidated ID tokens then cause an error. An explicit
userinfo_id_token_match = FALSE permits profile retrieval without a validated
identity; when one is available, its subject is always compared.
Ordinary JSON profiles and signed JWT UserInfo responses are supported.
Signed responses are verified using the provider's signing keys and
userinfo_allowed_algs; encrypted UserInfo is not supported. Set
userinfo_signed_jwt_required on the provider to require a signed response,
including when fetching manually with userinfo_required = FALSE,
and userinfo_jwt_required_time_claims on the client to require time claims
such as exp. Present time claims are checked even when not required.
Standard OIDC profile claims in ID tokens and UserInfo are checked against
their JSON types, including strings for names and email, a number for
updated_at, and an address object with string-valued standard members.
Applications should validate formats and policies for the claim values they
use, and supply schemas for custom extension claims.
For certificate-bound tokens (mTLS) and key-bound tokens (DPoP), the helper uses the client's certificate or signing key and checks the token binding. It handles one DPoP nonce challenge with a fresh-proof retry. See the advanced security vignette for configuration.
A list containing the user information returned by the provider. For JSON responses, arrays are simplified to vectors or data frames where possible. Signed JWT responses retain arrays as lists.
# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }
Check a returning login request and exchange the provider's temporary
authorization code for an OAuthToken (OAuth 2.0 Authorization Code flow).
Use this in a custom callback handler after starting authorization with
prepare_call(). It applies shinyOAuth's state, token, and configured
identity checks while your application manages the HTTP callback and stores
the returned token. oauth_module_server() handles these responsibilities
for Shiny sessions.
handle_callback( client, code, state, browser_token, shiny_session = NULL, iss = NULL, oauth_client = NULL, payload = NULL )handle_callback( client, code, state, browser_token, shiny_session = NULL, iss = NULL, oauth_client = NULL, payload = NULL )
client |
An OAuthClient object. |
code |
Authorization code received from the provider on a classic direct callback. |
state |
Encrypted state payload returned by the provider on a classic
direct callback. This should be the same value that was originally sent in
|
browser_token |
Browser token present in the user's session. This is
usually managed by |
shiny_session |
Optional captured Shiny session details for audit events.
Normally supplied by the module; leave |
iss |
Optional RFC 9207 callback issuer ( This low-level API cannot verify which redirect URI received the response.
Clients configured with |
oauth_client |
Compatibility alias for |
payload |
Compatibility alias for |
Pass the returned code, the callback's state, and the
browser token saved for this login. This helper accepts direct code/state
callbacks only. For signed responses using JWT Secured Authorization
Response Mode (JARM; "jwt", "query.jwt", or
"form_post.jwt"), use oauth_module_server() and, for POST responses,
oauth_form_post_ui(). There is no public JARM resume API.
An OAuthToken object. If callback validation, token exchange, or token verification fails, the function raises an error.
# Advanced example: your code supplies browser redirects and callback handling. # For a Shiny app, oauth_module_server() manages these steps for you. if (interactive()) { # Define client client <- oauth_client( provider = oauth_provider_github(), client_id = Sys.getenv("GITHUB_OAUTH_CLIENT_ID"), client_secret = Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100" ) # Get the login URL and store state in client's state store # `<browser_token>` must be unpredictable and persisted for this transaction # in storage bound to the application's exact origin (scheme, host, port). # The module combines origin-scoped storage with an independent marker cookie # and checks both on return. A cookie alone does not provide this boundary: # cookies can be shared by applications on different ports of the same host. # Shiny applications should use oauth_module_server() for the complete flow. authorization_url <- prepare_call(client, "<browser_token>") # Redirect user to authorization URL; retrieve code & state from the query; # recover this transaction's `<browser_token>` through the origin-bound flow # and verify its independent marker before calling handle_callback(). code <- "..." state <- "..." browser_token <- "..." # Handle callback, exchanging code for token and validating state token <- handle_callback(client, code, state, browser_token) }# Advanced example: your code supplies browser redirects and callback handling. # For a Shiny app, oauth_module_server() manages these steps for you. if (interactive()) { # Define client client <- oauth_client( provider = oauth_provider_github(), client_id = Sys.getenv("GITHUB_OAUTH_CLIENT_ID"), client_secret = Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100" ) # Get the login URL and store state in client's state store # `<browser_token>` must be unpredictable and persisted for this transaction # in storage bound to the application's exact origin (scheme, host, port). # The module combines origin-scoped storage with an independent marker cookie # and checks both on return. A cookie alone does not provide this boundary: # cookies can be shared by applications on different ports of the same host. # Shiny applications should use oauth_module_server() for the complete flow. authorization_url <- prepare_call(client, "<browser_token>") # Redirect user to authorization URL; retrieve code & state from the query; # recover this transaction's `<browser_token>` through the origin-bound flow # and verify its independent marker before calling handle_callback(). code <- "..." state <- "..." browser_token <- "..." # Handle callback, exchanging code for token and validating state token <- handle_callback(client, code, state, browser_token) }
Ask the provider to check an access or refresh token. This is called token
introspection and requires a configured introspection_url. Use it when you
need the provider's current token status rather than only a locally recorded
expiry time: a token may have been revoked before its expiry. To require it
automatically during login and refresh, set introspect = TRUE on
oauth_client().
introspect_token( client, token, token_kind = c("access", "refresh"), async = FALSE, shiny_session = NULL, oauth_client = NULL, oauth_token = NULL, which = NULL )introspect_token( client, token, token_kind = c("access", "refresh"), async = FALSE, shiny_session = NULL, oauth_client = NULL, oauth_token = NULL, which = NULL )
client |
OAuthClient object |
token |
OAuthToken object to introspect |
token_kind |
Which token to introspect: "access" (default) or "refresh". |
async |
If |
shiny_session |
Optional captured Shiny session details for audit events.
Normally supplied by the module; leave |
oauth_client |
Compatibility alias for |
oauth_token |
Compatibility alias for |
which |
Compatibility alias for |
Read result[["active"]]: TRUE means active, FALSE means inactive, and NA
means the result is unknown. Use isTRUE(result[["active"]]) if your code must
require a definite confirmation.
Unsupported endpoints, missing tokens, unsuccessful HTTP responses, and
unusable response bodies return a descriptive status. The provider must
return active as a JSON boolean. Other types return "invalid_active".
Encoded or decoded body limits return "body_too_large"; unsupported
compression returns "unsupported_encoding". Both leave active = NA.
Other transport failures and decoding errors raise conditions (or reject
the asynchronous promise) instead of returning a status result.
Requests use the client's configured credentials and token_auth_style.
A list with fields:
supported: logical, TRUE when an introspection endpoint is configured.
active: logical or NA, where NA means the provider did not return a
usable RFC 7662 active value.
raw: parsed introspection response list, or NULL when the endpoint is
unsupported or the response could not be parsed.
status: machine-readable status such as "ok",
"introspection_unsupported", "missing_token", "invalid_json",
"missing_active", "invalid_active", or "http_<code>".
# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }
Test whether a URL is allowed by shinyOAuth's ordinary scheme and host rules.
HTTPS is accepted by default. HTTP is limited to local development hosts
unless you change allowed_non_https_hosts. Supply allowed_hosts to
restrict which services your app may contact.
Call this when checking configured endpoint URLs or diagnosing a URL-policy rejection. The provider and API request helpers apply these checks internally; a direct call lets you inspect the result without making a network request.
is_ok_host( url, allowed_non_https_hosts = getOption("shinyOAuth.allowed_non_https_hosts", default = c("localhost", "127.0.0.1", "::1", "[::1]")), allowed_hosts = getOption("shinyOAuth.allowed_hosts", default = NULL) )is_ok_host( url, allowed_non_https_hosts = getOption("shinyOAuth.allowed_non_https_hosts", default = c("localhost", "127.0.0.1", "::1", "[::1]")), allowed_hosts = getOption("shinyOAuth.allowed_hosts", default = NULL) )
url |
Single URL or vector of URLs (character; length 1 or more) |
allowed_non_https_hosts |
Character vector of hostnames that are allowed to use HTTP instead of HTTPS. Defaults to localhost equivalents. Supports globs |
allowed_hosts |
Optional allowlist of hosts/domains; if supplied (length > 0), only these hosts are permitted. Supports globs |
Both host lists support * (any characters), ? (one character), and a
leading dot: ".example.com" matches the domain and its subdomains.
"*" permits every host. If allowed_hosts is empty, only the scheme
rules apply. Missing values, empty strings, and malformed URLs return FALSE.
If the scheme is absent, this helper tries HTTP, then HTTPS. Request helpers
can impose additional requirements, including an absolute URL. OIDC
discovery has a separate HTTPS policy and requires an explicit loopback
development opt-in; a TRUE result here does not override it.
Defaults come from shinyOAuth.allowed_hosts and
shinyOAuth.allowed_non_https_hosts.
Logical indicator (TRUE if all URLs pass all checks; FALSE otherwise)
# HTTPS allowed by default is_ok_host("https://example.com") # HTTP allowed for localhost is_ok_host("http://localhost:8100") # Restrict to a specific domain (allowlist) is_ok_host("https://api.example.com", allowed_hosts = c(".example.com")) # Caution: a catch-all pattern disables host restrictions # (only scheme rules remain). Avoid unless you truly intend it is_ok_host("https://anywhere.example", allowed_hosts = c("*"))# HTTPS allowed by default is_ok_host("https://example.com") # HTTP allowed for localhost is_ok_host("http://localhost:8100") # Restrict to a specific domain (allowlist) is_ok_host("https://api.example.com", allowed_hosts = c(".example.com")) # Caution: a catch-all pattern disables host restrictions # (only scheme rules remain). Avoid unless you truly intend it is_ok_host("https://anywhere.example", allowed_hosts = c("*"))
Choose who may use saved OAuth connections: the browser that created them,
or an account already authenticated by your application. Supply the policy to
oauth_connections() with the matching retention mode. These factories do not
set cookies, authenticate users or enable retention on oauth_module_server()
calls by themselves.
oauth_browser_owner( idle_timeout = 1800, absolute_timeout = 28800, same_site = c("Lax", "Strict"), allow_http_loopback = FALSE, max_entries = 1000L ) oauth_account_owner( resolver, idle_timeout, absolute_timeout, reauth_after_seconds, max_entries = 1000L ) ## S3 method for class 'OAuthOwnerPolicy' print(x, ...)oauth_browser_owner( idle_timeout = 1800, absolute_timeout = 28800, same_site = c("Lax", "Strict"), allow_http_loopback = FALSE, max_entries = 1000L ) oauth_account_owner( resolver, idle_timeout, absolute_timeout, reauth_after_seconds, max_entries = 1000L ) ## S3 method for class 'OAuthOwnerPolicy' print(x, ...)
idle_timeout |
Maximum owner inactivity in seconds. Resource and status
reads do not count as activity. Use the server manager's |
absolute_timeout |
Maximum owner lifetime in seconds, independent of
activity. Must be at least |
same_site |
Owner-cookie policy, |
allow_http_loopback |
Explicit development-only exception for HTTP on
localhost or a loopback address. Default |
max_entries |
Maximum owner-registry entries per manager, a positive
whole number. Browser mode counts browsers that authorize a service. A
separate provisional visitor pool has the same limit; its oldest entries
may be replaced and expire after at most five minutes, unless a live
authorization transaction protects them until its expiry. When all visitor
entries have pending authorizations, new visitors are rejected. Account mode counts
authentication generations, including retired generations until their local
reauthentication deadline. This limit is independent of the connection
store's |
resolver |
Trusted application function accepting the current Shiny
session. On every call it must validate the application's local login and
return |
reauth_after_seconds |
Maximum age of the verified local authentication, in seconds. Required for account retention; refresh cannot reset this age. |
x |
An |
... |
Unused print arguments. |
Browser retention identifies an authorized browser session, not a verified
person. The owner cookie is HttpOnly, host-only, has root path and uses Secure
and a __Host- name on HTTPS. It contains no OAuth token or patient data. Server-side
idle and absolute limits are authoritative. A cookie is never accepted as an
owner without a matching live server record for this application origin.
Cookies are shared across ports on the same host, even with HttpOnly, Secure
and a __Host- name (RFC 6265 section 8.5).
All HTTPS services on that host that receive the owner cookie must be trusted,
because they can read and replay it. The origin hash in the cookie name
distinguishes applications but does not isolate the cookie from other ports.
Use separate hostnames for mutually untrusted services.
Cookie rotation invalidates the previous session generation immediately and preserves the original absolute lifetime. Local logout removes the live owner session. The manager checks that generation before code exchange and credential commit, with no grace period for pending authorization, and handles credential cleanup after logout. An external provider login cannot establish a local owner or implicitly link browser connections to an account.
A full owner registry rejects new retained owners without evicting live
sessions or retirement records. Browser session end does not release ownership; logout or
idle/absolute expiry does. Account logout retains its retired generation until
authenticated_at + reauth_after_seconds so it cannot be enrolled again.
Size max_entries for that entire window, not only simultaneous Shiny sessions.
Account retention requires finite idle, absolute and local reauthentication
lifetimes. The internal session registry re-runs resolver when resolving or
validating an owner and checks the intended subject/session generation.
Expiry or logout retires that generation until a fresh local authentication
session is supplied. A resolver error fails closed with a redacted error.
The application remains responsible for validating its local session, including
signature, expiry, revocation and account changes. Supplying this configuration
is not proof that an arbitrary user ID is authenticated.
An OAuthOwnerPolicy configuration object. Browser policies use an
opaque server-issued cookie and server-side owner registry. Account policies
use the trusted local-session resolver described below.
oauth_connection_store_memory()
# Browser connections expire after 15 minutes idle or 8 hours in total. oauth_browser_owner(idle_timeout = 15 * 60, absolute_timeout = 8 * 3600) # Supply your application's trusted local-login validator for account retention. # It must revalidate the session on each call and return the documented fields. account_policy <- function(validate_local_session) { oauth_account_owner( resolver = validate_local_session, idle_timeout = 15 * 60, absolute_timeout = 8 * 3600, reauth_after_seconds = 8 * 3600 ) }# Browser connections expire after 15 minutes idle or 8 hours in total. oauth_browser_owner(idle_timeout = 15 * 60, absolute_timeout = 8 * 3600) # Supply your application's trusted local-login validator for account retention. # It must revalidate the session on each call and return the documented fields. account_policy <- function(validate_local_session) { oauth_account_owner( resolver = validate_local_session, idle_timeout = 15 * 60, absolute_timeout = 8 * 3600, reauth_after_seconds = 8 * 3600 ) }
Create a client with the credentials assigned by your provider, the URL where
users return after login, and the permissions your app needs. Pass the result
to oauth_module_server().
oauth_client( provider, client_id, client_secret = character(0), redirect_uri, enforce_callback_issuer = NULL, scopes = character(0), resource = character(0), claims = NULL, state_store = cachem::cache_mem(max_age = 300), state_payload_max_age = 300, state_entropy = 64, state_key = random_urlsafe(128), client_assertion_private_key = NULL, client_assertion_private_key_kid = NULL, client_assertion_alg = NULL, client_assertion_audience = NULL, mtls_client_cert_file = NULL, mtls_client_key_file = NULL, mtls_client_key_password = NULL, mtls_client_ca_file = NULL, mtls_certificate_bound_access_tokens = FALSE, request_object_mode = c("parameters", "request", "request_uri"), response_mode = NULL, request_object_signing_alg = NULL, request_object_audience = NULL, request_object_encryption_alg = NULL, request_object_encryption_enc = NULL, request_object_encryption_kid = NULL, request_object_ttl = 45, request_object_nbf_skew = NULL, dpop_private_key = NULL, dpop_private_key_kid = NULL, dpop_signing_alg = NULL, dpop_require_access_token = NULL, scope_validation = c("warn", "strict", "none"), claims_validation = c("none", "warn", "strict"), userinfo_jwt_required_time_claims = character(0), required_acr_values = character(0), introspect = FALSE, introspection_checks = character(0), authorization_server_mode = c("single", "multi_issuer", "multi_redirect_uri"), authorization_server_redirect_uris = character(0), dpop_require_observed_cnf = FALSE, jarm_signed_response_alg = NULL, jarm_encrypted_response_alg = NULL, jarm_encrypted_response_enc = NULL, jarm_decryption_private_key = NULL, jarm_decryption_private_key_kid = NULL, jarm_max_lifetime = 600, endpoint_auth = list(), mtls_require_observed_cnf = TRUE, trusted_id_token_audiences = character(0), compare_callback_issuer = NULL, client_assertion_typ = "JWT", authorization_method = "GET", resource_bases = character(), required_scopes = character(), label = default_client_label(provider), ..., introspect_elements = NULL, token_targets = list(), default_token_target = if (length(token_targets) == 1L) { names(token_targets) } else { character() } )oauth_client( provider, client_id, client_secret = character(0), redirect_uri, enforce_callback_issuer = NULL, scopes = character(0), resource = character(0), claims = NULL, state_store = cachem::cache_mem(max_age = 300), state_payload_max_age = 300, state_entropy = 64, state_key = random_urlsafe(128), client_assertion_private_key = NULL, client_assertion_private_key_kid = NULL, client_assertion_alg = NULL, client_assertion_audience = NULL, mtls_client_cert_file = NULL, mtls_client_key_file = NULL, mtls_client_key_password = NULL, mtls_client_ca_file = NULL, mtls_certificate_bound_access_tokens = FALSE, request_object_mode = c("parameters", "request", "request_uri"), response_mode = NULL, request_object_signing_alg = NULL, request_object_audience = NULL, request_object_encryption_alg = NULL, request_object_encryption_enc = NULL, request_object_encryption_kid = NULL, request_object_ttl = 45, request_object_nbf_skew = NULL, dpop_private_key = NULL, dpop_private_key_kid = NULL, dpop_signing_alg = NULL, dpop_require_access_token = NULL, scope_validation = c("warn", "strict", "none"), claims_validation = c("none", "warn", "strict"), userinfo_jwt_required_time_claims = character(0), required_acr_values = character(0), introspect = FALSE, introspection_checks = character(0), authorization_server_mode = c("single", "multi_issuer", "multi_redirect_uri"), authorization_server_redirect_uris = character(0), dpop_require_observed_cnf = FALSE, jarm_signed_response_alg = NULL, jarm_encrypted_response_alg = NULL, jarm_encrypted_response_enc = NULL, jarm_decryption_private_key = NULL, jarm_decryption_private_key_kid = NULL, jarm_max_lifetime = 600, endpoint_auth = list(), mtls_require_observed_cnf = TRUE, trusted_id_token_audiences = character(0), compare_callback_issuer = NULL, client_assertion_typ = "JWT", authorization_method = "GET", resource_bases = character(), required_scopes = character(), label = default_client_label(provider), ..., introspect_elements = NULL, token_targets = list(), default_token_target = if (length(token_targets) == 1L) { names(token_targets) } else { character() } )
provider |
The service configuration, created with a provider helper such as
|
client_id |
The identifier assigned when you register your app with the provider. |
client_secret |
The secret issued for your app, preferably read with
It is required for |
redirect_uri |
The URL where users return after login. It must match the callback URL registered with your provider, including scheme, host, port, and path. Use HTTPS in production. |
enforce_callback_issuer |
Logical or When |
scopes |
Character vector of permissions to request. The provider defines
the available names. For OIDC ( |
resource |
Optional RFC 8707 resource indicator(s). Supply a character
vector of absolute URIs to request audience-restricted tokens for one or
more protected resources. Each value is sent as a repeated |
claims |
Optional request for specific OIDC user information, beyond scopes.
Default Lists are JSON-encoded with |
state_store |
Storage for pending logins. The default
|
state_payload_max_age |
Maximum age of a pending login's encrypted state, in seconds. Default 300. This is checked separately from the state store's entry lifetime; both must allow the returning login. |
state_entropy |
Length in characters of the random state identifier, from 22 to 128. Default 64. Most apps should keep the default. |
state_key |
Secret used to encrypt and protect pending login details.
A random key is generated when omitted. This is separate from
For multiple R processes, supply the same key and shared |
client_assertion_private_key |
Optional private key for |
client_assertion_private_key_kid |
Optional key identifier (kid) to include in the JWT header
for |
client_assertion_alg |
Optional JWT signing algorithm to use for client assertions.
When omitted, defaults to |
client_assertion_audience |
Optional override for the |
mtls_client_cert_file |
Optional path to the PEM-encoded client
certificate (or certificate chain) used for RFC 8705 mutual TLS (mTLS) client
authentication and certificate-bound protected-resource requests. Required
when |
mtls_client_key_file |
Optional path to the PEM-encoded private key used
with |
mtls_client_key_password |
Optional password used to decrypt an encrypted
PEM private key referenced by |
mtls_client_ca_file |
Optional path to a PEM CA bundle used to validate the remote HTTPS server certificate when making mTLS requests. This is mainly useful for local or test environments that use self-signed server certificates. |
mtls_certificate_bound_access_tokens |
Logical. Whether this
client intends to request RFC 8705 certificate-bound access tokens when
the provider advertises that capability. Default is Set this to Requires |
request_object_mode |
Controls how the authorization request is transported to the provider.
If the provider has a Use a signed Request Object when the provider requires JAR or when it must
verify the integrity of the authorization parameters. |
response_mode |
How the provider returns the login result. Leave Signed responses (JWT Secured Authorization Response Mode, JARM)
use |
request_object_signing_alg |
Optional JWS algorithm override for
signed authorization requests when |
request_object_audience |
Optional override for the |
request_object_encryption_alg |
Optional JWE key-management
algorithm override for encrypted Request Objects. Current outbound support
is limited to |
request_object_encryption_enc |
Optional JWE content-encryption
algorithm override for encrypted Request Objects. Current outbound support
is limited to the AES-CBC-HMAC family ( |
request_object_encryption_kid |
Optional key identifier ( |
request_object_ttl |
Positive number of seconds to keep signed
authorization request objects ( |
request_object_nbf_skew |
Optional non-negative number of
seconds. When provided, shinyOAuth adds an |
dpop_private_key |
Private key for tying tokens to this app's requests
using Demonstrating Proof of Possession (DPoP). Only needed when your
provider/API supports DPoP. Accepts an
|
dpop_private_key_kid |
Optional key identifier ( |
dpop_signing_alg |
Optional JWT signing algorithm to use for DPoP
proofs. When omitted, a compatible asymmetric default is selected based on
the private key type/curve (for example |
dpop_require_access_token |
Logical or |
scope_validation |
Controls how scope discrepancies are handled when
the authorization server grants fewer scopes than requested. RFC 6749
Section 3.3 permits servers to issue tokens with reduced scope, and
Section 5.1 allows token responses to omit
|
claims_validation |
What to do if requested claims are missing or have
unexpected values: |
userinfo_jwt_required_time_claims |
Optional character vector of
temporal JWT claims that must be present when the UserInfo response is a
signed JWT ( Default is |
required_acr_values |
Optional character vector of acceptable login
requirements, such as a provider's multi-factor authentication (MFA) policy.
Use the provider's Authentication Context Class Reference (ACR) identifiers.
The validated ID token must contain a matching |
introspect |
If |
introspection_checks |
Optional character vector of additional
requirements to enforce on the introspection response when
|
authorization_server_mode |
Declares whether this client is part of an application that can interact with more than one authorization server, and which RFC 9700 mix-up defense it uses. One of:
|
authorization_server_redirect_uris |
Complete character vector of
redirect URIs used by the application for its authorization servers when
|
dpop_require_observed_cnf |
Logical. When |
jarm_signed_response_alg |
Optional expected JWS algorithm for
signed JWT Secured Authorization Responses (JARM). When omitted and the
effective response mode is JARM, shinyOAuth defaults to |
jarm_encrypted_response_alg |
Optional expected JWE
key-management algorithm for encrypted JARM responses. Current inbound
support is limited to |
jarm_encrypted_response_enc |
Optional expected JWE
content-encryption algorithm for encrypted JARM responses. Current inbound
support is limited to the AES-CBC-HMAC family ( |
jarm_decryption_private_key |
Optional private key
used to decrypt encrypted JARM responses. Can be an |
jarm_decryption_private_key_kid |
Optional key
identifier ( |
jarm_max_lifetime |
Positive number of seconds. Maximum accepted
lifetime for a JARM response JWT. Default is 600 seconds, matching JARM's
recommended 10-minute upper bound for authorization response JWTs. When a
JARM payload includes |
endpoint_auth |
Named list of authentication overrides for |
mtls_require_observed_cnf |
Logical, default |
trusted_id_token_audiences |
Character vector of additional ID-token
audiences explicitly trusted by this client. Defaults to |
compare_callback_issuer |
Logical or |
client_assertion_typ |
JWT header |
authorization_method |
Browser method for sending the authorization
request: |
resource_bases |
Optional named character vector of approved API base
URLs for |
required_scopes |
Optional requested scopes that every usable connection
needs, default |
label |
Optional display label used in connection summaries; defaults to
the provider name, with control characters replaced by spaces and shortened
to 128 UTF-8 bytes if needed. If the provider name is empty, missing or not a
single string, the default is |
... |
Deprecated renamed arguments accepted temporarily for backward compatibility. |
introspect_elements |
Compatibility alias for |
token_targets |
Optional named list of API token declarations. Each
entry has |
default_token_target |
Name of the initial code-redemption target and
the default for connection methods. Required with multiple targets;
|
Create the client outside server() so its settings and pending login state
remain available when the callback returns. Configure provider, client_id,
client_secret (if issued), redirect_uri, and scopes from the app
registration. Use state_store and state_key for shared login state across
workers, and validation arguments to require particular scopes, claims, or
authentication context. See the usage vignette for a complete app, or
the advanced security vignette for certificate and signed-request settings.
OAuthClient object
# Register an app with GitHub and store its credentials in your environment. # This creates the configuration; it does not start login or contact GitHub. client <- oauth_client( provider = oauth_provider_github(), client_id = "your-client-id", client_secret = "your-client-secret", redirect_uri = "http://127.0.0.1:8100", scopes = c("read:user", "user:email") ) # In a real app, read credentials with Sys.getenv() and create client # outside server(). Inside server(), start login with: # auth <- oauth_module_server("auth", client)# Register an app with GitHub and store its credentials in your environment. # This creates the configuration; it does not start login or contact GitHub. client <- oauth_client( provider = oauth_provider_github(), client_id = "your-client-id", client_secret = "your-client-secret", redirect_uri = "http://127.0.0.1:8100", scopes = c("read:user", "user:email") ) # In a real app, read credentials with Sys.getenv() and create client # outside server(). Inside server(), start login with: # auth <- oauth_module_server("auth", client)
Build a list of settings to register a certificate-based client with your
provider using mutual TLS (mTLS). Use this when preparing metadata for dynamic
client registration or when your provider asks for certificate identifiers,
public keys, or certificate-bound token settings. It derives those settings
from an oauth_client() already configured for mTLS.
The result is a metadata list, ready to include in a registration request. Submit it through your provider's registration process; this function does not register the client or upload the certificate.
oauth_client_mtls_registration( client, tls_client_auth_type = c("subject_dn", "san_dns", "san_uri", "san_ip", "san_email"), tls_client_auth_value = NULL, jwks_uri = NULL, oauth_client = NULL )oauth_client_mtls_registration( client, tls_client_auth_type = c("subject_dn", "san_dns", "san_uri", "san_ip", "san_email"), tls_client_auth_value = NULL, jwks_uri = NULL, oauth_client = NULL )
client |
OAuthClient configured for RFC 8705 mutual TLS client authentication or for certificate-bound access tokens. |
tls_client_auth_type |
For |
tls_client_auth_value |
Optional explicit value for the selected
|
jwks_uri |
Optional absolute URL of a JWKS document to publish for
|
oauth_client |
Compatibility alias for |
For tls_client_auth, the result identifies the client certificate using
one selected subject or alternative-name field. For
self_signed_tls_client_auth, it contains an inline jwks with the
certificate chain (x5c), or the supplied jwks_uri.
For certificate-bound tokens without mTLS client authentication, the result
uses the corresponding registration authentication method (for example,
public becomes none) and sets
tls_client_certificate_bound_access_tokens = TRUE. See
the advanced security vignette for when these configurations are useful.
A JSON-ready list of RFC 7591/RFC 8705 client metadata.
# Set these environment variables to your existing certificate and key files. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "tls_client_auth" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback", mtls_client_cert_file = Sys.getenv("OAUTH_MTLS_CERT_FILE"), mtls_client_key_file = Sys.getenv("OAUTH_MTLS_KEY_FILE") ) oauth_client_mtls_registration(client)# Set these environment variables to your existing certificate and key files. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "tls_client_auth" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback", mtls_client_cert_file = Sys.getenv("OAUTH_MTLS_CERT_FILE"), mtls_client_key_file = Sys.getenv("OAUTH_MTLS_KEY_FILE") ) oauth_client_mtls_registration(client)
Use your Apple developer key to create the client secret expected by Sign in
with Apple. Pass the returned string as client_secret to oauth_client().
Unlike a fixed password, this secret expires; replace it before its expiry.
oauth_client_secret_apple( client_id, team_id, key_id, private_key, expires_in = 15776700, issued_at = Sys.time(), audience = "https://appleid.apple.com" )oauth_client_secret_apple( client_id, team_id, key_id, private_key, expires_in = 15776700, issued_at = Sys.time(), audience = "https://appleid.apple.com" )
client_id |
Apple Services ID or App ID used as the OAuth client id |
team_id |
Apple Developer Team ID. Apple documents this as a 10-character identifier |
key_id |
Apple Sign in with Apple private-key identifier ( |
private_key |
Apple private key as an |
expires_in |
Positive whole-number lifetime in seconds. Must be at least
one second and no more than
|
issued_at |
Issue time for the JWT. Defaults to |
audience |
Audience claim. Defaults to |
The helper signs a JWT with ES256. It places your Team ID in iss, your
client ID in sub, Apple's URL in aud, and the key ID in the header.
The lifetime must not exceed 15,777,000 seconds (about six months).
A compact signed JWT string suitable for oauth_client(..., client_secret = ...)
## Not run: key <- openssl::ec_keygen(curve = "P-256") oauth_client_secret_apple( client_id = "com.example.web", team_id = "ABCDEFGHIJ", key_id = "ABC123DEFG", private_key = key ) ## End(Not run)## Not run: key <- openssl::ec_keygen(curve = "P-256") oauth_client_secret_apple( client_id = "com.example.web", team_id = "ABCDEFGHIJ", key_id = "ABC123DEFG", private_key = key ) ## End(Not run)
Combine one module's reactive token with its client and approved API addresses
configured on oauth_client(). Call [["request"]]() on the returned connection instead of
assembling a token, client and URL for each request. It reads the reactive token
again after refresh or logout and restricts requests to the configured APIs.
This optional wrapper expires with its Shiny session; it does not implement
refresh itself or retain credentials across redirects.
Clients with token_targets must use the module's connection() factory,
which retains the authorization's permission limits alongside its tokens.
oauth_connection( client, token_reactive, session = shiny::getDefaultReactiveDomain() )oauth_connection( client, token_reactive, session = shiny::getDefaultReactiveDomain() )
client |
An OAuthClient with non-empty |
token_reactive |
A Shiny reactive expression returning the current OAuthToken
or |
session |
The owning Shiny session; defaults to the current session. |
Create the reference once inside server(). Access it only in that session's
reactive context. [["summary"]]() excludes tokens, identity claims and extension
context. [["is_usable"]]() checks local presence, known expiry and required scopes;
it cannot guarantee remote authorization. Unknown token expiry is unusable.
Paths are relative to the selected base directory. Absolute and root-relative references (including pagination links) must stay within that same base. Dot segments and ambiguous encodings are rejected; redirects are never followed. Bearer, DPoP and mTLS use the existing transport and the configured client.
Use request-level required_scopes for optional operations. They must be
included in the client's requested scopes and covered by the current grant.
The package cannot infer arbitrary API permissions from an HTTP method/path.
The legacy module continues to own refresh and logout. Retained connection
storage and its independent lifecycle are available through
oauth_connections() and oauth_connections_server().
An OAuthConnection with [["id"]], [["is_usable"]](), [["summary"]]() and
[["request"]](resource_id, path = "", query = NULL, method = "GET", required_scopes = character(), configure = NULL). Requests return httr2
responses. configure can add a body and application headers; see
OAuthConnection for its contract.
## Not run: # Configure outside server(): client <- oauth_client(provider, "registered-app", redirect_uri = "https://app.example/callback", scopes = "read", resource_bases = c(api = "https://api.example/v1")) # Inside server(): auth <- oauth_module_server("auth", client) connection <- oauth_connection(client, shiny::reactive(auth[["token"]])) data <- shiny::reactive({ shiny::req(connection[["is_usable"]]()) connection[["request"]]("api", "records", required_scopes = "read") }) ## End(Not run)## Not run: # Configure outside server(): client <- oauth_client(provider, "registered-app", redirect_uri = "https://app.example/callback", scopes = "read", resource_bases = c(api = "https://api.example/v1")) # Inside server(): auth <- oauth_module_server("auth", client) connection <- oauth_connection(client, shiny::reactive(auth[["token"]])) data <- shiny::reactive({ shiny::req(connection[["is_usable"]]()) connection[["request"]]("api", "records", required_scopes = "read") }) ## End(Not run)
Stores encrypted credential envelopes and coordinates their lifecycle across
Shiny sessions in one R process. Create one store outside server(). This
adapter does not survive process restarts and rejects use from a copied worker
process. It is a connection-manager building block, not an authentication API.
oauth_connection_store_memory( max_age = 28800, refresh_timeout = 60, max_entries = 1000L ) ## S3 method for class 'OAuthConnectionStore' print(x, ...)oauth_connection_store_memory( max_age = 28800, refresh_timeout = 60, max_entries = 1000L ) ## S3 method for class 'OAuthConnectionStore' print(x, ...)
max_age |
Maximum record and transaction-deduplication lifetime in seconds. Each record must also supply an earlier or equal absolute expiry. |
refresh_timeout |
Maximum time in seconds for a claimed refresh. An abandoned operation becomes uncertain; its old credentials cannot be retried. |
max_entries |
Maximum live record and transaction-reservation count, including tombstones and recently expired transactions. A full store rejects creation instead of evicting another connection. |
x |
An |
... |
Unused print arguments. |
Methods are trusted server-side operations. The manager must validate the current owner session and generation before calling them. The opaque owner identifier scopes records and can survive session rotation; knowledge of an owner or connection ID does not authenticate a user. Records contain ciphertext only. Encryption keys stay with the manager, outside this adapter. Never expose these methods or credential imports as HTTP routes.
[["create"]](owner, id, transaction, client, fingerprint, sealed, expires_at)
returns a record with a new revision, or NULL for a duplicate connection/transaction.
[["read"]](owner, id) returns the owner's record, or NULL when absent, expired,
or owned by someone else. It includes the sealed envelope for internal use.
[["list"]](owner) returns metadata lists without ciphertext or operation IDs.
[["begin_refresh"]](owner, id, revision) claims an active record and returns its
new revision and operation ID. A conflicting claim returns NULL.
[["commit_refresh"]](owner, id, operation, revision, sealed) installs credentials
only for the current claim, returning the updated record or NULL.
[["fail_refresh"]](owner, id, operation, revision, outcome) releases a claim only
for "not_consumed"; "possibly_consumed" and "consumed" remove the old
envelope and mark the record "uncertain". Returns the record or NULL.
[["disconnect"]](owner, id, revision) first installs a credential-free tombstone,
then returns list(record, previous) for bounded remote cleanup. Conflicts
return NULL; the caller must reload before retrying.
[["disconnect_owner"]](owner) tombstones all of that owner's records and returns
the previous records for bounded cleanup. Other owners are unaffected.
Reads expire abandoned refresh claims before returning data. Tombstones and transaction IDs remain at least until the original absolute expiry so late completions cannot restore a disconnected grant. Operations are synchronous and do not yield or perform network calls. Atomicity applies to this R process only. A separate shared backend needs its own verified concurrency contract.
An OAuthConnectionStore adapter with the methods described below.
oauth_client(), oauth_connection()
# Create once, outside server(), then supply to oauth_connections(). store <- oauth_connection_store_memory(max_age = 8 * 3600, max_entries = 100)# Create once, outside server(), then supply to oauth_connections(). store <- oauth_connection_store_memory(max_age = 8 * 3600, max_entries = 100)
Create one manager outside server() for a named set of client/API
configurations from oauth_client() or smart_client(). Use oauth_connections_ui() to handle
callbacks and oauth_connections_server() for each Shiny session. Each
successful authorization creates a separate connection, including repeated
authorizations at the same client.
Connections are independent local records. Providers may reuse an upstream
grant, so revoking one can invalidate tokens held by other connections.
Within a manager, refreshes sharing one credential are serialized. Rotation
or an uncertain refresh invalidates other records holding the same credential;
authorize those records again. Their identity and permissions are never
replaced by another connection's response. Successful revocation also
invalidates known copies of that credential. Distinct tokens may still share
an upstream grant whose revocation effects the manager cannot predict.
Retired credential digests survive connection replacement and expiry for at
least the store's maximum age and the longest client state lifetime. A separate
registry holds at most 10,000 digests or pending reservations. A full registry
rejects refresh or skips remote revocation before sending credentials; it does
not evict retirement evidence. Existing unrelated access remains usable.
oauth_connections( clients, app_origin, retention = c("shiny", "browser", "account"), retention_seconds = 28800, owner_policy = NULL, store = NULL, keys = NULL, callback_policy = "distinct_routes" ) ## S3 method for class 'OAuthConnections' print(x, ...)oauth_connections( clients, app_origin, retention = c("shiny", "browser", "account"), retention_seconds = 28800, owner_policy = NULL, store = NULL, keys = NULL, callback_policy = "distinct_routes" ) ## S3 method for class 'OAuthConnections' print(x, ...)
clients |
Non-empty named list of OAuthClient objects, at most 64.
|
app_origin |
Public application origin, including a non-default port. HTTPS is required for retained owners, except an explicit browser-owner HTTP loopback exception. Session-only development also permits loopback HTTP. |
retention |
|
retention_seconds |
Maximum lifetime of each stored grant, in seconds.
Positive and finite, no larger than the store's |
owner_policy |
|
store |
A store from |
keys |
Named list with |
callback_policy |
|
x |
A connection manager to print. |
... |
Unused print arguments. |
A manager is bound to one UI/server module ID and one public origin. Create another manager for another namespace. The memory store and owner registries survive Shiny sessions, not R restarts; copies in another R process fail closed.
In session-only mode, a pending authorization survives navigation through the existing single-use OAuth state and browser binding, while existing grants are discarded with the old Shiny session. Browser/account mode additionally binds authorization to the initiating local owner and its session generation.
These are optional package interfaces, not SMART protocol objects. This manager
supports generic OAuth clients and the SMART discovery, scope and launch
policies configured by smart_client().
An OAuthConnections server-side configuration object. Printing shows
only the retention mode and client count. It contains client configuration
and deployment keys and must never be sent to the browser.
oauth_connections_ui(), oauth_connections_server()
# Replace this example provider and client ID with your registered application. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback/service", resource_bases = c(api = "https://api.example.com") ) # Create the manager once, outside server(). Default retention is one Shiny session. manager <- oauth_connections( clients = list(service = client), app_origin = "http://127.0.0.1:8100" ) ui <- oauth_connections_ui( shiny::fluidPage( shiny::actionButton("connect", "Connect to service"), shiny::verbatimTextOutput("connections") ), id = "auth", manager = manager ) server <- function(input, output, session) { auth <- oauth_connections_server("auth", manager) shiny::observeEvent(input[["connect"]], auth[["connect"]]("service")) output[["connections"]] <- shiny::renderPrint(auth[["connections"]]()) } # Construct the app without launching it or contacting the provider. app <- shiny::shinyApp( ui, server, uiPattern = ".*", options = list(port = 8100) )# Replace this example provider and client ID with your registered application. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback/service", resource_bases = c(api = "https://api.example.com") ) # Create the manager once, outside server(). Default retention is one Shiny session. manager <- oauth_connections( clients = list(service = client), app_origin = "http://127.0.0.1:8100" ) ui <- oauth_connections_ui( shiny::fluidPage( shiny::actionButton("connect", "Connect to service"), shiny::verbatimTextOutput("connections") ), id = "auth", manager = manager ) server <- function(input, output, session) { auth <- oauth_connections_server("auth", manager) shiny::observeEvent(input[["connect"]], auth[["connect"]]("service")) output[["connections"]] <- shiny::renderPrint(auth[["connections"]]()) } # Construct the app without launching it or contacting the provider. app <- shiny::shinyApp( ui, server, uiPattern = ".*", options = list(port = 8100) )
Call once inside server() with the manager and ID used by
oauth_connections_ui(). The returned methods select stored connections by
opaque IDs and recheck the local owner for every operation. Tokens are kept
on the server and are not returned by summaries.
oauth_connections_server( id, manager, async = FALSE, refresh_proactively = FALSE, refresh_lead_seconds = 60, refresh_check_interval_ms = 10000 )oauth_connections_server( id, manager, async = FALSE, refresh_proactively = FALSE, refresh_lead_seconds = 60, refresh_check_interval_ms = 10000 )
id |
Module ID shared with |
manager |
Configuration from |
async |
Whether authorization and refresh use the existing async worker transport. Owner checks and store mutations stay in the original R process. |
refresh_proactively |
Refresh before expiry when a refresh credential is available. Otherwise the manager attempts refresh at expiry. |
refresh_lead_seconds |
Non-negative number of seconds before expiry used for proactive refresh. Background checks never extend owner inactivity limits. |
refresh_check_interval_ms |
Positive polling interval in milliseconds, at least 100. Safely retryable automatic refresh failures wait at least 30 seconds across all connections sharing the same refresh credential and registration; an uncertain refresh requires reconnecting. |
References expire with this Shiny session even when their stored grants survive.
A new session obtains new references after owner verification. The manager
coordinates refresh across its connections. Refresh preserves the original
authentication time and retention expiry.
Reactive connection reads also recheck expiry at refresh_check_interval_ms,
including references used without connections() or errors(). These checks
notify dependent expressions when lifecycle state changes; unchanged polling
does not rerun application requests or extend owner inactivity limits.
Notifications to application code reflect only this owner's record changes.
Without an ID, connection() returns NULL when no authorization remains,
selects the sole retained authorization, or raises shinyOAuth_access_error
with reason selection_required when several remain. Expired, limited and
uncertain authorizations count toward ambiguity; disconnected rows do not.
The connection factory, $access_token() and $has_scopes() avoid rerunning
consumers on unchanged token rotations. Acquisition never extends owner idle
limits or replays application requests. See vignette("external-integrations").
Resource requests and status reads never reset owner inactivity, including when
reactive expressions rerun after automatic refresh. Call touch() from a user
input event handler to count an application action as activity. Do not call it
from polling observers or ordinary reactive readers. Connecting, explicitly
refreshing and disconnecting also count as activity.
Remote revocation is best effort: at most ten seconds per disconnect/logout
batch, at most two seconds and one HTTP attempt per credential. Results are
accepted, unsupported, missing, failed or not_attempted for access and
refresh credentials. accepted describes the endpoint response, not proof of
prior token validity. Local disconnect remains effective if revocation fails.
A provider may revoke an entire authorization grant or related credentials
(RFC 7009 section 2.1). Separate connection IDs do not establish independent
provider grants, even after repeated consent or account selection. Consequently,
default revoke = TRUE may also end access for sibling connections or other
applications covered by the provider's revocation policy. Use revoke = FALSE
when preserving those authorizations is required. This also applies to
credentials returned by work already in flight. Removed credentials remain
valid remotely until the provider expires
or revokes them. Local summaries and is_usable() do not detect such remote
changes: handle API authorization failures and obtain a new authorization.
Use this API inside its owning session's reactive context. Session setup requires a matching HTTP Origin on the Shiny request. Raw HTTP routes cannot import credentials or select an owner. The manager supports one R process.
A server-side list with:
connect(client_name): request a new authorization without discarding others.
EHR-only clients report fresh_ehr_launch_required and return FALSE;
use their registered smart_launch_route() to start authorization.
connections(): reactive list of redacted connection summaries.
connection(connection_id = NULL): an OAuthConnection for requests, refresh,
scope checks and server-side $access_token() retrieval for external SDKs.
reauthorize(connection_id): end this local authorization and start its
replacement with the retained scope limit, without upstream revocation.
Local state preparation is checked first; rejection preserves current access.
The replacement summary includes replaces_connection_id. Failed/cancelled
replacement leaves the old reference invalid. Unrelated authorizations
remain usable; shared-credential protections still apply. EHR-only clients
require a fresh EHR launch instead.
touch(): record explicit user activity after checking the current owner.
Call from an input event handler; returns TRUE invisibly.
disconnect(connection_id, revoke = TRUE): remove local usability first,
then return separate local and remote revocation results.
remote$access aggregates the primary and acquired target tokens, with
precedence failed, not_attempted, unsupported, accepted, missing.
disconnect_all(revoke = TRUE): cancel pending authorizations and disconnect
this owner's stored connections; return a list of results.
logout(revoke = TRUE, reload = TRUE): invalidate the local owner/session
generation first, disconnect its connections, and normally reload the UI.
This does not log the user out of the external OAuth provider or the app's
own account authentication system.
errors(): reactive list of per-client module error codes, with no raw
provider text. An ended owner is reported as owner_unavailable.
# Replace this example provider and client ID with your registered application. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback/service", resource_bases = c(api = "https://api.example.com") ) # Create the manager once, outside server(). Default retention is one Shiny session. manager <- oauth_connections( clients = list(service = client), app_origin = "http://127.0.0.1:8100" ) ui <- oauth_connections_ui( shiny::fluidPage( shiny::actionButton("connect", "Connect to service"), shiny::verbatimTextOutput("connections") ), id = "auth", manager = manager ) server <- function(input, output, session) { auth <- oauth_connections_server("auth", manager) shiny::observeEvent(input[["connect"]], auth[["connect"]]("service")) output[["connections"]] <- shiny::renderPrint(auth[["connections"]]()) } # Construct the app without launching it or contacting the provider. app <- shiny::shinyApp( ui, server, uiPattern = ".*", options = list(port = 8100) )# Replace this example provider and client ID with your registered application. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback/service", resource_bases = c(api = "https://api.example.com") ) # Create the manager once, outside server(). Default retention is one Shiny session. manager <- oauth_connections( clients = list(service = client), app_origin = "http://127.0.0.1:8100" ) ui <- oauth_connections_ui( shiny::fluidPage( shiny::actionButton("connect", "Connect to service"), shiny::verbatimTextOutput("connections") ), id = "auth", manager = manager ) server <- function(input, output, session) { auth <- oauth_connections_server("auth", manager) shiny::observeEvent(input[["connect"]], auth[["connect"]]("service")) output[["connections"]] <- shiny::renderPrint(auth[["connections"]]()) } # Construct the app without launching it or contacting the provider. app <- shiny::shinyApp( ui, server, uiPattern = ".*", options = list(port = 8100) )
Wrap the application's UI with the same manager and module ID used by
oauth_connections_server(). Browser retention establishes its HttpOnly
owner cookie on an ordinary page request before Shiny starts. OAuth callbacks
use the existing validated callback bridge and clean continuation.
This wrapper also includes the browser dependency and response headers;
do not add a separate use_shinyOAuth() call or nest oauth_ui().
oauth_connections_ui( base_ui, id, manager, request_uri_resolver = NULL, app_base_path = "/", launch_routes = list(), additional_clients = list() )oauth_connections_ui( base_ui, id, manager, request_uri_resolver = NULL, app_base_path = "/", launch_routes = list(), additional_clients = list() )
base_ui |
A Shiny UI object or request-dependent UI function. |
id |
Module ID shared with |
manager |
Configuration from |
request_uri_resolver |
Optional trusted function mapping a Rook request to its public absolute URI. Required when a trusted reverse proxy changes the apparent scheme/host. Do not trust arbitrary forwarded headers. The resolved origin must match the manager's configured origin. |
app_base_path |
Public path at which the Shiny application is hosted,
default |
launch_routes |
List of |
additional_clients |
Optional named list of ordinary OAuth/OIDC clients
used by separate |
Raw GET and form POST callbacks never create or rotate an owner. A POST may
lack a SameSite owner cookie. With Strict owner cookies, a validated callback
first serves an inert same-origin document that navigates to the clean
continuation, allowing the browser to send its existing cookie. The document
does not establish an owner or exchange credentials. A managed continuation
must carry a still-valid owner before credentials can be exchanged.
Ordinary entry without a Strict cookie also uses a bounded same-origin
navigation before creating an empty owner. Browsers must supply same-origin
Fetch Metadata or a same-origin Referer or Origin header on that request.
The shinyOAuth_owner_recovery query parameter is reserved for this navigation.
A validated ordinary callback for
additional_clients can establish a new empty manager owner independently.
An invalid cookie on an ordinary
page is cleared using an HTTP response and a same-origin redirect; the next
request establishes a new empty browser owner.
The cookie is scoped to its host. Separate applications on that host must be trusted; different ports or paths do not isolate their cookies. The wrapper adds no owner cookie for session-only or account retention.
A request UI function for shinyApp(..., uiPattern = ".*").
oauth_browser_owner(), oauth_ui()
# Replace this example provider and client ID with your registered application. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback/service", resource_bases = c(api = "https://api.example.com") ) # Create the manager once, outside server(). Default retention is one Shiny session. manager <- oauth_connections( clients = list(service = client), app_origin = "http://127.0.0.1:8100" ) ui <- oauth_connections_ui( shiny::fluidPage( shiny::actionButton("connect", "Connect to service"), shiny::verbatimTextOutput("connections") ), id = "auth", manager = manager ) server <- function(input, output, session) { auth <- oauth_connections_server("auth", manager) shiny::observeEvent(input[["connect"]], auth[["connect"]]("service")) output[["connections"]] <- shiny::renderPrint(auth[["connections"]]()) } # Construct the app without launching it or contacting the provider. app <- shiny::shinyApp( ui, server, uiPattern = ".*", options = list(port = 8100) )# Replace this example provider and client ID with your registered application. provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback/service", resource_bases = c(api = "https://api.example.com") ) # Create the manager once, outside server(). Default retention is one Shiny session. manager <- oauth_connections( clients = list(service = client), app_origin = "http://127.0.0.1:8100" ) ui <- oauth_connections_ui( shiny::fluidPage( shiny::actionButton("connect", "Connect to service"), shiny::verbatimTextOutput("connections") ), id = "auth", manager = manager ) server <- function(input, output, session) { auth <- oauth_connections_server("auth", manager) shiny::observeEvent(input[["connect"]], auth[["connect"]]("service")) output[["connections"]] <- shiny::renderPrint(auth[["connections"]]()) } # Construct the app without launching it or contacting the provider. app <- shiny::shinyApp( ui, server, uiPattern = ".*", options = list(port = 8100) )
Accept the provider's login callback as an HTTP POST and continue login with
oauth_module_server(). Use this when you select response_mode = "form_post"
or "form_post.jwt", either because the provider requires it or to keep
callback parameters out of the browser URL. The POST arrives before a Shiny
session exists; this wrapper receives it and makes the validated callback
available to the server module. For query-string callbacks, use oauth_ui().
oauth_form_post_ui( base_ui, id = NULL, client = NULL, callback_path = NULL, request_uri_resolver = NULL, clients = NULL )oauth_form_post_ui( base_ui, id = NULL, client = NULL, callback_path = NULL, request_uri_resolver = NULL, clients = NULL )
base_ui |
Existing Shiny UI object, or a UI function accepting |
id |
Shiny module id used by |
client |
OAuthClient object used by |
callback_path |
Optional URL path to accept POST callbacks on. Defaults
to the path component of |
request_uri_resolver |
Optional function accepting the Rook |
clients |
Optional client registry as in |
Set response_mode on your oauth_client(), wrap your UI here, and use
the same id and client in oauth_module_server(). The wrapper includes
the browser setup and privacy header supplied by oauth_ui().
For a callback path below the app root, pass uiPattern = ".*" to
shiny::shinyApp() so Shiny routes the callback to this wrapper.
The wrapper checks the incoming address and login state, stores the callback
temporarily, and redirects the browser to a normal Shiny page with a
single-use handle. Raw callback values do not appear in that redirected URL.
For "form_post.jwt", it also validates the signed response using
JWT Secured Authorization Response Mode (JARM).
Every accepted POST receives its own random handle. Handles expire after
120 seconds or the smaller of state_payload_max_age and the state store lifetime.
The module still verifies the browser binding before consuming login state.
Each client/provider/module namespace has a separate bounded pool in the
state store: 1024 partitions of two slots, with each login assigned to one
partition. A full partition rejects new callbacks until a slot is consumed
or expires; a POST never evicts an unexpired response. Hash collisions can
share capacity, but repeating one callback cannot displace another login's
pending response. An expired handle fails validation and cannot select a
later response stored in the same slot. Completed
logins invalidate their remaining candidates. The process-local memory store
also removes sibling candidates; shared stores retain them until expiry to
avoid deleting a new occupant during cleanup. Shared stores require atomic
take and set_if_absent; slots are claimed atomically with a short TTL.
The process-local memory store reuses only consumed or expired slots. Pending
form-post handles issued by an older version must be restarted after upgrading.
A Shiny UI function. Pass it to shiny::shinyApp() and, for non-root
callback paths, use uiPattern = ".*" so Shiny routes the callback path to
this UI function.
If a proxy receives HTTPS and forwards HTTP to Shiny, supply a
request_uri_resolver that reconstructs the public address after verifying
the request came from your trusted proxy. The default resolver does not
trust forwarded headers. The resulting address must match the configured
redirect origin and callback path. See the advanced security vignette.
Callback size limits are documented in the package options reference.
if ( # Example requires a local or remote Keycloak realm whose client allows # http://127.0.0.1:8100/callback as a valid redirect URI. nzchar(Sys.getenv("KEYCLOAK_BASE_URL")) && nzchar(Sys.getenv("KEYCLOAK_REALM")) && nzchar(Sys.getenv("KEYCLOAK_CLIENT_ID")) && interactive() ) { library(shiny) library(shinyOAuth) options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) provider <- oauth_provider_keycloak( base_url = Sys.getenv("KEYCLOAK_BASE_URL"), realm = Sys.getenv("KEYCLOAK_REALM") ) client <- oauth_client( provider = provider, client_id = Sys.getenv("KEYCLOAK_CLIENT_ID"), client_secret = Sys.getenv("KEYCLOAK_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100/callback", scopes = c("openid", "profile", "email"), response_mode = "form_post" ) base_ui <- fluidPage( uiOutput("login") ) ui <- oauth_form_post_ui(base_ui, id = "auth", client = client) server <- function(input, output, session) { auth <- oauth_module_server("auth", client, auto_redirect = TRUE) output[["login"]] <- renderUI({ if (auth[["authenticated"]]) { user_info <- auth[["token"]]@userinfo tagList( tags[["p"]]("You are logged in!"), tags[["pre"]](paste(capture.output(str(user_info)), collapse = "\n")) ) } else { tags[["p"]]("You are not logged in.") } }) } runApp( shinyApp(ui, server, uiPattern = ".*"), port = 8100, launch.browser = FALSE ) }if ( # Example requires a local or remote Keycloak realm whose client allows # http://127.0.0.1:8100/callback as a valid redirect URI. nzchar(Sys.getenv("KEYCLOAK_BASE_URL")) && nzchar(Sys.getenv("KEYCLOAK_REALM")) && nzchar(Sys.getenv("KEYCLOAK_CLIENT_ID")) && interactive() ) { library(shiny) library(shinyOAuth) options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) provider <- oauth_provider_keycloak( base_url = Sys.getenv("KEYCLOAK_BASE_URL"), realm = Sys.getenv("KEYCLOAK_REALM") ) client <- oauth_client( provider = provider, client_id = Sys.getenv("KEYCLOAK_CLIENT_ID"), client_secret = Sys.getenv("KEYCLOAK_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100/callback", scopes = c("openid", "profile", "email"), response_mode = "form_post" ) base_ui <- fluidPage( uiOutput("login") ) ui <- oauth_form_post_ui(base_ui, id = "auth", client = client) server <- function(input, output, session) { auth <- oauth_module_server("auth", client, auto_redirect = TRUE) output[["login"]] <- renderUI({ if (auth[["authenticated"]]) { user_info <- auth[["token"]]@userinfo tagList( tags[["p"]]("You are logged in!"), tags[["pre"]](paste(capture.output(str(user_info)), collapse = "\n")) ) } else { tags[["p"]]("You are not logged in.") } }) } runApp( shinyApp(ui, server, uiPattern = ".*"), port = 8100, launch.browser = FALSE ) }
Call oauth_module_server() inside your Shiny server() function to manage
login for each user. It sends users to the provider, checks their return,
and gives your app reactive login status and user information.
Create client with oauth_client() outside server(), and wrap your
complete UI with oauth_ui(ui, id = "auth", client = client), using the
same module ID and client. Use oauth_form_post_ui() for POST callbacks.
These wrappers include the browser dependency; no separate
use_shinyOAuth() call is needed.
This uses the OAuth 2.0 Authorization Code flow, with OpenID Connect (OIDC) identity checks when configured for an OIDC provider.
oauth_module_server( id, client, auto_redirect = TRUE, async = FALSE, indefinite_session = FALSE, reauth_after_seconds = NULL, refresh_proactively = FALSE, refresh_lead_seconds = 60, refresh_check_interval_ms = 10000, revoke_on_session_end = FALSE, tab_title_cleaning = TRUE, tab_title_replacement = NULL, request_uri_base_url = NULL, browser_cookie_path = NULL, browser_cookie_samesite = c("Strict", "Lax", "None"), refresh_check_interval = NULL )oauth_module_server( id, client, auto_redirect = TRUE, async = FALSE, indefinite_session = FALSE, reauth_after_seconds = NULL, refresh_proactively = FALSE, refresh_lead_seconds = 60, refresh_check_interval_ms = 10000, revoke_on_session_end = FALSE, tab_title_cleaning = TRUE, tab_title_replacement = NULL, request_uri_base_url = NULL, browser_cookie_path = NULL, browser_cookie_samesite = c("Strict", "Lax", "None"), refresh_check_interval = NULL )
id |
A name for this Shiny module, such as |
client |
The app configuration created with |
auto_redirect |
If |
async |
If |
indefinite_session |
If TRUE, the module will not automatically clear
the token due to access-token expiry or the |
reauth_after_seconds |
Optional maximum interactive-authentication age
in seconds. If set, the module removes the token (and thus sets
|
refresh_proactively |
If |
refresh_lead_seconds |
Number of seconds before expiry to attempt
proactive refresh (default: 60). An already running refresh, including one
requested on demand, has this many seconds plus 5 after access-token expiry
to finish before the module clears the authorization. This grace period
never extends |
refresh_check_interval_ms |
Fallback interval in milliseconds for checking expiry and refresh (default 10000). Known expiry times are scheduled directly; this interval is used as a safety check or when expiry is unknown or infinite. |
revoke_on_session_end |
If TRUE, automatically revokes provider tokens
when the Shiny session ends (e.g., browser tab closed, session timeout).
Also attempts revocation when access-token expiry, maximum authentication
age, or refresh failure automatically clears the session, before losing its
credentials. A retained session keeps its provider grant.
This is a best-effort operation. Revocation runs asynchronously only when
the module is configured with |
tab_title_cleaning |
If TRUE (default), removes any query string suffix from the browser tab title after the OAuth callback, so titles like "localhost:8100?code=...&state=..." become "localhost:8100" |
tab_title_replacement |
Optional character string to explicitly set the
browser tab title after the OAuth callback. If provided, it takes
precedence over |
request_uri_base_url |
Optional absolute base URL used when
|
browser_cookie_path |
URL path covered by the login cookie. Default |
browser_cookie_samesite |
Cookie setting controlling when the browser sends
the login cookie on requests from other sites. One of |
refresh_check_interval |
Compatibility alias for |
Login starts automatically by default. Use auto_redirect = FALSE and
auth[["request_login"]]() to start it from a button. Read auth[["authenticated"]]
in reactive code, and use req(auth[["authenticated"]]) before server operations
that require login. Your app must also enforce its own access rules.
See the usage vignette for a complete app and instructions for registration, API calls, and deployment.
A shiny::reactiveValues() object. If you assign it to auth,
its main fields are:
auth[["authenticated"]]: TRUE when a token is present and the configured
checks have passed, otherwise FALSE. With indefinite_session = TRUE,
the flag stays true while a token is kept, including after refresh errors.
With token targets, it tracks the retained authorization across individual
token expiry and recoverable target failures. Connection methods check the
selected token's lifetime and permissions before use; the configured
authentication-age limit still applies to the whole authorization.
auth[["token"]]: an OAuthToken, or NULL before login or after clearing
the session. Read properties with @, for example auth[["token"]]@userinfo.
Additional token response parameters are available in
auth[["token"]]@extra_fields; auth[["token"]]@initial_extra_fields preserves
the parameters from the initial code exchange across refreshes.
auth[["error"]], auth[["error_description"]]: the error code and available
diagnostic detail. Use your own user-facing message; these fields can
include sensitive provider information.
auth[["error_uri"]]: an optional provider help URL. Only absolute HTTPS
URLs on provider or explicitly allowed hosts are surfaced. Treat it as
untrusted navigation input. NULL means the provider omitted the URL or
supplied a value that did not pass validation.
auth[["token_stale"]]: TRUE when a target authorization retains an
expired primary token, or an indefinite session keeps an expired token
or one whose refresh failed. Resets after successful login, primary-token
refresh, or authorization clearing, including logout and maximum
authentication age. Refreshing a secondary target does not renew the
primary token.
The object also supplies:
auth$connection(): the current OAuthConnection, or NULL before login
or after the module clears the authorization. The reference and its $id
survive refresh; logout, reauthorization and session closure invalidate it.
Use connection <- shiny::req(auth$connection()) inside reactive code,
then connection$access_token() for a server-side SDK or database driver.
Accessor-triggered refresh works without refresh_proactively = TRUE;
the module's configured expiry/session-clearing policy still applies.
auth$reauthorize(): invalidate the current local authorization and begin
replacement with its retained scope limit, without upstream revocation.
Local state preparation is checked first; rejection preserves current access.
Failed/cancelled replacement after that does not revive old references.
With no prior grant, this starts ordinary login. It does not force account selection or
a password prompt; configured maximum authentication age still applies.
auth[["request_login"]](): start login. Waits for browser setup when needed
and does nothing if the session is already authenticated. Uses a browser
form when the client selects authorization_method = "POST"; the app's
Content Security Policy form-action must permit the provider endpoint.
auth[["logout"]](): clear the local login and attempt to revoke tokens when
supported, following async. It does not sign out of the provider account.
auth[["build_auth_url"]](): advanced helper for a custom login link.
Rejects POST clients; use request_login() for their form submission.
Creates pending login state as well as the URL, so retain the result for
the link instead of rebuilding it on every UI update.
Rotates and checks the browser binding before creating state. Returns a
promise resolving to the URL (or NA on failure or an obsolete result);
use promises::then(). PAR URLs carry shinyOAuth.par_request_uri,
shinyOAuth.par_expires_in, and shinyOAuth.par_expires_at attributes
to help you decide when to regenerate the link. request_login()
handles these details for button-based login.
Inside observeEvent(), register the promise handler and then return
invisible(NULL) so Shiny can process the browser acknowledgment. Do not
return the pending promise from the observer itself.
auth[["has_browser_token"]](): reports whether the browser token is
available. Use it before building a custom login URL; it does not report
whether the user is authenticated.
auth[["set_browser_token"]](): asks the browser to establish its binding
when missing. The token becomes available after the browser reports it
back to Shiny. An existing token is left unchanged.
auth[["clear_browser_token"]](): clears the browser binding.
request_login() manages cookie setup automatically, and logout()
handles cookie rotation when ending a session.
Other fields manage the module internally and are not needed in app code.
With async = TRUE, configure mirai::daemons() or a non-sequential
future::plan() before starting the app. Slow provider requests can then
run outside the main R process. Without this, network waits can delay all
Shiny sessions sharing that process.
Advanced operations sent to workers include PAR, signed Request Object
preparation, and query JARM verification. State-store operations and Shiny
Request Object publication stay in the main process. Discovery during app
setup, standalone prepare_call(), and JARM verification in
oauth_form_post_ui() remain synchronous. Use timeouts on those network
and storage operations; see the package options reference.
Open the app at its registered return address in a regular browser with
cookies, session storage, and Web Crypto enabled. Embedded IDE viewers may
prevent login. On Posit Connect or Connect Cloud, open the direct app URL in
a new browser tab rather than using the dashboard's embedded preview. In
Connect Cloud, copy the sharing URL from Settings > URL. Register the
exact public callback URL with your provider, including any callback path.
The module emits a reminder once per R process when Posit's deployment
environment markers are present; it does not detect whether a page is embedded.
The binding token stays in origin- and tab-scoped session storage; the
cookie contains an independent marker, which must match the stored record.
The temporary browser cookie follows the state store's max_age, with a
300-second fallback when that lifetime is unavailable. The separate
state_payload_max_age client setting limits the age of the login request.
Each new login uses a fresh server-selected browser binding and its own marker
cookie. Application callback routes and module namespaces identify the storage
record. Separate tabs can complete logins independently; complete a login in
the tab that started it. Starting another login in the same tab and module
replaces that tab's pending binding. Pending logins must be restarted after
upgrading from versions that used local storage.
Private browser-binding inputs are excluded from Shiny bookmarks. Do not
copy auth[["browser_token"]] into custom bookmark values, URLs, or logs.
Treat the entire hostname as a trust boundary: cookies are shared across
ports, even with __Host-, Secure, or HttpOnly. Use a dedicated hostname
when other services are not trusted. The origin-scoped check prevents cookie
adoption across ports, but co-hosted services can still disrupt cookies.
oauth_ui(), oauth_client(), OAuthToken, oauth_form_post_ui()
# Register http://127.0.0.1:8100 as the GitHub OAuth App callback URL. if ( # Example requires configured GitHub OAuth 2.0 app # (go to https://github.com/settings/developers to create one): nzchar(Sys.getenv("GITHUB_OAUTH_CLIENT_ID")) && nzchar(Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET")) && interactive() ) { library(shiny) library(shinyOAuth) # Define client client <- oauth_client( provider = oauth_provider_github(), client_id = Sys.getenv("GITHUB_OAUTH_CLIENT_ID"), client_secret = Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100", scopes = c("read:user", "user:email") ) # Choose which app you want to run app_to_run <- NULL while (!isTRUE(app_to_run %in% c(1:4))) { app_to_run <- readline( prompt = paste0( "Which example app do you want to run?\n", " 1: Auto-redirect login\n", " 2: Manual login button\n", " 3: Fetch additional resource with access token\n", " 4: No app (all will be defined but none run)\n", "Enter 1, 2, 3, or 4... " ) ) } if (app_to_run %in% c(1:3)) { cli::cli_alert_info(paste0( "Will run example app {app_to_run} on {.url http://127.0.0.1:8100}\n", "Open this URL in a regular browser (viewers in RStudio/Positron/etc. ", "cannot perform necessary redirects)" )) } # Example app with auto-redirect (1) ----------------------------------------- ui_1 <- oauth_ui( fluidPage( uiOutput("login") ), id = "auth", client = client ) server_1 <- function(input, output, session) { # Auto-redirect (default): auth <- oauth_module_server( "auth", client, auto_redirect = TRUE ) output[["login"]] <- renderUI({ if (auth[["authenticated"]]) { user_info <- auth[["token"]]@userinfo tagList( tags[["p"]]("You are logged in!"), tags[["pre"]](paste(capture.output(str(user_info)), collapse = "\n")) ) } else { tags[["p"]]("You are not logged in.") } }) } app_1 <- shinyApp(ui_1, server_1) if (app_to_run == "1") { runApp( app_1, port = 8100, launch.browser = FALSE ) } # Example app with manual login button (2) ----------------------------------- ui_2 <- oauth_ui( fluidPage( actionButton("login_btn", "Login"), actionButton("logout_btn", "Logout"), uiOutput("login") ), id = "auth", client = client ) server_2 <- function(input, output, session) { auth <- oauth_module_server( "auth", client, auto_redirect = FALSE ) observeEvent(input[["login_btn"]], { auth[["request_login"]]() }) observeEvent(input[["logout_btn"]], { auth[["logout"]]() }) output[["login"]] <- renderUI({ if (auth[["authenticated"]]) { user_info <- auth[["token"]]@userinfo tagList( tags[["p"]]("You are logged in!"), tags[["pre"]](paste(capture.output(str(user_info)), collapse = "\n")) ) } else { tags[["p"]]("You are not logged in.") } }) } app_2 <- shinyApp(ui_2, server_2) if (app_to_run == "2") { runApp( app_2, port = 8100, launch.browser = FALSE ) } # Example app requesting additional resource with access token (3) ----------- # Below app shows the authenticated username + their GitHub repositories, # fetched via GitHub API using the access token obtained during login ui_3 <- oauth_ui( fluidPage( uiOutput("ui") ), id = "auth", client = client ) server_3 <- function(input, output, session) { auth <- oauth_module_server( "auth", client, auto_redirect = TRUE ) repositories <- reactiveVal(NULL) repository_error <- reactiveVal(FALSE) observe({ req(auth[["authenticated"]]) # Example additional API request using the access token # (e.g., fetch user repositories from GitHub) # This loads one page; use the API's pagination for further results. repos_data <- tryCatch( { resp <- perform_resource_req( auth[["token"]], "https://api.github.com/user/repos", query = list(per_page = 30) ) httr2::resp_check_status(resp) httr2::resp_body_json(resp, simplifyVector = TRUE) }, error = function(e) NULL ) repository_error(is.null(repos_data)) repositories(repos_data) }) # Render username + their repositories output[["ui"]] <- renderUI({ if (isTRUE(auth[["authenticated"]])) { user_info <- auth[["token"]]@userinfo repos <- repositories() return(tagList( tags[["p"]](paste("You are logged in as:", user_info[["login"]])), tags[["h4"]]("Your repositories:"), if (repository_error()) { tags[["p"]]("Could not load repositories.") } else if (!is.null(repos) && length(repos) == 0) { tags[["p"]]("No repositories returned.") } else if (!is.null(repos)) { tags[["ul"]]( Map( function(url, name) { # Render names as text; accept only GitHub HTTPS links. if (isTRUE(grepl("^https://github\\.com/", url))) { tags[["li"]](tags[["a"]]( href = url, target = "_blank", rel = "noopener noreferrer", name )) } else { tags[["li"]](name) } }, repos[["html_url"]], repos[["full_name"]] ) ) } else { tags[["p"]]("Loading repositories...") } )) } return(tags[["p"]]("You are not logged in.")) }) } app_3 <- shinyApp(ui_3, server_3) if (app_to_run == "3") { runApp( app_3, port = 8100, launch.browser = FALSE ) } }# Register http://127.0.0.1:8100 as the GitHub OAuth App callback URL. if ( # Example requires configured GitHub OAuth 2.0 app # (go to https://github.com/settings/developers to create one): nzchar(Sys.getenv("GITHUB_OAUTH_CLIENT_ID")) && nzchar(Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET")) && interactive() ) { library(shiny) library(shinyOAuth) # Define client client <- oauth_client( provider = oauth_provider_github(), client_id = Sys.getenv("GITHUB_OAUTH_CLIENT_ID"), client_secret = Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100", scopes = c("read:user", "user:email") ) # Choose which app you want to run app_to_run <- NULL while (!isTRUE(app_to_run %in% c(1:4))) { app_to_run <- readline( prompt = paste0( "Which example app do you want to run?\n", " 1: Auto-redirect login\n", " 2: Manual login button\n", " 3: Fetch additional resource with access token\n", " 4: No app (all will be defined but none run)\n", "Enter 1, 2, 3, or 4... " ) ) } if (app_to_run %in% c(1:3)) { cli::cli_alert_info(paste0( "Will run example app {app_to_run} on {.url http://127.0.0.1:8100}\n", "Open this URL in a regular browser (viewers in RStudio/Positron/etc. ", "cannot perform necessary redirects)" )) } # Example app with auto-redirect (1) ----------------------------------------- ui_1 <- oauth_ui( fluidPage( uiOutput("login") ), id = "auth", client = client ) server_1 <- function(input, output, session) { # Auto-redirect (default): auth <- oauth_module_server( "auth", client, auto_redirect = TRUE ) output[["login"]] <- renderUI({ if (auth[["authenticated"]]) { user_info <- auth[["token"]]@userinfo tagList( tags[["p"]]("You are logged in!"), tags[["pre"]](paste(capture.output(str(user_info)), collapse = "\n")) ) } else { tags[["p"]]("You are not logged in.") } }) } app_1 <- shinyApp(ui_1, server_1) if (app_to_run == "1") { runApp( app_1, port = 8100, launch.browser = FALSE ) } # Example app with manual login button (2) ----------------------------------- ui_2 <- oauth_ui( fluidPage( actionButton("login_btn", "Login"), actionButton("logout_btn", "Logout"), uiOutput("login") ), id = "auth", client = client ) server_2 <- function(input, output, session) { auth <- oauth_module_server( "auth", client, auto_redirect = FALSE ) observeEvent(input[["login_btn"]], { auth[["request_login"]]() }) observeEvent(input[["logout_btn"]], { auth[["logout"]]() }) output[["login"]] <- renderUI({ if (auth[["authenticated"]]) { user_info <- auth[["token"]]@userinfo tagList( tags[["p"]]("You are logged in!"), tags[["pre"]](paste(capture.output(str(user_info)), collapse = "\n")) ) } else { tags[["p"]]("You are not logged in.") } }) } app_2 <- shinyApp(ui_2, server_2) if (app_to_run == "2") { runApp( app_2, port = 8100, launch.browser = FALSE ) } # Example app requesting additional resource with access token (3) ----------- # Below app shows the authenticated username + their GitHub repositories, # fetched via GitHub API using the access token obtained during login ui_3 <- oauth_ui( fluidPage( uiOutput("ui") ), id = "auth", client = client ) server_3 <- function(input, output, session) { auth <- oauth_module_server( "auth", client, auto_redirect = TRUE ) repositories <- reactiveVal(NULL) repository_error <- reactiveVal(FALSE) observe({ req(auth[["authenticated"]]) # Example additional API request using the access token # (e.g., fetch user repositories from GitHub) # This loads one page; use the API's pagination for further results. repos_data <- tryCatch( { resp <- perform_resource_req( auth[["token"]], "https://api.github.com/user/repos", query = list(per_page = 30) ) httr2::resp_check_status(resp) httr2::resp_body_json(resp, simplifyVector = TRUE) }, error = function(e) NULL ) repository_error(is.null(repos_data)) repositories(repos_data) }) # Render username + their repositories output[["ui"]] <- renderUI({ if (isTRUE(auth[["authenticated"]])) { user_info <- auth[["token"]]@userinfo repos <- repositories() return(tagList( tags[["p"]](paste("You are logged in as:", user_info[["login"]])), tags[["h4"]]("Your repositories:"), if (repository_error()) { tags[["p"]]("Could not load repositories.") } else if (!is.null(repos) && length(repos) == 0) { tags[["p"]]("No repositories returned.") } else if (!is.null(repos)) { tags[["ul"]]( Map( function(url, name) { # Render names as text; accept only GitHub HTTPS links. if (isTRUE(grepl("^https://github\\.com/", url))) { tags[["li"]](tags[["a"]]( href = url, target = "_blank", rel = "noopener noreferrer", name )) } else { tags[["li"]](name) } }, repos[["html_url"]], repos[["full_name"]] ) ) } else { tags[["p"]]("Loading repositories...") } )) } return(tags[["p"]]("You are not logged in.")) }) } app_3 <- shinyApp(ui_3, server_3) if (app_to_run == "3") { runApp( app_3, port = 8100, launch.browser = FALSE ) } }
Configure a service from its documented endpoint URLs and protocol settings
when no named provider helper fits or OIDC discovery is unavailable.
Pass the resulting provider to oauth_client().
For a supported service, its named helper is an easier starting point;
for OIDC, oauth_provider_oidc_discover() can look up the settings.
oauth_provider( name, auth_url, token_url, userinfo_url = NA_character_, introspection_url = NA_character_, revocation_url = NA_character_, par_url = NA_character_, par_required = FALSE, authorization_request_front_channel_mode = "compat", request_object_signing_alg_values_supported = character(), request_object_encryption_alg_values_supported = character(), request_object_encryption_enc_values_supported = character(), request_object_encryption_jwk = NULL, signed_request_object_required = FALSE, request_parameter_supported = NA, request_uri_parameter_supported = NA, request_uri_registration_required = NA, token_endpoint_auth_signing_alg_values_supported = character(), dpop_signing_alg_values_supported = character(), authorization_response_iss_parameter_supported = FALSE, response_modes_supported = character(), mtls_endpoint_aliases = list(), mtls_client_certificate_bound_access_tokens = FALSE, issuer = NA_character_, issuer_match = "url", use_nonce = NULL, use_pkce = TRUE, pkce_method = "S256", userinfo_required = NULL, userinfo_id_token_match = NULL, userinfo_signed_jwt_required = FALSE, userinfo_id_selector = function(userinfo) { userinfo[["sub"]] }, id_token_required = NULL, id_token_validation = NULL, extra_auth_params = list(), extra_token_params = list(), extra_token_headers = character(), token_auth_style = "header", jwks_cache = NULL, jwks_pins = character(), jwks_pin_mode = "any", jwks_host_issuer_match = NULL, jwks_host_allow_only = NULL, id_token_allowed_algs = c("RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "Ed25519", "EdDSA"), allowed_token_types = c("Bearer"), leeway = getOption("shinyOAuth.leeway", 30), id_token_at_hash_required = FALSE, infer_oidc_from_issuer = TRUE, jwks_uri = NA_character_, userinfo_allowed_algs = NULL, jarm_signing_alg_values_supported = character(), jarm_encryption_alg_values_supported = character(), jarm_encryption_enc_values_supported = character(), jarm_tolerate_duplicate_top_level_iss = FALSE, endpoint_auth_metadata = list(), ..., allowed_algs = NULL, allow_missing_token_type = FALSE, token_target_mode = "none", token_response_scope_format = "space" )oauth_provider( name, auth_url, token_url, userinfo_url = NA_character_, introspection_url = NA_character_, revocation_url = NA_character_, par_url = NA_character_, par_required = FALSE, authorization_request_front_channel_mode = "compat", request_object_signing_alg_values_supported = character(), request_object_encryption_alg_values_supported = character(), request_object_encryption_enc_values_supported = character(), request_object_encryption_jwk = NULL, signed_request_object_required = FALSE, request_parameter_supported = NA, request_uri_parameter_supported = NA, request_uri_registration_required = NA, token_endpoint_auth_signing_alg_values_supported = character(), dpop_signing_alg_values_supported = character(), authorization_response_iss_parameter_supported = FALSE, response_modes_supported = character(), mtls_endpoint_aliases = list(), mtls_client_certificate_bound_access_tokens = FALSE, issuer = NA_character_, issuer_match = "url", use_nonce = NULL, use_pkce = TRUE, pkce_method = "S256", userinfo_required = NULL, userinfo_id_token_match = NULL, userinfo_signed_jwt_required = FALSE, userinfo_id_selector = function(userinfo) { userinfo[["sub"]] }, id_token_required = NULL, id_token_validation = NULL, extra_auth_params = list(), extra_token_params = list(), extra_token_headers = character(), token_auth_style = "header", jwks_cache = NULL, jwks_pins = character(), jwks_pin_mode = "any", jwks_host_issuer_match = NULL, jwks_host_allow_only = NULL, id_token_allowed_algs = c("RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "Ed25519", "EdDSA"), allowed_token_types = c("Bearer"), leeway = getOption("shinyOAuth.leeway", 30), id_token_at_hash_required = FALSE, infer_oidc_from_issuer = TRUE, jwks_uri = NA_character_, userinfo_allowed_algs = NULL, jarm_signing_alg_values_supported = character(), jarm_encryption_alg_values_supported = character(), jarm_encryption_enc_values_supported = character(), jarm_tolerate_duplicate_top_level_iss = FALSE, endpoint_auth_metadata = list(), ..., allowed_algs = NULL, allow_missing_token_type = FALSE, token_target_mode = "none", token_response_scope_format = "space" )
name |
Provider name (e.g., "github", "google"). Cosmetic only; used in logging and audit events |
auth_url |
URL of the provider's login and permission page. |
token_url |
URL where R exchanges the returned code for tokens. |
userinfo_url |
User info endpoint URL (optional) |
introspection_url |
Optional URL where the provider can confirm whether a token is still active (RFC 7662). |
revocation_url |
Optional URL where the app can ask the provider to invalidate a token, for example during logout (RFC 7009). |
par_url |
Optional Pushed Authorization Request (PAR) URL (RFC 9126).
When set, shinyOAuth first sends the authorization request from server to
provider and then redirects the browser with the returned |
par_required |
Logical. Whether the provider
requires authorization requests to be sent via PAR. When |
authorization_request_front_channel_mode |
Character scalar controlling
which browser-visible outer parameters shinyOAuth keeps when the actual
authorization request is carried by JAR or PAR. Use |
request_object_signing_alg_values_supported |
Optional vector of JWS
algorithms that the provider advertises for signed Request Objects (RFC
9101). This is mainly used for early validation when an OAuthClient
sends |
request_object_encryption_alg_values_supported |
Optional vector of JWE key-management algorithms that the provider advertises for encrypted Request Objects. This metadata is used for early validation when an OAuthClient enables Request Object encryption. |
request_object_encryption_enc_values_supported |
Optional vector of JWE content-encryption algorithms that the provider advertises for encrypted Request Objects. This metadata is used for early validation when an OAuthClient enables Request Object encryption. |
request_object_encryption_jwk |
Optional explicit recipient public key used to encrypt Request Objects when discovery-backed JWKS selection is not available or when you need to pin one specific encryption key. Accepts an OpenSSL public key, a PEM public-key string, a parsed JWK object, or a JWK JSON string. |
signed_request_object_required |
Logical. Whether the provider requires
signed Request Objects for authorization requests. When |
request_parameter_supported |
Logical or |
request_uri_parameter_supported |
Logical or |
request_uri_registration_required |
Logical or |
token_endpoint_auth_signing_alg_values_supported |
Optional vector of
JWS algorithms that the provider advertises for JWT-based client
authentication ( |
dpop_signing_alg_values_supported |
Optional vector of JWS algorithms
that the provider advertises for DPoP proof JWTs (RFC 9449). This
metadata is used for early validation of |
authorization_response_iss_parameter_supported |
Logical. Whether the
provider advertises RFC 9207 support for returning an |
response_modes_supported |
Optional character vector of OAuth/OIDC
|
mtls_endpoint_aliases |
Optional named list of RFC 8705 mTLS endpoint
aliases. Names should follow the metadata keys such as |
mtls_client_certificate_bound_access_tokens |
Logical. Whether the
authorization server advertises RFC 8705 capability to issue
certificate-bound access tokens. This describes server capability; the
client still has to opt into mTLS separately. When |
issuer |
Optional authorization-server issuer URL. You need this for issuer validation and features such as ID-token validation. shinyOAuth uses it to verify issuer claims and locate signing keys (JWKS), typically through an OIDC discovery document. |
issuer_match |
Character scalar controlling how strictly the discovery
document's
In most cases, keep the default |
use_nonce |
Whether to tie the ID token to this login using a random nonce. Keep enabled for OIDC. The nonce is sent in the request and checked in the returned ID token. |
use_pkce |
Whether to protect the code exchange using Proof Key for Code
Exchange (PKCE). Leave enabled; public clients require it.
It sends a |
pkce_method |
PKCE code challenge method ("S256" or "plain"). "S256" is recommended. Use "plain" only if you are working with a provider that does not support "S256". |
userinfo_required |
Whether to fetch a user profile after token exchange.
The result is stored in |
userinfo_id_token_match |
Whether fetched userinfo requires a validated ID
token for comparison. When both are available, their actual |
userinfo_signed_jwt_required |
Whether to require the user profile to arrive
as a signed JWT ( |
userinfo_id_selector |
A function that extracts the user ID from the userinfo response. Should take a single argument (the userinfo list) and return the user ID as a string. This is used for helpers that need a provider-specific application user
identifier, such as audit fields. It does not replace OIDC subject binding:
when a validated ID token and UserInfo are both available, their actual
|
id_token_required |
Whether to require an ID token to be returned
during token exchange. If no ID token is returned, the token exchange
will fail. This only makes sense for OpenID Connect providers and may
require the client's scope to include Both the S7 constructor and |
id_token_validation |
Whether to perform ID token validation after token exchange.
This requires the provider to be a valid OpenID Connect provider with a configured
Both the S7 constructor and |
extra_auth_params |
Extra parameters for authorization URL |
extra_token_params |
Extra parameters for token exchange.
|
extra_token_headers |
Extra headers for back-channel token-style
requests (named character vector), applied only to token exchange and
refresh. Configure |
token_auth_style |
How the client authenticates at the token endpoint. One of:
|
jwks_cache |
Storage for the provider's public signing keys. Defaults to
|
jwks_pins |
Optional character vector of RFC 7638 JWK thumbprints
(base64url) to pin against. If non-empty, fetched JWKS must contain keys
whose thumbprints match these values depending on |
jwks_pin_mode |
Pinning policy when |
jwks_host_issuer_match |
When TRUE, enforce that the discovery |
jwks_host_allow_only |
Optional explicit hostname that the jwks_uri must match.
When provided, jwks_uri host must equal this value (exact match). You can
pass either just the host (e.g., "www.googleapis.com") or a full URL; only
the host component will be used. If you need to include a port or an IPv6
literal, pass a full URL (e.g., |
id_token_allowed_algs |
Optional vector of allowed JWT algorithms for ID tokens.
Use to restrict acceptable |
allowed_token_types |
Character vector of acceptable OAuth token types
returned by the token endpoint (case-insensitive). Successful token
responses must include |
leeway |
Clock skew leeway (seconds) applied to ID token |
id_token_at_hash_required |
Whether to require the |
infer_oidc_from_issuer |
Whether setting |
jwks_uri |
Optional URL of the provider's public signing keys (JWKS). Normally these are located through OIDC discovery. Set this for manual key configuration, including OAuth-only JARM providers. |
userinfo_allowed_algs |
Optional signing algorithm allowlist for UserInfo
JWTs. |
jarm_signing_alg_values_supported |
Optional vector of JWS algorithms that the provider advertises for signed JWT Secured Authorization Responses (JARM). |
jarm_encryption_alg_values_supported |
Optional vector of JWE key-management algorithms that the provider advertises for encrypted JARM responses. |
jarm_encryption_enc_values_supported |
Optional vector of JWE content-encryption algorithms that the provider advertises for encrypted JARM responses. |
jarm_tolerate_duplicate_top_level_iss |
Logical. Whether shinyOAuth
should tolerate repeated identical top-level |
endpoint_auth_metadata |
Named list of independent |
... |
Deprecated renamed arguments accepted temporarily for backward compatibility. |
allowed_algs |
Compatibility alias for |
allow_missing_token_type |
Logical, default |
token_target_mode |
Explicit acquisition protocol for clients declaring
|
token_response_scope_format |
Token response scope separator, |
Supply name, auth_url, and token_url to start. Add userinfo_url to
fetch profiles. Supplying issuer enables OIDC defaults, including ID token
validation, unless infer_oidc_from_issuer = FALSE. Advanced arguments must match
your provider's capabilities; see the advanced security vignette.
OAuthProvider object
# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }
Look up Apple's login settings and return an OAuthProvider for use with
oauth_client(). This helper makes an OIDC discovery request during setup.
oauth_provider_apple(name = "apple")oauth_provider_apple(name = "apple")
name |
Optional provider name (default "apple") |
Configure your client with:
Your Apple Services ID or App ID as client_id.
A client secret created with oauth_client_secret_apple() using your
Apple developer key.
An HTTPS return address with a domain name; Apple does not accept localhost or IP addresses.
response_mode = "form_post" and oauth_form_post_ui() when requesting
email or name.
Read identity information from the validated ID token's claims. Apple has
no userinfo endpoint, so userinfo_required is FALSE. The one-time
user payload that Apple may send with a form POST callback is not mapped
into token@userinfo; do not rely on this helper to retrieve that payload.
Apple's documented email_verified strings ("true" and "false") are
normalized to logical values in validated claims after signature and issuer
verification. Other providers retain the standard JSON Boolean requirement.
OAuthProvider object configured for Sign in with Apple
# Sign in with Apple requires an Apple Services ID, Team ID, key ID, and the # corresponding P-256 private key. Network access is required for discovery. if (interactive()) { apple_provider <- oauth_provider_apple() apple_secret <- oauth_client_secret_apple( client_id = "com.example.web", team_id = "ABCDEFGHIJ", key_id = "ABC123DEFG", private_key = openssl::read_key("AuthKey_ABC123DEFG.p8") ) apple_client <- oauth_client( provider = apple_provider, client_id = "com.example.web", client_secret = apple_secret, redirect_uri = "https://example.com/oauth/callback", scopes = c("openid", "email", "name"), response_mode = "form_post" ) }# Sign in with Apple requires an Apple Services ID, Team ID, key ID, and the # corresponding P-256 private key. Network access is required for discovery. if (interactive()) { apple_provider <- oauth_provider_apple() apple_secret <- oauth_client_secret_apple( client_id = "com.example.web", team_id = "ABCDEFGHIJ", key_id = "ABC123DEFG", private_key = openssl::read_key("AuthKey_ABC123DEFG.p8") ) apple_client <- oauth_client( provider = apple_provider, client_id = "com.example.web", client_secret = apple_secret, redirect_uri = "https://example.com/oauth/callback", scopes = c("openid", "email", "name"), response_mode = "form_post" ) }
Look up login settings for your Auth0 domain. Pass the result to
oauth_client() with your registered app credentials. This helper makes
a discovery request during setup.
oauth_provider_auth0(domain, name = "auth0", audience = NULL)oauth_provider_auth0(domain, name = "auth0", audience = NULL)
domain |
Your Auth0 domain, e.g., "your-domain.auth0.com" |
name |
Optional provider name (default "auth0") |
audience |
Optional audience value to send in authorization requests. |
OAuthProvider object configured for the specified Auth0 domain
## Not run: oauth_provider_auth0("your-domain.auth0.com") ## End(Not run)## Not run: oauth_provider_auth0("your-domain.auth0.com") ## End(Not run)
Configure login for an authentik OAuth2/OIDC application. Supply the
instance URL and application slug, then pass the registered credentials
to oauth_client() with the scopes configured in authentik.
oauth_provider_authentik( base_url, application_slug, name = "authentik", token_auth_style = NULL )oauth_provider_authentik( base_url, application_slug, name = "authentik", token_auth_style = NULL )
base_url |
authentik instance URL, for example |
application_slug |
Application slug configured in authentik (not the display name or client ID). |
name |
Optional provider name (default |
token_auth_style |
Authentication style for token requests: "header"
(client_secret_basic), "body" (client_secret_post), or "public"
(public client; send |
Uses authentik's default per-application issuer mode and preserves its
trailing slash. Global issuer mode is not supported by this preset;
configure the endpoints explicitly with oauth_provider() for that mode.
For refresh tokens, both request "offline_access" and enable the
corresponding scope mapping in authentik. Configure a signing key that
supports RS256 for OIDC discovery.
See authentik's provider guide.
OAuthProvider object configured for the authentik application.
## Not run: oauth_provider_authentik("https://auth.example.com", "shiny-app") ## End(Not run)## Not run: oauth_provider_authentik("https://auth.example.com", "shiny-app") ## End(Not run)
Configure OIDC login for an Amazon Cognito user pool. Register an app client
with the authorization code grant, configure a user pool login domain, and
pass the app credentials to oauth_client() with the enabled OIDC scopes.
oauth_provider_cognito(issuer, name = "cognito", token_auth_style = NULL)oauth_provider_cognito(issuer, name = "cognito", token_auth_style = NULL)
issuer |
Exact user pool issuer URL from AWS, for example
|
name |
Optional provider name (default |
token_auth_style |
Authentication style for token requests: "header"
(client_secret_basic), "body" (client_secret_post), or "public"
(public client; send |
Use the exact user pool issuer, not the managed-login or custom domain.
Discovery obtains authorization, token, and UserInfo URLs on that login
domain while retaining the pool issuer for ID token validation. Both the
original cognito-idp and updated issuer-cognito-idp issuer forms are
supported, as are AWS partition-specific hostnames. No AWS credentials
are needed for discovery.
See Cognito endpoints.
OAuthProvider object configured for a Cognito user pool.
## Not run: oauth_provider_cognito( "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_Example" ) ## End(Not run)## Not run: oauth_provider_cognito( "https://cognito-idp.eu-west-1.amazonaws.com/eu-west-1_Example" ) ## End(Not run)
Create the provider configuration for a GitHub OAuth App, then pass it with
your app credentials to oauth_client(). This configures profile retrieval
from GitHub's API; GitHub does not return an OIDC ID token.
GitHub's comma-separated token response scopes are normalized before granted
permissions are validated and stored, including when name is customized.
oauth_provider_github(name = "github")oauth_provider_github(name = "github")
name |
Optional provider name (default "github") |
You can register a new GitHub OAuth 2.0 app in your OAuth App settings.
OAuthProvider object for use with a GitHub OAuth 2.0 app
oauth_provider_github()oauth_provider_github()
Configure OIDC sign-in with GitLab.com or a self-managed GitLab instance.
Register an OAuth application on that instance, then pass its credentials
to oauth_client() with scopes = c("openid", "profile", "email").
oauth_provider_gitlab( base_url = "https://gitlab.com", name = "gitlab", token_auth_style = NULL )oauth_provider_gitlab( base_url = "https://gitlab.com", name = "gitlab", token_auth_style = NULL )
base_url |
GitLab instance URL, including HTTPS and any deployment
subpath. Defaults to |
name |
Optional provider name (default |
token_auth_style |
Authentication style for token requests: "header"
(client_secret_basic), "body" (client_secret_post), or "public"
(public client; send |
Supply only the scopes your app needs; "openid" is enough to request an
ID token. API and repository access require separate GitLab scopes.
See GitLab's OIDC guide.
OAuthProvider object configured for GitLab.
## Not run: oauth_provider_gitlab() oauth_provider_gitlab("https://gitlab.example.edu") ## End(Not run)## Not run: oauth_provider_gitlab() oauth_provider_gitlab("https://gitlab.example.edu") ## End(Not run)
Configure OIDC sign-in with Globus Auth using its published endpoints and
RS512 ID token signatures. Register a confidential web application at
https://app.globus.org/settings/developers and request
scopes = c("openid", "profile", "email") on oauth_client().
oauth_provider_globus(name = "globus")oauth_provider_globus(name = "globus")
name |
Optional provider name (default |
This preset does not make a discovery request. Globus currently advertises RS512 only, whereas generic OIDC discovery requires advertised RS256 support. The preset pins RS512 and the Globus JWKS URL, retaining signature, issuer, audience, nonce, UserInfo subject, and S256 PKCE validation.
This helper supports OIDC login. Globus API grants can return separate
credentials for several resource servers in other_tokens. Those fields
are preserved in token@extra_fields, but shinyOAuth does not automatically
select, refresh, or revoke the nested tokens. Keep this login client's
scopes limited to openid, profile, email, and optionally
offline_access; use a separate Globus-aware integration for multi-resource
API authorization. Never send the login access token to another resource
server merely because a nested token granted access to it.
See the Globus Auth guide.
OAuthProvider object configured for Globus Auth login.
oauth_provider_globus()oauth_provider_globus()
Use your Google app registration with oauth_client() to add Google
sign-in. The helper configures OIDC validation and profile retrieval.
oauth_provider_google(name = "google")oauth_provider_google(name = "google")
name |
Optional provider name (default "google") |
You can register a new Google OAuth 2.0 app in the Google Cloud Console. Configure the client ID & secret in your OAuthClient.
This preset uses a restricted Google OIDC profile: ID tokens must have
iss = "https://accounts.google.com", matching Google's discovery issuer.
Google's ID token validation guidance
also permits "accounts.google.com"; this alternate issuer is not accepted
by this preset. Issuer comparison remains exact, as for other OIDC providers.
OAuthProvider object for use with a Google OAuth 2.0 app
oauth_provider_google()oauth_provider_google()
Configure Sign in with Hugging Face. Register an OAuth application in your
Hugging Face settings, then pass its credentials to oauth_client().
oauth_provider_huggingface(name = "huggingface")oauth_provider_huggingface(name = "huggingface")
name |
Optional provider name (default |
For login, request scopes = c("openid", "profile"), adding "email"
only if needed. Repository access and inference require their own scopes,
such as "read-repos" or "inference-api", and user consent.
This preset targets confidential applications with a client secret.
See Sign in with Hugging Face.
OAuthProvider object configured for Hugging Face.
## Not run: oauth_provider_huggingface() ## End(Not run)## Not run: oauth_provider_huggingface() ## End(Not run)
Look up login settings for a Keycloak realm. Supply your server URL
and realm name, then pass the result to oauth_client(). This helper
contacts the Keycloak server during setup.
oauth_provider_keycloak( base_url, realm, name = paste0("keycloak-", realm), token_auth_style = "body", jarm_tolerate_duplicate_top_level_iss = TRUE )oauth_provider_keycloak( base_url, realm, name = paste0("keycloak-", realm), token_auth_style = "body", jarm_tolerate_duplicate_top_level_iss = TRUE )
base_url |
Base URL of the Keycloak server, e.g.,
"http://localhost:8080". Local HTTP development also requires
|
realm |
Keycloak realm name, e.g., "myrealm" |
name |
Optional provider name. Defaults to |
token_auth_style |
Optional override for token endpoint authentication
method. One of "header" (client_secret_basic), "body"
(client_secret_post), "public" (send |
jarm_tolerate_duplicate_top_level_iss |
Logical. Defaults to |
OAuthProvider object configured for the specified Keycloak realm
## Not run: oauth_provider_keycloak("https://login.example.com", realm = "myrealm") ## End(Not run)## Not run: oauth_provider_keycloak("https://login.example.com", realm = "myrealm") ## End(Not run)
Create provider settings for Microsoft Entra ID. Choose which accounts may
sign in with tenant, then pass the provider and your own registered app's
credentials to oauth_client().
oauth_provider_microsoft( name = "microsoft", tenant = c("common", "organizations", "consumers"), id_token_validation = NULL, userinfo_required = TRUE )oauth_provider_microsoft( name = "microsoft", tenant = c("common", "organizations", "consumers"), id_token_validation = NULL, userinfo_required = TRUE )
name |
Optional friendly name for the provider. Defaults to "microsoft" |
tenant |
Tenant identifier ("common", "organizations", "consumers", or directory GUID). Defaults to "common" |
id_token_validation |
Optional override (logical). If |
userinfo_required |
Retrieve profile information from Microsoft Graph.
Set FALSE for an API token with a different audience. ID-token validation
and nonce checks remain enabled according to |
Use a directory (tenant) ID to target one organization. "organizations"
allows work or school accounts, "consumers" allows personal Microsoft
accounts, and "common" allows both. Your app registration and app access
rules must also permit the intended accounts.
ID token validation is enabled for these tenant choices. For a directory ID,
the issuer must match that directory. "common" and "organizations" use
Microsoft's tenant-independent issuer template and signing-key issuer rules.
"consumers" uses the consumer tenant issuer. The helper restricts ID token
algorithms to RS256 and fetches UserInfo from Microsoft Graph by default.
Set userinfo_required = FALSE when using an API token that cannot call Graph;
validated ID-token claims remain available through connection$identity().
Setting id_token_validation = FALSE disables ID token and nonce checks and
leaves OAuth plus any configured profile retrieval. Keep the default for OIDC
sign-in.
Tenant domains and other unrecognized tenant identifiers require this
explicit opt-out; otherwise use the directory GUID to retain OIDC validation.
OAuthProvider object configured for Microsoft identity platform
if ( # Example requires configured Microsoft Entra ID (Azure AD) tenant: nzchar(Sys.getenv("MS_TENANT")) && interactive() && requireNamespace("later", quietly = TRUE) ) { library(shiny) library(shinyOAuth) # Configure provider and client (Microsoft Entra ID with your tenant) client <- oauth_client( provider = oauth_provider_microsoft( # Provide your own tenant ID here (set as environment variable MS_TENANT) tenant = Sys.getenv("MS_TENANT") ), # Azure CLI public-client app ID; the tenant must permit this app. # For your deployed app, use your own registration and redirect URI: client_id = "04b07795-8ddb-461a-bbee-02f9e1bf7b46", client_secret = "", redirect_uri = "http://localhost:8100", scopes = c("openid", "profile", "email") ) # UI ui <- oauth_ui( fluidPage( h3("OAuth demo (Microsoft Entra ID)"), uiOutput("oauth_error"), tags[["hr"]](), h4("Auth object (summary)"), verbatimTextOutput("auth_print"), tags[["hr"]](), h4("User info"), verbatimTextOutput("user_info") ), id = "auth", client = client ) # Server server <- function(input, output, session) { auth <- oauth_module_server("auth", client) output[["auth_print"]] <- renderText({ authenticated <- auth[["authenticated"]] tok <- auth[["token"]] err <- auth[["error"]] paste0( "Authenticated?", if (isTRUE(authenticated)) " YES" else " NO", "\n", "Has token? ", if (!is.null(tok)) "YES" else "NO", "\n", "Has error? ", if (!is.null(err)) "YES" else "NO", "\n\n", "Token present: ", !is.null(tok), "\n", "Has refresh token: ", !is.null(tok) && isTRUE(nzchar(tok@refresh_token)), "\n", "Has ID token: ", !is.null(tok) && !is.na(tok@id_token), "\n", "Expires at: ", if (!is.null(tok)) tok@expires_at else "N/A" ) }) output[["user_info"]] <- renderPrint({ req(auth[["authenticated"]]) auth[["token"]]@userinfo }) observeEvent( list(auth[["error"]], auth[["error_description"]]), { if (interactive() && !is.null(auth[["error_description"]])) { rlang::inform(c( "OAuth error details", "i" = paste0("error: ", auth[["error"]]), "i" = paste0("error_description: ", auth[["error_description"]]) )) } }, ignoreInit = TRUE ) output[["oauth_error"]] <- renderUI({ if (is.null(auth[["error"]])) { return(NULL) } msg <- if (identical(auth[["error"]], "access_denied")) { "Sign-in was canceled or denied. Please try again." } else { "Authentication failed. Please try again." } div(class = "alert alert-danger", role = "alert", msg) }) } # Need to open app in 'localhost:8100' to match with redirect_uri # of the public Azure CLI app (above). Browser must use 'localhost' # too to properly set the browser cookie. But Shiny only redirects to # '127.0.0.1' & blocks process once it runs. So we disable browser # launch by Shiny & then use 'later::later()' to open the browser # ourselves a short moment after the app starts later::later( function() { utils::browseURL("http://localhost:8100") }, delay = 0.25 ) # Run app runApp(shinyApp(ui, server), port = 8100, launch.browser = FALSE) }if ( # Example requires configured Microsoft Entra ID (Azure AD) tenant: nzchar(Sys.getenv("MS_TENANT")) && interactive() && requireNamespace("later", quietly = TRUE) ) { library(shiny) library(shinyOAuth) # Configure provider and client (Microsoft Entra ID with your tenant) client <- oauth_client( provider = oauth_provider_microsoft( # Provide your own tenant ID here (set as environment variable MS_TENANT) tenant = Sys.getenv("MS_TENANT") ), # Azure CLI public-client app ID; the tenant must permit this app. # For your deployed app, use your own registration and redirect URI: client_id = "04b07795-8ddb-461a-bbee-02f9e1bf7b46", client_secret = "", redirect_uri = "http://localhost:8100", scopes = c("openid", "profile", "email") ) # UI ui <- oauth_ui( fluidPage( h3("OAuth demo (Microsoft Entra ID)"), uiOutput("oauth_error"), tags[["hr"]](), h4("Auth object (summary)"), verbatimTextOutput("auth_print"), tags[["hr"]](), h4("User info"), verbatimTextOutput("user_info") ), id = "auth", client = client ) # Server server <- function(input, output, session) { auth <- oauth_module_server("auth", client) output[["auth_print"]] <- renderText({ authenticated <- auth[["authenticated"]] tok <- auth[["token"]] err <- auth[["error"]] paste0( "Authenticated?", if (isTRUE(authenticated)) " YES" else " NO", "\n", "Has token? ", if (!is.null(tok)) "YES" else "NO", "\n", "Has error? ", if (!is.null(err)) "YES" else "NO", "\n\n", "Token present: ", !is.null(tok), "\n", "Has refresh token: ", !is.null(tok) && isTRUE(nzchar(tok@refresh_token)), "\n", "Has ID token: ", !is.null(tok) && !is.na(tok@id_token), "\n", "Expires at: ", if (!is.null(tok)) tok@expires_at else "N/A" ) }) output[["user_info"]] <- renderPrint({ req(auth[["authenticated"]]) auth[["token"]]@userinfo }) observeEvent( list(auth[["error"]], auth[["error_description"]]), { if (interactive() && !is.null(auth[["error_description"]])) { rlang::inform(c( "OAuth error details", "i" = paste0("error: ", auth[["error"]]), "i" = paste0("error_description: ", auth[["error_description"]]) )) } }, ignoreInit = TRUE ) output[["oauth_error"]] <- renderUI({ if (is.null(auth[["error"]])) { return(NULL) } msg <- if (identical(auth[["error"]], "access_denied")) { "Sign-in was canceled or denied. Please try again." } else { "Authentication failed. Please try again." } div(class = "alert alert-danger", role = "alert", msg) }) } # Need to open app in 'localhost:8100' to match with redirect_uri # of the public Azure CLI app (above). Browser must use 'localhost' # too to properly set the browser cookie. But Shiny only redirects to # '127.0.0.1' & blocks process once it runs. So we disable browser # launch by Shiny & then use 'later::later()' to open the browser # ourselves a short moment after the app starts later::later( function() { utils::browseURL("http://localhost:8100") }, delay = 0.25 ) # Run app runApp(shinyApp(ui, server), port = 8100, launch.browser = FALSE) }
Build OIDC provider URLs from a base address and known endpoint paths.
Use this when configuring an OIDC service without discovery, with its
endpoint paths available from the service configuration or documentation.
Use oauth_provider_oidc_discover() if your provider offers discovery, which
looks up its actual URLs. This helper is for manual configuration; its
default paths must match the service you are using.
oauth_provider_oidc( name, base_url, auth_path = "/authorize", token_path = "/token", userinfo_path = "/userinfo", introspection_path = "/introspect", use_nonce = TRUE, id_token_validation = TRUE, jwks_host_issuer_match = TRUE, allowed_token_types = c("Bearer"), ..., token_auth_style = "header", issuer = base_url )oauth_provider_oidc( name, base_url, auth_path = "/authorize", token_path = "/token", userinfo_path = "/userinfo", introspection_path = "/introspect", use_nonce = TRUE, id_token_validation = TRUE, jwks_host_issuer_match = TRUE, allowed_token_types = c("Bearer"), ..., token_auth_style = "header", issuer = base_url )
name |
Friendly name for the provider |
base_url |
Base URL for OIDC endpoints |
auth_path |
Authorization endpoint path (default: "/authorize") |
token_path |
Token endpoint path (default: "/token") |
userinfo_path |
User info endpoint path (default: "/userinfo") |
introspection_path |
Token introspection endpoint path (default: "/introspect") |
use_nonce |
Logical, whether to use OIDC nonce. Defaults to TRUE |
id_token_validation |
Logical, whether to validate ID tokens automatically for this provider. Defaults to TRUE |
jwks_host_issuer_match |
When TRUE (default), enforce that the JWKS host
discovered from the provider matches the issuer host exactly. For
providers that serve JWKS from a different host (e.g., Google), set
|
allowed_token_types |
Character vector of allowed token types for access tokens issued by this provider. Defaults to 'Bearer' |
... |
Additional arguments passed to |
token_auth_style |
Token endpoint client authentication style passed to
|
issuer |
Exact OIDC issuer identifier. Defaults to |
OAuthProvider object
# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }
Supply your provider's issuer URL to create an OAuthProvider without
entering each service URL yourself. The helper downloads the provider's
discovery document (OpenID Connect Discovery) and enables OIDC login checks.
Pass the result to oauth_client().
oauth_provider_oidc_discover( issuer, name = NULL, use_pkce = TRUE, use_nonce = TRUE, id_token_validation = TRUE, token_auth_style = NULL, id_token_allowed_algs = c("RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "Ed25519", "EdDSA"), allowed_token_types = c("Bearer"), jwks_host_issuer_match = TRUE, issuer_match = c("url", "host", "none"), ..., allowed_algs = NULL )oauth_provider_oidc_discover( issuer, name = NULL, use_pkce = TRUE, use_nonce = TRUE, id_token_validation = TRUE, token_auth_style = NULL, id_token_allowed_algs = c("RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "Ed25519", "EdDSA"), allowed_token_types = c("Bearer"), jwks_host_issuer_match = TRUE, issuer_match = c("url", "host", "none"), ..., allowed_algs = NULL )
issuer |
The OIDC issuer base URL (including scheme), e.g.,
"https://login.example.com". The standard discovery-document URL ending
in |
name |
Optional friendly provider name. Defaults to the issuer hostname |
use_pkce |
Logical, whether to use PKCE for this provider. Defaults to TRUE. Public clients require PKCE. Setting FALSE also prevents automatic selection of public-client authentication; a confidential-client method must be available or explicitly configured instead. |
use_nonce |
Logical, whether to use OIDC nonce. Defaults to TRUE |
id_token_validation |
Logical, whether to validate ID tokens automatically for this provider. Defaults to TRUE |
token_auth_style |
Authentication style for token requests: "header"
(client_secret_basic), "body" (client_secret_post), or "public"
(public client; send |
id_token_allowed_algs |
Character vector of allowed ID token signing algorithms. Defaults to a broad set of common algorithms, including RSA (RS*), ECDSA (ES*), Ed25519, and legacy EdDSA. If the discovery document advertises supported algorithms, the intersection of advertised and caller-provided algorithms is used to avoid runtime mismatches. If there's no overlap, discovery fails with a configuration error (no fallback). |
allowed_token_types |
Character vector of allowed token types for access tokens issued by this provider. Defaults to 'Bearer' |
jwks_host_issuer_match |
When TRUE (default), enforce that the JWKS host
discovered from the provider matches the issuer host exactly. For
providers that serve JWKS from a different host, set
|
issuer_match |
Character scalar controlling how strictly to validate the
discovery document's
Prefer |
... |
Additional fields passed to |
allowed_algs |
Compatibility alias for |
This function makes a network request. Call it once during app setup, outside
server(). Copy the issuer URL exactly from your provider's configuration,
including any trailing slash. A full discovery-document URL is also accepted.
Discovered token, UserInfo, introspection, and revocation endpoints are
copied into the provider when present. Discovering an introspection endpoint
does not itself require token introspection; set introspect = TRUE on
oauth_client() when login and refresh must perform that check.
Discovery describes what a service supports. Your app's registration may
require a particular token_auth_style, secret, or key; configure those to
match the registration. Automatic selection prefers "header", then "body",
then "public" when none is advertised and PKCE is enabled. For a public
client registration, set token_auth_style = "public" explicitly. JWT and
mTLS methods must also be selected explicitly.
Explicit choices must match the discovery metadata. If
token_endpoint_auth_methods_supported is omitted, the OIDC default is
client_secret_basic ("header"); other methods must be advertised.
OAuthProvider object configured from discovery
The discovered issuer must match the requested identifier by default.
Endpoints must use HTTPS. Host allowlisting does not permit HTTP: local OIDC
development requires options(shinyOAuth.allow_insecure_oidc_loopback = TRUE)
and a loopback host. options(shinyOAuth.allowed_hosts) can further restrict
endpoint hosts. Signing-key hosts have their own policy: by default they must
match the issuer host; use jwks_host_allow_only for a known different host.
The document must advertise the code flow (response_types_supported
includes "code"), non-empty subject_types_supported, RS256 in
id_token_signing_alg_values_supported, and a jwks_uri even when automatic
ID token validation is disabled. The permitted ID token algorithms are the
intersection of id_token_allowed_algs and the advertised algorithms; an empty
intersection is an error. Discovery keeps PKCE S256 and errors when the
provider explicitly excludes it, unless you select pkce_method = "plain".
Discovery also records capabilities for PAR, signed/encrypted requests,
JARM, DPoP, mTLS, and callback issuer identification. Client construction
checks the selected features against this metadata. See oauth_provider()
for individual fields and the advanced security vignette for setup.
When omitted by the provider, OIDC defaults apply: the JAR request
parameter is unsupported, request URIs are supported, request URI registration is not
required, and response modes are c("query", "fragment"). A mode still
has to be implemented by shinyOAuth to be usable. Caller-published request
URI registration must be arranged with the provider; discovery cannot check
your registration. PAR-issued handles do not need that client-hosted URI
registration.
# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }
Look up login settings for your Okta domain and authorization server.
Pass the result to oauth_client() with your registered app credentials.
This helper makes a discovery request during setup.
oauth_provider_okta(domain, auth_server = "default", name = "okta")oauth_provider_okta(domain, auth_server = "default", name = "okta")
domain |
Your Okta domain, e.g., "dev-123456.okta.com" |
auth_server |
Authorization server ID for a custom authorization
server (default "default"). Use |
name |
Optional provider name (default "okta") |
OAuthProvider object configured for the specified Okta domain
## Not run: oauth_provider_okta("dev-123456.okta.com") ## End(Not run)## Not run: oauth_provider_okta("dev-123456.okta.com") ## End(Not run)
Configure researcher sign-in with ORCID. Discovery selects ORCID's
client_secret_post authentication; pass a client secret to oauth_client().
oauth_provider_orcid(name = "orcid", sandbox = FALSE)oauth_provider_orcid(name = "orcid", sandbox = FALSE)
name |
Optional provider name (default |
sandbox |
Logical; use ORCID's sandbox instead of production. |
Request scopes = "openid" on the client. ORCID uses this instead of
/authenticate for OIDC login; do not request both. Its OIDC metadata does
not advertise profile or email scopes. Additional record permissions
depend on whether you registered a Public or Member API client.
Sandbox and production require separate credentials and user accounts. Production redirect URIs must use HTTPS. ORCID does not advertise PKCE in its discovery metadata, so this confidential-client preset uses client authentication and validated OIDC nonce without claiming PKCE protection. See the ORCID authentication guide.
OAuthProvider object configured for ORCID.
## Not run: oauth_provider_orcid() oauth_provider_orcid(sandbox = TRUE) ## End(Not run)## Not run: oauth_provider_orcid() oauth_provider_orcid(sandbox = TRUE) ## End(Not run)
Look up Slack's OpenID Connect settings for Sign in with Slack. This
helper contacts the discovery service during setup; pass its result to
oauth_client() with your Slack app credentials.
oauth_provider_slack( name = "slack", profile = c("confidential", "public_pkce") )oauth_provider_slack( name = "slack", profile = c("confidential", "public_pkce") )
name |
Optional provider name (default "slack") |
profile |
Slack app registration profile: |
OAuthProvider object configured for Slack
## Not run: oauth_provider_slack() ## End(Not run)## Not run: oauth_provider_slack() ## End(Not run)
Connect your app to a user's Spotify account. Pass this provider to
oauth_client() and request the scopes needed by the Spotify API calls you
plan to make. The helper configures profile retrieval through Spotify's API
and does not expect an ID token.
oauth_provider_spotify(name = "spotify", allow_legacy_id = FALSE)oauth_provider_spotify(name = "spotify", allow_legacy_id = FALSE)
name |
Optional provider name (default "spotify") |
allow_legacy_id |
Whether to fall back to Spotify's mutable |
Spotify requires scopes to be included in the authorization request.
Set requested scopes on the client with oauth_client(..., scopes = ...).
Identity uses Spotify's immutable account_id. Existing installations must
migrate stored account mappings and audit digests from id before upgrading.
Link the old and new identifiers only from a successfully authenticated
profile; do not use display names or email to merge accounts. To temporarily
preserve old mappings, explicitly replace provider@userinfo_id_selector
with function(userinfo) userinfo[["id"]] while completing the migration.
OAuthProvider object for use with a Spotify OAuth 2.0 app
For a Shiny app that connects to Spotify to display the user's listening data, see the Spotify example.
oauth_provider_spotify()oauth_provider_spotify()
Configure institutional OIDC login through SURFconext. Register your service
with SURFconext before using its client credentials with oauth_client().
oauth_provider_surfconext( environment = c("production", "test"), name = "surfconext", token_auth_style = NULL )oauth_provider_surfconext( environment = c("production", "test"), name = "surfconext", token_auth_style = NULL )
environment |
SURFconext environment: |
name |
Optional provider name (default |
token_auth_style |
Authentication style for token requests: "header"
(client_secret_basic), "body" (client_secret_post), or "public"
(public client; send |
Request the "openid" scope. Additional claims depend on your service's
agreed attribute release policy; requesting "email" or "profile" does
not guarantee those attributes will be released. Use the validated issuer
and sub to identify a user, rather than assuming email is present.
Test and production environments require their own service configuration.
See SURFconext for service providers.
OAuthProvider object configured for SURFconext.
## Not run: oauth_provider_surfconext() oauth_provider_surfconext(environment = "test") ## End(Not run)## Not run: oauth_provider_surfconext() oauth_provider_surfconext(environment = "test") ## End(Not run)
Wrap your complete UI in oauth_ui() when using oauth_module_server().
It adds the browser code needed for login and protects callback responses
from caching and referrer disclosure. Supply id and client to accept
query callbacks before rendering any application UI or scripts.
oauth_ui( base_ui, id = NULL, client = NULL, request_uri_resolver = NULL, clients = NULL )oauth_ui( base_ui, id = NULL, client = NULL, request_uri_resolver = NULL, clients = NULL )
base_ui |
Your app's complete UI, such as a |
id |
Shiny module ID, required with |
client |
OAuthClient used by the server module, required with |
request_uri_resolver |
Optional trusted public request URI resolver;
see |
clients |
Optional named list of OAuthClient objects keyed by module
ID, mutually exclusive with |
Build the page as usual, for example with fluidPage(), then use
ui <- oauth_ui(ui, id = "auth", client = client), using the same module
ID and client as the server. Pass the result to shiny::shinyApp(). UI functions
are supported too, including functions accepting the Shiny request.
This is the recommended setup for ordinary query-callback apps. It includes
use_shinyOAuth() setup; do not add a separate call inside the wrapped UI.
With client, it also serves client-hosted Request Objects at the app root
using independent, single-use handles. Shared-worker apps need a shared
client@state_store with atomic take(); a memory store supports one process.
For response_mode = "form_post" or "form_post.jwt", use
oauth_form_post_ui() instead; it includes this setup and accepts POST
callbacks.
GET callbacks are validated and sealed into short-lived, single-use bridge
handles in the client's state store, then redirected to a clean URL before
application HTML is rendered. Logical state is consumed only after the
Shiny module verifies browser binding. The storage requirements and quotas
are the same as oauth_form_post_ui(). Register any fixed application query
parameters in client@redirect_uri; other inbound parameters are discarded.
For non-root callback paths use uiPattern = ".*" in shiny::shinyApp().
For multiple providers, supply clients = list(auth_a = client_a, auth_b = client_b) instead of id and client. Names are the server module
IDs. The registry accepts query and form-post callbacks on configured routes.
Each client must select a multi-server defense. Shared routes require
authorization_server_mode = "multi_issuer" and distinct trusted issuers.
An RFC 9207 iss or signed JARM issuer selects the configured client; the
complete callback is then verified before a bridge handle is stored.
Encrypted JARM on a shared route requires an outer iss identifying one
distinct configured issuer. The decrypted, signed response must match it.
Do not nest wrappers to route multiple providers.
Without id and client, ordinary pages still render, but raw OAuth GET
callbacks fail closed with a setup error. Earlier oauth_ui(ui) query-flow
applications must add those arguments. When integrating use_shinyOAuth()
directly, provide an equivalent dedicated callback endpoint: third-party or
application scripts must not execute on an unsanitized callback page.
HTML responses include Cache-Control: no-store, Pragma: no-cache, and
Referrer-Policy: no-referrer. The browser reads these headers
before loading page resources. The meta tag from use_shinyOAuth() takes
effect only once the browser reads that tag, so it may miss early resource
requests. You can also set the same HTTP header at your web server.
A UI function to use as the ui argument to shiny::shinyApp().
oauth_module_server(), oauth_form_post_ui(), use_shinyOAuth()
base_ui <- shiny::fluidPage( shiny::h2("My app"), shiny::uiOutput("login") ) # After creating your OAuth client, wrap the UI once with callback handling: # ui <- oauth_ui(base_ui, id = "auth", client = client) # Use this UI with your app's server function: # shiny::shinyApp(ui = ui, server = server)base_ui <- shiny::fluidPage( shiny::h2("My app"), shiny::uiOutput("login") ) # After creating your OAuth client, wrap the UI once with callback handling: # ui <- oauth_ui(base_ui, id = "auth", client = client) # Use this UI with your app's server function: # shiny::shinyApp(ui = ui, server = server)
An OAuthClient holds your app's registration with a provider: its client ID,
credentials, return address, and requested permissions. It also holds the
pending login state and client-specific token validation settings used by
the Shiny module and token helpers. Create it with oauth_client(), which
resolves defaults from the provider and the supplied client settings.
OAuthClient( provider = NULL, client_id = character(0), client_secret = character(0), client_assertion_private_key = NULL, client_assertion_private_key_kid = NA_character_, client_assertion_alg = NA_character_, client_assertion_audience = NA_character_, mtls_client_cert_file = NA_character_, mtls_client_key_file = NA_character_, mtls_client_key_password = NA_character_, mtls_client_ca_file = NA_character_, mtls_certificate_bound_access_tokens = FALSE, request_object_mode = "parameters", response_mode = NA_character_, request_object_signing_alg = NA_character_, request_object_audience = NA_character_, request_object_encryption_alg = NA_character_, request_object_encryption_enc = NA_character_, request_object_encryption_kid = NA_character_, request_object_ttl = 45, request_object_nbf_skew = NA_real_, dpop_private_key = NULL, dpop_private_key_kid = NA_character_, dpop_signing_alg = NA_character_, dpop_require_access_token = !is.null(dpop_private_key), redirect_uri = character(0), enforce_callback_issuer = FALSE, scopes = character(0), resource = character(0), claims = NULL, state_store = cachem::cache_mem(max_age = 300), state_payload_max_age = 300, state_entropy = 64, state_key = random_urlsafe(n = 128), scope_validation = "warn", claims_validation = "none", userinfo_jwt_required_time_claims = character(0), required_acr_values = character(0), introspect = FALSE, introspection_checks = character(0), endpoint_auth = list(), token_targets = list(), default_token_target = character(0), authorization_server_mode = "single", authorization_server_redirect_uris = character(0), dpop_require_observed_cnf = FALSE, jarm_signed_response_alg = NA_character_, jarm_encrypted_response_alg = NA_character_, jarm_encrypted_response_enc = NA_character_, jarm_decryption_private_key = NULL, jarm_decryption_private_key_kid = NA_character_, jarm_max_lifetime = 600, mtls_require_observed_cnf = TRUE, trusted_id_token_audiences = character(0), compare_callback_issuer = is_valid_string(provider@issuer) && (missing(enforce_callback_issuer) || isTRUE(enforce_callback_issuer)), client_assertion_typ = "JWT", resource_bases = character(0), required_scopes = character(0), label = default_client_label(provider), authorization_method = "GET", scope_policy = list(), smart = list(), introspect_elements = NULL, client_private_key = NULL, client_private_key_kid = NULL, userinfo_jwt_required_temporal_claims = NULL, mtls_request_certificate_bound_access_tokens = NULL, tls_client_cert_file = NULL, tls_client_key_file = NULL, tls_client_key_password = NULL, tls_client_ca_file = NULL, authorization_request_mode = NULL, authorization_request_signing_alg = NULL, authorization_request_audience = NULL, authorization_request_encryption_alg = NULL, authorization_request_encryption_enc = NULL, authorization_request_encryption_kid = NULL, authorization_request_ttl = NULL, authorization_request_nbf_skew = NULL )OAuthClient( provider = NULL, client_id = character(0), client_secret = character(0), client_assertion_private_key = NULL, client_assertion_private_key_kid = NA_character_, client_assertion_alg = NA_character_, client_assertion_audience = NA_character_, mtls_client_cert_file = NA_character_, mtls_client_key_file = NA_character_, mtls_client_key_password = NA_character_, mtls_client_ca_file = NA_character_, mtls_certificate_bound_access_tokens = FALSE, request_object_mode = "parameters", response_mode = NA_character_, request_object_signing_alg = NA_character_, request_object_audience = NA_character_, request_object_encryption_alg = NA_character_, request_object_encryption_enc = NA_character_, request_object_encryption_kid = NA_character_, request_object_ttl = 45, request_object_nbf_skew = NA_real_, dpop_private_key = NULL, dpop_private_key_kid = NA_character_, dpop_signing_alg = NA_character_, dpop_require_access_token = !is.null(dpop_private_key), redirect_uri = character(0), enforce_callback_issuer = FALSE, scopes = character(0), resource = character(0), claims = NULL, state_store = cachem::cache_mem(max_age = 300), state_payload_max_age = 300, state_entropy = 64, state_key = random_urlsafe(n = 128), scope_validation = "warn", claims_validation = "none", userinfo_jwt_required_time_claims = character(0), required_acr_values = character(0), introspect = FALSE, introspection_checks = character(0), endpoint_auth = list(), token_targets = list(), default_token_target = character(0), authorization_server_mode = "single", authorization_server_redirect_uris = character(0), dpop_require_observed_cnf = FALSE, jarm_signed_response_alg = NA_character_, jarm_encrypted_response_alg = NA_character_, jarm_encrypted_response_enc = NA_character_, jarm_decryption_private_key = NULL, jarm_decryption_private_key_kid = NA_character_, jarm_max_lifetime = 600, mtls_require_observed_cnf = TRUE, trusted_id_token_audiences = character(0), compare_callback_issuer = is_valid_string(provider@issuer) && (missing(enforce_callback_issuer) || isTRUE(enforce_callback_issuer)), client_assertion_typ = "JWT", resource_bases = character(0), required_scopes = character(0), label = default_client_label(provider), authorization_method = "GET", scope_policy = list(), smart = list(), introspect_elements = NULL, client_private_key = NULL, client_private_key_kid = NULL, userinfo_jwt_required_temporal_claims = NULL, mtls_request_certificate_bound_access_tokens = NULL, tls_client_cert_file = NULL, tls_client_key_file = NULL, tls_client_key_password = NULL, tls_client_ca_file = NULL, authorization_request_mode = NULL, authorization_request_signing_alg = NULL, authorization_request_audience = NULL, authorization_request_encryption_alg = NULL, authorization_request_encryption_enc = NULL, authorization_request_encryption_kid = NULL, authorization_request_ttl = NULL, authorization_request_nbf_skew = NULL )
provider |
The service configuration, created with a provider helper such as
|
client_id |
The identifier assigned when you register your app with the provider. |
client_secret |
The secret issued for your app, preferably read with
It is required for |
client_assertion_private_key |
Optional private key for |
client_assertion_private_key_kid |
Optional key identifier (kid) to include in the JWT header
for |
client_assertion_alg |
Optional JWT signing algorithm to use for client assertions.
When omitted, defaults to |
client_assertion_audience |
Optional override for the |
mtls_client_cert_file |
Optional path to the PEM-encoded client
certificate (or certificate chain) used for RFC 8705 mutual TLS (mTLS) client
authentication and certificate-bound protected-resource requests. Required
when |
mtls_client_key_file |
Optional path to the PEM-encoded private key used
with |
mtls_client_key_password |
Optional password used to decrypt an encrypted
PEM private key referenced by |
mtls_client_ca_file |
Optional path to a PEM CA bundle used to validate the remote HTTPS server certificate when making mTLS requests. This is mainly useful for local or test environments that use self-signed server certificates. |
mtls_certificate_bound_access_tokens |
Logical. Whether this
client intends to request RFC 8705 certificate-bound access tokens when
the provider advertises that capability. Default is Set this to Requires |
request_object_mode |
Controls how the authorization request is transported to the provider.
If the provider has a Use a signed Request Object when the provider requires JAR or when it must
verify the integrity of the authorization parameters. |
response_mode |
How the provider returns the login result. Leave Signed responses (JWT Secured Authorization Response Mode, JARM)
use |
request_object_signing_alg |
Optional JWS algorithm override for
signed authorization requests when |
request_object_audience |
Optional override for the |
request_object_encryption_alg |
Optional JWE key-management
algorithm override for encrypted Request Objects. Current outbound support
is limited to |
request_object_encryption_enc |
Optional JWE content-encryption
algorithm override for encrypted Request Objects. Current outbound support
is limited to the AES-CBC-HMAC family ( |
request_object_encryption_kid |
Optional key identifier ( |
request_object_ttl |
Positive number of seconds to keep signed
authorization request objects ( |
request_object_nbf_skew |
Optional non-negative number of
seconds. When provided, shinyOAuth adds an |
dpop_private_key |
Private key for tying tokens to this app's requests
using Demonstrating Proof of Possession (DPoP). Only needed when your
provider/API supports DPoP. Accepts an
|
dpop_private_key_kid |
Optional key identifier ( |
dpop_signing_alg |
Optional JWT signing algorithm to use for DPoP
proofs. When omitted, a compatible asymmetric default is selected based on
the private key type/curve (for example |
dpop_require_access_token |
Logical or |
redirect_uri |
The URL where users return after login. It must match the callback URL registered with your provider, including scheme, host, port, and path. Use HTTPS in production. |
enforce_callback_issuer |
Logical or When |
scopes |
Character vector of permissions to request. The provider defines
the available names. For OIDC ( |
resource |
Optional RFC 8707 resource indicator(s). Supply a character
vector of absolute URIs to request audience-restricted tokens for one or
more protected resources. Each value is sent as a repeated |
claims |
Optional request for specific OIDC user information, beyond scopes.
Default Lists are JSON-encoded with |
state_store |
Storage for pending logins. The default
|
state_payload_max_age |
Maximum age of a pending login's encrypted state, in seconds. Default 300. This is checked separately from the state store's entry lifetime; both must allow the returning login. |
state_entropy |
Length in characters of the random state identifier, from 22 to 128. Default 64. Most apps should keep the default. |
state_key |
Secret used to encrypt and protect pending login details.
A random key is generated when omitted. This is separate from
For multiple R processes, supply the same key and shared |
scope_validation |
Controls how scope discrepancies are handled when
the authorization server grants fewer scopes than requested. RFC 6749
Section 3.3 permits servers to issue tokens with reduced scope, and
Section 5.1 allows token responses to omit
|
claims_validation |
What to do if requested claims are missing or have
unexpected values: |
userinfo_jwt_required_time_claims |
Optional character vector of
temporal JWT claims that must be present when the UserInfo response is a
signed JWT ( Default is |
required_acr_values |
Optional character vector of acceptable login
requirements, such as a provider's multi-factor authentication (MFA) policy.
Use the provider's Authentication Context Class Reference (ACR) identifiers.
The validated ID token must contain a matching |
introspect |
If |
introspection_checks |
Optional character vector of additional
requirements to enforce on the introspection response when
|
endpoint_auth |
Named list of authentication overrides for |
token_targets |
Optional named list of API token declarations. Each
entry has |
default_token_target |
Name of the initial code-redemption target and
the default for connection methods. Required with multiple targets;
|
authorization_server_mode |
Declares whether this client is part of an application that can interact with more than one authorization server, and which RFC 9700 mix-up defense it uses. One of:
|
authorization_server_redirect_uris |
Complete character vector of
redirect URIs used by the application for its authorization servers when
|
dpop_require_observed_cnf |
Logical. When |
jarm_signed_response_alg |
Optional expected JWS algorithm for
signed JWT Secured Authorization Responses (JARM). When omitted and the
effective response mode is JARM, shinyOAuth defaults to |
jarm_encrypted_response_alg |
Optional expected JWE
key-management algorithm for encrypted JARM responses. Current inbound
support is limited to |
jarm_encrypted_response_enc |
Optional expected JWE
content-encryption algorithm for encrypted JARM responses. Current inbound
support is limited to the AES-CBC-HMAC family ( |
jarm_decryption_private_key |
Optional private key
used to decrypt encrypted JARM responses. Can be an |
jarm_decryption_private_key_kid |
Optional key
identifier ( |
jarm_max_lifetime |
Positive number of seconds. Maximum accepted
lifetime for a JARM response JWT. Default is 600 seconds, matching JARM's
recommended 10-minute upper bound for authorization response JWTs. When a
JARM payload includes |
mtls_require_observed_cnf |
Logical, default |
trusted_id_token_audiences |
Character vector of additional ID-token
audiences explicitly trusted by this client. Defaults to |
compare_callback_issuer |
Logical or |
client_assertion_typ |
JWT header |
resource_bases |
Optional named character vector of approved API base
URLs for |
required_scopes |
Optional requested scopes that every usable connection
needs, default |
label |
Optional display label used in connection summaries; defaults to
the provider name, with control characters replaced by spaces and shortened
to 128 UTF-8 bytes if needed. If the provider name is empty, missing or not a
single string, the default is |
authorization_method |
Browser method for sending the authorization
request: |
scope_policy |
Internal versioned scope policy. Leave the default for
ordinary OAuth clients. SMART adapters install their own policy, including
required permissions; these checks cannot be disabled by |
smart |
Internal SMART configuration installed by |
introspect_elements |
Compatibility alias for |
client_private_key |
Compatibility alias for |
client_private_key_kid |
Compatibility alias for |
userinfo_jwt_required_temporal_claims |
Compatibility alias for |
mtls_request_certificate_bound_access_tokens |
Compatibility alias for |
tls_client_cert_file |
Compatibility alias for |
tls_client_key_file |
Compatibility alias for |
tls_client_key_password |
Compatibility alias for |
tls_client_ca_file |
Compatibility alias for |
authorization_request_mode |
Compatibility alias for |
authorization_request_signing_alg |
Compatibility alias for |
authorization_request_audience |
Compatibility alias for |
authorization_request_encryption_alg |
Compatibility alias for |
authorization_request_encryption_enc |
Compatibility alias for |
authorization_request_encryption_kid |
Compatibility alias for |
authorization_request_ttl |
Compatibility alias for |
authorization_request_nbf_skew |
Compatibility alias for |
Configure the app registration with provider, client_id, client_secret
(if issued), redirect_uri, and scopes. Create the client outside your Shiny
server() function, then pass it to oauth_module_server().
Use the state-store settings for deployments where callbacks can reach
different R processes, and the validation settings to enforce required
scopes, claims, or authentication context. Certificate (mTLS), key-binding
(DPoP), and signed-request/response (JAR/JARM) settings enable those protocol
features when supported by your provider and required by your deployment. See
the advanced security vignette for examples. The defaults described below
refer to oauth_client() unless stated otherwise.
Calling the constructor creates an OAuthClient object.
# Register an app with GitHub and store its credentials in your environment. # This creates the configuration; it does not start login or contact GitHub. client <- oauth_client( provider = oauth_provider_github(), client_id = "your-client-id", client_secret = "your-client-secret", redirect_uri = "http://127.0.0.1:8100", scopes = c("read:user", "user:email") ) # In a real app, read credentials with Sys.getenv() and create client # outside server(). Inside server(), start login with: # auth <- oauth_module_server("auth", client)# Register an app with GitHub and store its credentials in your environment. # This creates the configuration; it does not start login or contact GitHub. client <- oauth_client( provider = oauth_provider_github(), client_id = "your-client-id", client_secret = "your-client-secret", redirect_uri = "http://127.0.0.1:8100", scopes = c("read:user", "user:email") ) # In a real app, read credentials with Sys.getenv() and create client # outside server(). Inside server(), start login with: # auth <- oauth_module_server("auth", client)
Make API requests using a Shiny session's current OAuth credentials and the
client/API configuration supplied by an OAuthClient. For example, a hospital
connection selects that hospital's API address and reads the session's current
token for each request. Obtain it inside server() with auth$connection()
from oauth_module_server(), or auth$connection(id) from
oauth_connections_server(). Use $access_token() for external SDKs,
$request() for an approved API and $summary() for status without credentials.
The existing reactive token already updates on refresh; this object
combines that lookup with client selection, API-address restrictions and
session checks. With oauth_connection(), the application supplies the
matching module's token source; the manager resolves its own stored records.
These are optional shinyOAuth conveniences, not SMART on FHIR protocol objects.
Module factories pin each reference to one authorization. Refresh preserves
it; logout or replacement invalidates it permanently. Both factories supply
coordinated $access_token() and $refresh() methods. Every reference expires
when its Shiny
session closes. A manager can retain the underlying grant across redirects;
a new session obtains a new reference after verifying the local owner.
The older oauth_connection() wrapper follows its supplied token reactive
across logins and does not provide coordinated token acquisition or refresh.
Factory, $access_token() and $has_scopes() dependencies reflect
authorization/permission changes without invalidating consumers on unchanged
token rotations. Explicit status reads may update on each refresh. A connection
for an external SDK need not declare resource_bases; $request() still
requires a declared destination. See vignette("external-integrations").
Call [["is_usable"]](), [["summary"]]() and [["request"]]() in the owning session's
reactive context. If the connection cannot be resolved, [["is_usable"]]() returns
FALSE; [["summary"]]() and [["request"]]() raise an error. The ID is read-only and
cloning is disabled.
Managed resource and status reads do not count as owner activity. Record user
actions with the manager's touch() method in an input event handler; automatic
reactive updates must not prolong an idle owner's session.
idRead-only opaque character string identifying this reference.
A single module preserves it across refresh and replaces it on login.
A manager uses the stored grant's ID across sessions and refreshes;
oauth_connection() generates an ID lasting only for that reference.
The ID is never an access token and does not authorize access by itself.
OAuthConnection$new()Initialize a reference. This constructor is for internal use;
applications should use a module's connection() method or the legacy
oauth_connection() wrapper to establish session ownership.
OAuthConnection$new( id, client, resolve, refresh = NULL, acquire = NULL, is_current = NULL )
idOpaque character string identifying the reference.
clientThe OAuthClient to bind to this reference.
resolveInternal function with no arguments that enforces session
ownership and returns a list with client identical to this reference's
client and token containing the current OAuthToken or NULL.
It must raise an error when the owning session is unavailable.
refreshOptional internal function implementing the owner's
coordinated refresh. Legacy wrappers leave this NULL.
acquireInternal coordinated credential refresh for external use,
accepting async and returning TRUE or a promise resolving to TRUE.
is_currentOptional internal reactive predicate. FALSE permanently ends permission notifications for this authorization reference.
A new OAuthConnection instance.
OAuthConnection$access_token()Retrieve a bearer-token string for an external SDK or database driver.
Credentials are checked at use time and refreshed once when necessary.
Requires a reference from a module's connection() method. The method
never starts login, widens permissions, or retries an application request.
Keep the returned secret on the server and out of logs.
OAuthConnection$access_token( required_scopes = character(), min_valid_for = 60, force_refresh = FALSE, async = FALSE, target = NULL )
required_scopesOperation scopes, in addition to client requirements. These check permissions; they do not narrow the grant or add consent.
min_valid_forMinimum remaining token lifetime in seconds, including the caller's allowance for clock skew and operation duration. Unknown expiry requires refresh; an insufficient replacement fails without a loop.
force_refreshBypass the cached token and require acquisition. Existing refresh coordination and retry delays still apply.
asyncReturn a promise, including for a cached token. Authentication failures reject the promise. FALSE always returns a string or errors; it never waits on an in-flight asynchronous refresh.
targetOptional declared token-target name. NULL uses the client's
default. A missing cached target is acquired with the shared refresh
credential; scope checks use that target's evidence and retained limit.
Reasons are authorization_unavailable (ended or foreign session/reference),
insufficient_scope, refresh_pending (a synchronous caller cannot join
async work), refresh_unavailable (including retry pacing),
interaction_required (no usable refresh credential), lifetime_unavailable,
unsupported_token_binding, unknown_target and unsupported_target.
No reason initiates browser navigation.
A cached async result is still a promise. Pending async callers join the
same owned refresh and recheck the committed result before returning it.
Acquisition does not count as managed owner activity.
Microsoft tokens can contain previously consented permissions beyond the
configured operation scopes. This method returns the actual bearer token;
it cannot reduce those permissions for an SDK. Connection permission checks
still enforce the configured and retained local limits.
A bearer-token string, or a promise resolving to one. Acquisition
failures inherit shinyOAuth_access_error and expose a stable reason in
condition$context$reason. Argument/configuration errors can be synchronous.
Sender-constrained tokens must use their transport instead of this method.
OAuthConnection$has_scopes()Check recorded operation permissions without refreshing or requiring an unexpired access token. This is suitable for optional-feature UI; the actual operation must still check scopes and remote resource permissions.
OAuthConnection$has_scopes(scopes, target = NULL)
scopesCharacter vector of operation scopes to check.
targetOptional declared token-target name; defaults to the client's primary target. An unacquired target returns FALSE, without acquisition.
TRUE when the current authorization covers the configured scopes, otherwise FALSE. Malformed arguments raise input errors.
OAuthConnection$is_usable()Check whether the current token is locally usable. This checks token
presence, known unexpired lifetime and the client's required scopes.
It does not refresh the token, contact the provider or guarantee remote
authorization. Request-specific scopes are checked by [["request"]]().
OAuthConnection$is_usable()
A single logical value: TRUE for an active or limited
connection, otherwise FALSE, including when resolution fails.
OAuthConnection$refresh()Refresh a connection obtained from either module's connection() method.
Its owner coordinates refresh and verifies ownership before updating it.
References created with oauth_connection() use their existing module's
refresh lifecycle and cannot invoke this method.
OAuthConnection$refresh(scopes = NULL, target = NULL)
scopesOptional non-empty character vector requesting fewer
permissions for this connection, or NULL (default). Scopes must be
covered by the current grant and client configuration, and retain the
client's required scopes. SMART clients use semantic coverage.
targetOptional declared token-target name. With targets, narrowing applies only to the selected entry and persists across target switching and reauthorization. Other targets keep their own retained scope limits.
After explicit narrowing succeeds, subsequent refreshes (including
automatic refreshes and refreshes in another retained Shiny session)
request the accepted scope limit. Widening requires a new authorization.
This is a local connection policy: OAuth refresh-token scope itself is
not reduced by requesting a narrower access token. Ordinary OAuth
connections explicitly request their retained granted scopes when known,
including when no explicit narrowing was selected. SMART omits request
scope while its permissions equal the original launch grant.
Providers may reject requested scopes; there is no retry without them.
Microsoft target API scopes cannot be explicitly narrowed: Entra may
return all previously consented resource permissions. Such requests fail
before HTTP with access-error reason unsupported_scope_narrowing, leaving
the current authorization usable. Use a separate registration or change
provider consent when a token with fewer API permissions is required.
OIDC clients that require UserInfo must retain openid; narrowing that
removes it is rejected before exchange. Include any additional scopes
needed by the provider's profile endpoint in the client's required_scopes.
TRUE after a successful commit, or a promise resolving to TRUE
when the module uses async transport. Failure raises a redacted error.
OAuthConnection$summary()Resolve the current connection and return status information without credentials, identity claims or token extension fields. Raises an error when called outside the owning session or after that session closes.
OAuthConnection$summary()
Managed lifecycle states take precedence: refreshing means a refresh
claim is in progress, uncertain requires a new authorization after an
ambiguous refresh outcome, disconnected means local access was removed,
and unavailable means the stored credentials could not be restored.
Otherwise token status is evaluated in this order:
disconnected: there is no current token.
expiry_unknown: the token's expiry is unknown.
expired: the token has reached its expiry time.
insufficient_scope: the grant lacks a client-required scope.
limited: required scopes are covered, but some other requested scopes
are absent from the grant.
active: all requested scopes are covered.
Scope checks enforce retained target limits and use the token's current
granted_scopes, which may be
assumed or carried forward when an ordinary OAuth provider omits scope
information. SMART clients require explicit evidence and use semantic
coverage for both connection and operation permissions.
See OAuthToken for the distinction from verified scope evidence.
A named list with the following entries:
connection_id: the reference's character ID.
client_label: the client's application-defined character label.
status: one of the character values listed in this method's details.
expires_at: numeric seconds since the Unix epoch, NA_real_ when
there is no token or its expiry is unknown, or Inf for a
non-expiring token.
resource_ids: character vector of the client's approved resource IDs.
replaces_connection_id: present on managed replacement authorizations;
identifies the locally ended connection passed to reauthorize().
OAuthConnection$identity()Read explicitly selected fields from the authorization's cryptographically validated OIDC identity. This method never returns raw tokens or fetches profile data.
OAuthConnection$identity(claims = c("iss", "sub"), userinfo = character())
claimsCharacter vector of ID-token claim names, defaulting to
c("iss", "sub"). Use character() to select none.
userinfoCharacter vector of previously fetched UserInfo field
names, defaulting to none. UserInfo must have a sub exactly matching
the validated ID token before any requested profile fields are returned.
Call inside the owning session's reactive context. The result contains
sensitive identity data: select only what the application needs and keep
it out of logs and generic status displays. [["summary"]]() and printing
continue to omit identity. Ordinary OAuth connections without validated
OIDC identity cannot use this accessor.
Ordinary connections require a usable access token with openid currently
granted. Target connections retain their validated identity while the
authorization remains available in the owning session, even when the
primary access token expires or its current scopes no longer include
openid. Identity availability does not establish permission to call an
API; use $has_scopes() and $access_token() or $request() to check the
selected target's current permissions and token lifetime.
These are the last validated identity/profile snapshots; an OAuth refresh can retain earlier ID-token claims and does not establish fresh user authentication. This accessor does not log the user into your application or establish an account-retention owner. It does not count as owner activity.
A list with id_token_claims and userinfo, each containing only
selected fields that exist. Missing fields are omitted.
OAuthConnection$request()Resolve the current token and perform an authenticated request within a named resource base. The connection must be usable, and its current grant must cover any scopes required for this operation.
OAuthConnection$request( resource_id, path = "", query = NULL, method = "GET", required_scopes = character(), configure = NULL, refresh = FALSE, target = NULL, min_valid_for = 60 )
resource_idSingle character string naming an entry in the client's
resource_bases.
pathSingle character string resolved relative to the selected base
directory; "" selects the base itself. Absolute and root-relative URLs,
including pagination links, must remain within the same approved origin
and base path. Dot segments and ambiguous encodings are rejected.
queryOptional named list of query parameters, or NULL.
methodSingle HTTP method string, defaulting to "GET". TRACE
and TRACK are rejected by the resource transport.
required_scopesCharacter vector of scopes required for this
operation, in addition to the client's required scopes. They must have
been requested by the client and be covered by the current grant.
character() adds no operation-specific scope check.
configureOptional function taking an unauthenticated httr2::request()
and returning it with only body and application headers changed. Use
httr2::req_body_json(), httr2::req_body_form(), httr2::req_body_raw()
and httr2::req_headers(). Set the HTTP method with method above.
URL, transport policies, authentication and Host headers cannot be changed.
Configuration runs once. Both refresh modes recheck the current
authorization afterward, before sending the request.
refreshOpt in to coordinated token acquisition before sending the
request. Default FALSE preserves existing behavior. This synchronous
operation never waits on async refresh and never refreshes/replays after
an API failure. Generic HTTP retries are disabled for this opt-in call;
bound transport and DPoP nonce-challenge handling remain in force.
The destination, method, query and configure result are validated
before acquiring tokens. configure runs once; authorization is checked
again afterward and after any acquisition, before sending the request.
targetOptional declared token-target name. Target clients require
an explicit association through that declaration's resource_ids.
min_valid_forMinimum remaining lifetime when refresh = TRUE,
in seconds. Has the same meaning as on $access_token().
Uses perform_resource_req() with the configured client for Bearer, DPoP
and mTLS authentication. Redirects are never followed. Transport error
messages are redacted to exclude resource paths, queries and response
bodies. Scope requirements are supplied by the application; they cannot
be inferred from an arbitrary API's HTTP method and path.
An httr2 response object. Invalid resources, unusable connections, insufficient scopes and transport failures raise errors.
OAuthConnection$smart_context()Read interpreted context for a usable SMART connection in this session.
OAuthConnection$smart_context()
The sensitive context list documented in smart_context().
OAuthConnection$targets()Inspect declared targets without exposing credentials.
OAuthConnection$targets()
A named list of target status and granted scopes. A target with no
response yet has status not_acquired; this is not proof of denied consent.
OAuthConnection$smart_resource()Fetch the contextual Patient or validated fhirUser through the approved
FHIR base, using current read permissions. Prefer smart_patient() and
smart_fhir_user() in application code.
OAuthConnection$smart_resource(kind)
kindEither "patient" or "fhirUser".
An httr2 response. Missing context, scope or resource binding raises an error before an authenticated request is sent.
OAuthConnection$print()Print the class name and session-binding description, with credentials redacted. This does not resolve the current token.
OAuthConnection$print(...)
...Unused; accepted for compatibility with base::print().
This reference, invisibly.
oauth_connection(), OAuthClient, perform_resource_req()
## Not run: # Configure outside server(), using an existing provider: client <- oauth_client( provider, client_id = "registered-app", redirect_uri = "https://app.example/callback", scopes = c("read", "write"), resource_bases = c(api = "https://api.example/v1"), required_scopes = "read" ) server <- function(input, output, session) { auth <- oauth_module_server("auth", client, refresh_proactively = TRUE) output[["status"]] <- shiny::renderText({ connection <- shiny::req(auth[["connection"]]()) connection[["summary"]]()[["status"]] }) records <- shiny::reactive({ connection <- shiny::req(auth[["connection"]]()) response <- httr2::request("https://api.example/v1/records") |> httr2::req_auth_bearer_token(connection[["access_token"]]("read")) |> httr2::req_perform() httr2::resp_body_json(response) }) } ## End(Not run)## Not run: # Configure outside server(), using an existing provider: client <- oauth_client( provider, client_id = "registered-app", redirect_uri = "https://app.example/callback", scopes = c("read", "write"), resource_bases = c(api = "https://api.example/v1"), required_scopes = "read" ) server <- function(input, output, session) { auth <- oauth_module_server("auth", client, refresh_proactively = TRUE) output[["status"]] <- shiny::renderText({ connection <- shiny::req(auth[["connection"]]()) connection[["summary"]]()[["status"]] }) records <- shiny::reactive({ connection <- shiny::req(auth[["connection"]]()) response <- httr2::request("https://api.example/v1/records") |> httr2::req_auth_bearer_token(connection[["access_token"]]("read")) |> httr2::req_perform() httr2::resp_body_json(response) }) } ## End(Not run)
An OAuthProvider describes a service such as Google or GitHub: where users
sign in, where your app requests tokens, and which checks to perform.
Start with a provider helper such as oauth_provider_google() or
oauth_provider_oidc_discover(). Use oauth_provider() for manual setup;
these functions return an instance of this class with the corresponding
endpoint and validation settings.
OAuthProvider( name = character(0), auth_url = character(0), token_url = character(0), userinfo_url = NA_character_, introspection_url = NA_character_, revocation_url = NA_character_, par_url = NA_character_, par_required = FALSE, authorization_request_front_channel_mode = "compat", request_object_signing_alg_values_supported = character(0), request_object_encryption_alg_values_supported = character(0), request_object_encryption_enc_values_supported = character(0), request_object_encryption_jwk = NULL, signed_request_object_required = FALSE, request_parameter_supported = NA, request_uri_parameter_supported = NA, request_uri_registration_required = NA, token_endpoint_auth_signing_alg_values_supported = character(0), dpop_signing_alg_values_supported = character(0), authorization_response_iss_parameter_supported = FALSE, response_modes_supported = character(0), issuer = NA_character_, issuer_match = "url", use_nonce = is_valid_string(issuer) && isTRUE(infer_oidc_from_issuer), use_pkce = TRUE, pkce_method = "S256", userinfo_required = FALSE, userinfo_id_selector = function(userinfo) userinfo[["sub"]], userinfo_id_token_match = is_valid_string(userinfo_url) && is_ok_host(userinfo_url) && (isTRUE(id_token_validation) || isTRUE(use_nonce)), userinfo_signed_jwt_required = FALSE, id_token_required = is_valid_string(issuer) && isTRUE(infer_oidc_from_issuer), id_token_validation = is_valid_string(issuer) && isTRUE(infer_oidc_from_issuer), id_token_at_hash_required = FALSE, extra_auth_params = list(), extra_token_params = list(), extra_token_headers = character(0), mtls_endpoint_aliases = list(), mtls_client_certificate_bound_access_tokens = FALSE, token_auth_style = "header", jwks_cache = cachem::cache_mem(max_age = 3600), jwks_pins = character(0), jwks_pin_mode = "any", jwks_host_issuer_match = is_valid_string(issuer) && (isTRUE(id_token_validation) || isTRUE(id_token_required)), jwks_host_allow_only = NA_character_, id_token_allowed_algs = c("RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "Ed25519", "EdDSA"), allowed_token_types = "Bearer", leeway = getOption("shinyOAuth.leeway", 30), token_target_mode = "none", infer_oidc_from_issuer = TRUE, jwks_uri = NA_character_, userinfo_allowed_algs = NULL, token_response_scope_format = "space", allow_missing_token_type = FALSE, jarm_signing_alg_values_supported = character(0), jarm_encryption_alg_values_supported = character(0), jarm_encryption_enc_values_supported = character(0), jarm_tolerate_duplicate_top_level_iss = FALSE, endpoint_auth_metadata = list(), allowed_algs = NULL, require_pushed_authorization_requests = NULL, require_signed_request_object = NULL, require_request_uri_registration = NULL, tls_client_certificate_bound_access_tokens = NULL )OAuthProvider( name = character(0), auth_url = character(0), token_url = character(0), userinfo_url = NA_character_, introspection_url = NA_character_, revocation_url = NA_character_, par_url = NA_character_, par_required = FALSE, authorization_request_front_channel_mode = "compat", request_object_signing_alg_values_supported = character(0), request_object_encryption_alg_values_supported = character(0), request_object_encryption_enc_values_supported = character(0), request_object_encryption_jwk = NULL, signed_request_object_required = FALSE, request_parameter_supported = NA, request_uri_parameter_supported = NA, request_uri_registration_required = NA, token_endpoint_auth_signing_alg_values_supported = character(0), dpop_signing_alg_values_supported = character(0), authorization_response_iss_parameter_supported = FALSE, response_modes_supported = character(0), issuer = NA_character_, issuer_match = "url", use_nonce = is_valid_string(issuer) && isTRUE(infer_oidc_from_issuer), use_pkce = TRUE, pkce_method = "S256", userinfo_required = FALSE, userinfo_id_selector = function(userinfo) userinfo[["sub"]], userinfo_id_token_match = is_valid_string(userinfo_url) && is_ok_host(userinfo_url) && (isTRUE(id_token_validation) || isTRUE(use_nonce)), userinfo_signed_jwt_required = FALSE, id_token_required = is_valid_string(issuer) && isTRUE(infer_oidc_from_issuer), id_token_validation = is_valid_string(issuer) && isTRUE(infer_oidc_from_issuer), id_token_at_hash_required = FALSE, extra_auth_params = list(), extra_token_params = list(), extra_token_headers = character(0), mtls_endpoint_aliases = list(), mtls_client_certificate_bound_access_tokens = FALSE, token_auth_style = "header", jwks_cache = cachem::cache_mem(max_age = 3600), jwks_pins = character(0), jwks_pin_mode = "any", jwks_host_issuer_match = is_valid_string(issuer) && (isTRUE(id_token_validation) || isTRUE(id_token_required)), jwks_host_allow_only = NA_character_, id_token_allowed_algs = c("RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "Ed25519", "EdDSA"), allowed_token_types = "Bearer", leeway = getOption("shinyOAuth.leeway", 30), token_target_mode = "none", infer_oidc_from_issuer = TRUE, jwks_uri = NA_character_, userinfo_allowed_algs = NULL, token_response_scope_format = "space", allow_missing_token_type = FALSE, jarm_signing_alg_values_supported = character(0), jarm_encryption_alg_values_supported = character(0), jarm_encryption_enc_values_supported = character(0), jarm_tolerate_duplicate_top_level_iss = FALSE, endpoint_auth_metadata = list(), allowed_algs = NULL, require_pushed_authorization_requests = NULL, require_signed_request_object = NULL, require_request_uri_registration = NULL, tls_client_certificate_bound_access_tokens = NULL )
name |
Provider name (e.g., "github", "google"). Cosmetic only; used in logging and audit events |
auth_url |
URL of the provider's login and permission page. |
token_url |
URL where R exchanges the returned code for tokens. |
userinfo_url |
User info endpoint URL (optional) |
introspection_url |
Optional URL where the provider can confirm whether a token is still active (RFC 7662). |
revocation_url |
Optional URL where the app can ask the provider to invalidate a token, for example during logout (RFC 7009). |
par_url |
Optional Pushed Authorization Request (PAR) URL (RFC 9126).
When set, shinyOAuth first sends the authorization request from server to
provider and then redirects the browser with the returned |
par_required |
Logical. Whether the provider
requires authorization requests to be sent via PAR. When |
authorization_request_front_channel_mode |
Character scalar controlling
which browser-visible outer parameters shinyOAuth keeps when the actual
authorization request is carried by JAR or PAR. Use |
request_object_signing_alg_values_supported |
Optional vector of JWS
algorithms that the provider advertises for signed Request Objects (RFC
9101). This is mainly used for early validation when an OAuthClient
sends |
request_object_encryption_alg_values_supported |
Optional vector of JWE key-management algorithms that the provider advertises for encrypted Request Objects. This metadata is used for early validation when an OAuthClient enables Request Object encryption. |
request_object_encryption_enc_values_supported |
Optional vector of JWE content-encryption algorithms that the provider advertises for encrypted Request Objects. This metadata is used for early validation when an OAuthClient enables Request Object encryption. |
request_object_encryption_jwk |
Optional explicit recipient public key used to encrypt Request Objects when discovery-backed JWKS selection is not available or when you need to pin one specific encryption key. Accepts an OpenSSL public key, a PEM public-key string, a parsed JWK object, or a JWK JSON string. |
signed_request_object_required |
Logical. Whether the provider requires
signed Request Objects for authorization requests. When |
request_parameter_supported |
Logical or |
request_uri_parameter_supported |
Logical or |
request_uri_registration_required |
Logical or |
token_endpoint_auth_signing_alg_values_supported |
Optional vector of
JWS algorithms that the provider advertises for JWT-based client
authentication ( |
dpop_signing_alg_values_supported |
Optional vector of JWS algorithms
that the provider advertises for DPoP proof JWTs (RFC 9449). This
metadata is used for early validation of |
authorization_response_iss_parameter_supported |
Logical. Whether the
provider advertises RFC 9207 support for returning an |
response_modes_supported |
Optional character vector of OAuth/OIDC
|
issuer |
Optional authorization-server issuer URL. You need this for issuer validation and features such as ID-token validation. shinyOAuth uses it to verify issuer claims and locate signing keys (JWKS), typically through an OIDC discovery document. |
issuer_match |
Character scalar controlling how strictly the discovery
document's
In most cases, keep the default |
use_nonce |
Whether to tie the ID token to this login using a random nonce. Keep enabled for OIDC. The nonce is sent in the request and checked in the returned ID token. |
use_pkce |
Whether to protect the code exchange using Proof Key for Code
Exchange (PKCE). Leave enabled; public clients require it.
It sends a |
pkce_method |
PKCE code challenge method ("S256" or "plain"). "S256" is recommended. Use "plain" only if you are working with a provider that does not support "S256". |
userinfo_required |
Whether to fetch a user profile after token exchange.
The result is stored in |
userinfo_id_selector |
A function that extracts the user ID from the userinfo response. Should take a single argument (the userinfo list) and return the user ID as a string. This is used for helpers that need a provider-specific application user
identifier, such as audit fields. It does not replace OIDC subject binding:
when a validated ID token and UserInfo are both available, their actual
|
userinfo_id_token_match |
Whether fetched userinfo requires a validated ID
token for comparison. When both are available, their actual |
userinfo_signed_jwt_required |
Whether to require the user profile to arrive
as a signed JWT ( |
id_token_required |
Whether to require an ID token to be returned
during token exchange. If no ID token is returned, the token exchange
will fail. This only makes sense for OpenID Connect providers and may
require the client's scope to include Both the S7 constructor and |
id_token_validation |
Whether to perform ID token validation after token exchange.
This requires the provider to be a valid OpenID Connect provider with a configured
Both the S7 constructor and |
id_token_at_hash_required |
Whether to require the |
extra_auth_params |
Extra parameters for authorization URL |
extra_token_params |
Extra parameters for token exchange.
|
extra_token_headers |
Extra headers for back-channel token-style
requests (named character vector), applied only to token exchange and
refresh. Configure |
mtls_endpoint_aliases |
Optional named list of RFC 8705 mTLS endpoint
aliases. Names should follow the metadata keys such as |
mtls_client_certificate_bound_access_tokens |
Logical. Whether the
authorization server advertises RFC 8705 capability to issue
certificate-bound access tokens. This describes server capability; the
client still has to opt into mTLS separately. When |
token_auth_style |
How the client authenticates at the token endpoint. One of:
|
jwks_cache |
Storage for the provider's public signing keys. Defaults to
|
jwks_pins |
Optional character vector of RFC 7638 JWK thumbprints
(base64url) to pin against. If non-empty, fetched JWKS must contain keys
whose thumbprints match these values depending on |
jwks_pin_mode |
Pinning policy when |
jwks_host_issuer_match |
When TRUE, enforce that the discovery |
jwks_host_allow_only |
Optional explicit hostname that the jwks_uri must match.
When provided, jwks_uri host must equal this value (exact match). You can
pass either just the host (e.g., "www.googleapis.com") or a full URL; only
the host component will be used. If you need to include a port or an IPv6
literal, pass a full URL (e.g., |
id_token_allowed_algs |
Optional vector of allowed JWT algorithms for ID tokens.
Use to restrict acceptable |
allowed_token_types |
Character vector of acceptable OAuth token types
returned by the token endpoint (case-insensitive). Successful token
responses must include |
leeway |
Clock skew leeway (seconds) applied to ID token |
token_target_mode |
Explicit acquisition protocol for clients declaring
|
infer_oidc_from_issuer |
Whether setting |
jwks_uri |
Optional URL of the provider's public signing keys (JWKS). Normally these are located through OIDC discovery. Set this for manual key configuration, including OAuth-only JARM providers. |
userinfo_allowed_algs |
Optional signing algorithm allowlist for UserInfo
JWTs. |
token_response_scope_format |
Token response scope separator, |
allow_missing_token_type |
Logical, default |
jarm_signing_alg_values_supported |
Optional vector of JWS algorithms that the provider advertises for signed JWT Secured Authorization Responses (JARM). |
jarm_encryption_alg_values_supported |
Optional vector of JWE key-management algorithms that the provider advertises for encrypted JARM responses. |
jarm_encryption_enc_values_supported |
Optional vector of JWE content-encryption algorithms that the provider advertises for encrypted JARM responses. |
jarm_tolerate_duplicate_top_level_iss |
Logical. Whether shinyOAuth
should tolerate repeated identical top-level |
endpoint_auth_metadata |
Named list of independent |
allowed_algs |
Compatibility alias for |
require_pushed_authorization_requests |
Compatibility alias for |
require_signed_request_object |
Compatibility alias for |
require_request_uri_registration |
Compatibility alias for |
tls_client_certificate_bound_access_tokens |
Compatibility alias for |
Endpoint URLs identify the provider's authorization, token, and profile services.
Provider helpers fill in these URLs and suitable defaults. A separate
oauth_client() holds your app's credentials and requested permissions.
See the usage vignette for the complete setup.
Calling the constructor creates an OAuthProvider object.
# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }# Configure generic OAuth 2.0 provider (no OIDC) generic_provider <- oauth_provider( name = "example", auth_url = "https://example.com/oauth/authorize", token_url = "https://example.com/oauth/token", # Optional URL for fetching user info: userinfo_url = "https://example.com/oauth/userinfo" ) # Configure generic OIDC provider manually # (This defaults to using nonce & ID token validation) generic_oidc_provider <- oauth_provider_oidc( name = "My OIDC", base_url = "https://my-issuer.example.com" ) # Configure a OIDC provider via OIDC discovery # (requires network access) if (interactive()) { # Using Auth0 sample issuer as an example oidc_discovery_provider <- oauth_provider_oidc_discover( issuer = "https://samples.auth0.com/" ) } # GitHub preconfigured provider github_provider <- oauth_provider_github() # Google preconfigured provider google_provider <- oauth_provider_google() # Microsoft preconfigured provider # For a complete app using a custom tenant ID, see: # https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html # Spotify preconfigured provider spotify_provider <- oauth_provider_spotify() # Slack via OIDC discovery # (requires network access) if (interactive()) { slack_provider <- oauth_provider_slack() } # Keycloak # (requires configured Keycloak realm; example below is therefore not run) if (interactive()) { options(shinyOAuth.allow_insecure_oidc_loopback = TRUE) oauth_provider_keycloak(base_url = "http://localhost:8080", realm = "myrealm") } # Auth0 # (requires configured Auth0 domain; example below is therefore not run) if (interactive()) { oauth_provider_auth0(domain = "your-tenant.auth0.com") } # Okta # (requires configured Okta domain; example below is therefore not run) if (interactive()) { oauth_provider_okta(domain = "dev-123456.okta.com") }
An OAuthToken holds credentials and user information returned after login.
The Shiny module supplies it as auth[["token"]], and handle_callback() returns
it for custom integrations. Pass it to perform_resource_req() to call an
API, or to the token helpers for refresh, introspection, and revocation.
Read properties with @, for example auth[["token"]]@userinfo. Profile fields
depend on the provider. Keep access and refresh tokens out of the UI and logs.
OAuthToken( access_token = character(0), token_type = NA_character_, refresh_token = NA_character_, id_token = NA_character_, expires_at = Inf, userinfo = list(), cnf = list(), granted_scopes = character(0), granted_scopes_verified = FALSE, id_token_validated = FALSE, original_id_token = NA_character_, extra_fields = list(), initial_extra_fields = list(), smart_context = list(), original_granted_scopes = character(0) )OAuthToken( access_token = character(0), token_type = NA_character_, refresh_token = NA_character_, id_token = NA_character_, expires_at = Inf, userinfo = list(), cnf = list(), granted_scopes = character(0), granted_scopes_verified = FALSE, id_token_validated = FALSE, original_id_token = NA_character_, extra_fields = list(), initial_extra_fields = list(), smart_context = list(), original_granted_scopes = character(0) )
access_token |
Access token |
token_type |
OAuth access token type (for example |
refresh_token |
Refresh token (if provided by the provider) |
id_token |
ID token (if provided by the provider; OpenID Connect) |
expires_at |
Numeric timestamp (seconds since epoch) when the access
token expires, |
userinfo |
List containing user information fetched from the provider's userinfo endpoint (if fetched) |
cnf |
Optional confirmation claim set returned alongside a
sender-constrained access token or observed on another token surface. For
RFC 8705 certificate-bound tokens, this may contain |
granted_scopes |
Normalized scope tokens currently associated with the
access token. When a provider omits |
granted_scopes_verified |
Logical flag indicating whether the current
token response explicitly proved |
id_token_validated |
Logical flag indicating whether the ID token was
cryptographically validated (signature verified and standard claims checked)
during the OAuth flow. Defaults to |
original_id_token |
Initial login ID token retained as the refresh
continuity baseline. Refresh never replaces it with a newer ID token.
For manually constructed tokens, the first refresh initializes this from
|
extra_fields |
List of additional parameters from the latest successful
token endpoint response. Excludes |
initial_extra_fields |
List of additional parameters from the initial
successful authorization-code exchange. Preserved across refreshes and
replaced on a new login. Defaults to an empty list for manually constructed
tokens; refresh does not infer an initial response from |
smart_context |
Internal interpreted SMART context. Empty for ordinary
tokens; populated only by SMART token processing. Use |
original_granted_scopes |
Initial accepted SMART grant, preserved across refreshes to distinguish unchanged grants from strict scope reductions. Empty for ordinary OAuth tokens. Set by SMART token processing. |
The id_token_claims property is a read-only computed property that returns
the decoded JWT payload of the ID token as a named list. This surfaces all
standard and optional OIDC claims (e.g., sub, iss, aud, acr, amr,
auth_time, nonce, at_hash, etc.) without requiring manual JWT
decoding. Returns an empty list when no ID token is present or if the token
cannot be decoded.
Note: id_token_claims always decodes the JWT payload regardless
of whether the ID token's signature was verified.
Check the id_token_validated property to determine whether the claims
were cryptographically validated.
For validated Apple ID tokens, exact "true"/"false" strings in
email_verified are returned as logical values, as during validation.
The original signed id_token is retained unchanged.
Additional response parameters retain their parsed names and values,
including nested lists and explicit JSON null values (R NULL). Use
"custom_field" %in% names(token@extra_fields) to distinguish an absent field
from a field explicitly returned as null. These parameters are not ID
token claims and are not covered by id_token_validated. The initial
snapshot records the initial response data, not current access permissions.
No automatic merging, resource fetching, or interpretation is performed.
Both lists can contain sensitive data; keep them out of the UI and logs.
Calling the constructor creates an OAuthToken object.
# Inside reactive server code, after a successful login: # auth[["token"]]@userinfo # auth[["token"]]@expires_at # auth[["token"]]@id_token_validated # auth[["token"]]@id_token_claims[["sub"]] # auth[["token"]]@extra_fields[["custom_field"]] # auth[["token"]]@initial_extra_fields[["custom_field"]]# Inside reactive server code, after a successful login: # auth[["token"]]@userinfo # auth[["token"]]@expires_at # auth[["token"]]@id_token_validated # auth[["token"]]@id_token_claims[["sub"]] # auth[["token"]]@extra_fields[["custom_field"]] # auth[["token"]]@initial_extra_fields[["custom_field"]]
perform_resource_req()
Deprecated alias for perform_resource_req().
Use perform_resource_req() for Bearer, DPoP, and mTLS-protected resource requests instead.
perform_client_bearer_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, idempotent = NULL, resource_hosts = NULL, oauth_client = NULL )perform_client_bearer_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, idempotent = NULL, resource_hosts = NULL, oauth_client = NULL )
token |
Either an OAuthToken object or a raw access token string. |
url |
Either the absolute URL to call or an |
method |
Optional HTTP method (character). Defaults to "GET". When
the effective token type is |
headers |
Optional named list or named character vector of extra
headers to set on the request. Header names are case-insensitive.
Names must be unique HTTP field-name tokens, and values must not contain
prohibited control characters. DPoP requests reject |
query |
Optional named list of query parameters to append to the URL. |
follow_redirect |
Logical or |
check_url |
Logical. If |
client |
Optional OAuthClient. Required when the effective
token type is |
token_type |
Optional override for the access token type when |
dpop_nonce |
Optional DPoP nonce to embed in the proof for this
request. This is primarily useful after a resource server challenges with
|
idempotent |
Whether ordinary network/HTTP failures may be
retried safely. |
resource_hosts |
Optional non-empty character vector of trusted resource
host patterns, using |
oauth_client |
Compatibility alias for |
Same value as perform_resource_req().
Send an authenticated API request on the user's behalf and return an
httr2 response. Pass the token from login and the API URL, then read the
response with httr2::resp_body_json() or another httr2 response helper.
perform_resource_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, idempotent = NULL, resource_hosts = NULL, oauth_client = NULL )perform_resource_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, idempotent = NULL, resource_hosts = NULL, oauth_client = NULL )
token |
Either an OAuthToken object or a raw access token string. |
url |
Either the absolute URL to call or an |
method |
Optional HTTP method (character). Defaults to "GET". When
the effective token type is |
headers |
Optional named list or named character vector of extra
headers to set on the request. Header names are case-insensitive.
Names must be unique HTTP field-name tokens, and values must not contain
prohibited control characters. DPoP requests reject |
query |
Optional named list of query parameters to append to the URL. |
follow_redirect |
Logical or |
check_url |
Logical. If |
client |
Optional OAuthClient. Required when the effective
token type is |
token_type |
Optional override for the access token type when |
dpop_nonce |
Optional DPoP nonce to embed in the proof for this
request. This is primarily useful after a resource server challenges with
|
idempotent |
Whether ordinary network/HTTP failures may be
retried safely. |
resource_hosts |
Optional non-empty character vector of trusted resource
host patterns, using |
oauth_client |
Compatibility alias for |
Only send a token to an API you intend to authorize. The package applies its
URL policy, timeouts, and redirect defaults. It supports Bearer authentication
and tokens tied to a key (DPoP) or certificate (mTLS). For DPoP or mTLS,
also supply client so the request uses the matching key or
certificate.
An httr2 response object.
# Make request using OAuthToken object # (code is not run because it requires a real token from user interaction) if (interactive()) { # Inside reactive server code, after login has succeeded: token <- auth[["token"]] # Recommended for most callers: build + perform in one step. response <- perform_resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Build only when you need to inspect the request yourself. request <- resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Inspect request settings without printing authentication headers. # httr2::req_perform(request) sends it when ready. # Or start from your own httr2 request and still let shinyOAuth perform it # so DPoP nonce retries remain available. custom_request <- httr2::request("https://api.example.com/resource") |> httr2::req_headers(Accept = "application/json") |> httr2::req_url_query(limit = 5) response <- perform_resource_req(token, custom_request) # Constrain dynamic URLs before attaching a token. check_url alone does not # restrict HTTPS hosts unless a global allowed_hosts policy is configured. response <- perform_resource_req( token, input[["resource_url"]], resource_hosts = "api.example.com", follow_redirect = FALSE ) }# Make request using OAuthToken object # (code is not run because it requires a real token from user interaction) if (interactive()) { # Inside reactive server code, after login has succeeded: token <- auth[["token"]] # Recommended for most callers: build + perform in one step. response <- perform_resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Build only when you need to inspect the request yourself. request <- resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Inspect request settings without printing authentication headers. # httr2::req_perform(request) sends it when ready. # Or start from your own httr2 request and still let shinyOAuth perform it # so DPoP nonce retries remain available. custom_request <- httr2::request("https://api.example.com/resource") |> httr2::req_headers(Accept = "application/json") |> httr2::req_url_query(limit = 5) response <- perform_resource_req(token, custom_request) # Constrain dynamic URLs before attaching a token. check_url alone does not # restrict HTTPS hosts unless a global allowed_hosts policy is configured. response <- perform_resource_req( token, input[["resource_url"]], resource_hosts = "api.example.com", follow_redirect = FALSE ) }
Creates the same one-use state, PKCE and nonce as prepare_call(), returning
a plain list describing how to send the request. Use this for applications
that handle their own browser navigation and handle_callback(). In Shiny,
the module's request_login() sends the request automatically.
prepare_authorization_request( client, browser_token, request_uri_publisher = NULL )prepare_authorization_request( client, browser_token, request_uri_publisher = NULL )
client |
An OAuthClient object. |
browser_token |
Browser-bound token used to tie the login attempt to the current browser session. |
request_uri_publisher |
Optional function used when
|
For GET, navigate the browser to url. For POST, create a form with url as
its action, method POST, and application/x-www-form-urlencoded encoding.
Add one hidden input per fields entry, assigning its name and value
through DOM properties or an HTML escaping library, then submit the form in
the current browser window. Repeated names (such as OAuth resource) must
remain repeated inputs. Do not send the authorization request from R: the
provider needs to interact with the user's browser and login cookies.
The client selects authorization_method = "POST" explicitly. Ordinary
OAuth providers may not support POST; confirm their documentation first.
smart_client() additionally requires the authorize-post capability.
The outgoing method is independent of the callback response_mode.
Configured PAR and Request Object requirements still apply. Each result
belongs to one login attempt; do not cache it or reuse it after logout.
POST preserves the authorization endpoint's fixed query and sends newly
composed fields in the body. It permits up to 256 fields and 128 KiB of
encoded form data; CR/LF and the browser-reserved _charset_ field are
rejected to prevent the browser changing field values. Existing state and
callback size limits also apply. Configure
your app's Content Security Policy form-action to allow the authorization
endpoint. Custom callers must preserve browser binding and callback handling.
A list with method ("GET" or "POST"), url and fields.
fields is empty for GET; for POST it is a list of lists, each with scalar
character name and value. PAR expiry attributes are preserved as
documented in prepare_call(). The result contains transient authorization
data: do not log it or expose it to other browser sessions.
provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback" ) # In a custom browser flow, bind this secret to the initiating browser and # supply it again to handle_callback(). Shiny modules manage this for you. browser_token <- paste(format(openssl::rand_bytes(64)), collapse = "") request <- prepare_authorization_request(client, browser_token) request[["method"]]provider <- oauth_provider( name = "Example service", auth_url = "https://example.com/authorize", token_url = "https://example.com/token", token_auth_style = "public" ) client <- oauth_client( provider = provider, client_id = "example-client", redirect_uri = "http://127.0.0.1:8100/callback" ) # In a custom browser flow, bind this secret to the initiating browser and # supply it again to handle_callback(). Shiny modules manage this for you. browser_token <- paste(format(openssl::rand_bytes(64)), collapse = "") request <- prepare_authorization_request(client, browser_token) request[["method"]]
Prepare a login request and return the URL to open in the user's browser.
Use this when your application controls the browser redirect and callback
handling itself but needs shinyOAuth to construct the OAuth 2.0 authorization
request. Pair it with handle_callback() to complete the code flow.
prepare_call( client, browser_token, request_uri_publisher = NULL, oauth_client = NULL )prepare_call( client, browser_token, request_uri_publisher = NULL, oauth_client = NULL )
client |
An OAuthClient object. |
browser_token |
Browser-bound token used to tie the login attempt to the current browser session. |
request_uri_publisher |
Optional function used when
|
oauth_client |
Compatibility alias for |
In a Shiny app using oauth_module_server(), call auth[["request_login"]]()
to start login through the module, which manages both operations and the
reactive session state.
The helper records one-time state and creates any required PKCE and nonce
values. Custom callers must preserve the browser binding and process the
returning callback themselves.
For an explicitly configured POST client, use prepare_authorization_request()
instead. This URL-only helper rejects POST before storing a transaction.
A length-1 string containing the authorization URL to send the user
to. When PAR is used, the returned string also carries
shinyOAuth.par_request_uri, shinyOAuth.par_expires_in, and
shinyOAuth.par_expires_at attributes so callers can tell when the pushed
authorization request should be regenerated.
# Advanced example: your code supplies browser redirects and callback handling. # For a Shiny app, oauth_module_server() manages these steps for you. if (interactive()) { # Define client client <- oauth_client( provider = oauth_provider_github(), client_id = Sys.getenv("GITHUB_OAUTH_CLIENT_ID"), client_secret = Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100" ) # Get the login URL and store state in client's state store # `<browser_token>` must be unpredictable and persisted for this transaction # in storage bound to the application's exact origin (scheme, host, port). # The module combines origin-scoped storage with an independent marker cookie # and checks both on return. A cookie alone does not provide this boundary: # cookies can be shared by applications on different ports of the same host. # Shiny applications should use oauth_module_server() for the complete flow. authorization_url <- prepare_call(client, "<browser_token>") # Redirect user to authorization URL; retrieve code & state from the query; # recover this transaction's `<browser_token>` through the origin-bound flow # and verify its independent marker before calling handle_callback(). code <- "..." state <- "..." browser_token <- "..." # Handle callback, exchanging code for token and validating state token <- handle_callback(client, code, state, browser_token) }# Advanced example: your code supplies browser redirects and callback handling. # For a Shiny app, oauth_module_server() manages these steps for you. if (interactive()) { # Define client client <- oauth_client( provider = oauth_provider_github(), client_id = Sys.getenv("GITHUB_OAUTH_CLIENT_ID"), client_secret = Sys.getenv("GITHUB_OAUTH_CLIENT_SECRET"), redirect_uri = "http://127.0.0.1:8100" ) # Get the login URL and store state in client's state store # `<browser_token>` must be unpredictable and persisted for this transaction # in storage bound to the application's exact origin (scheme, host, port). # The module combines origin-scoped storage with an independent marker cookie # and checks both on return. A cookie alone does not provide this boundary: # cookies can be shared by applications on different ports of the same host. # Shiny applications should use oauth_module_server() for the complete flow. authorization_url <- prepare_call(client, "<browser_token>") # Redirect user to authorization URL; retrieve code & state from the query; # recover this transaction's `<browser_token>` through the origin-bound flow # and verify its independent marker before calling handle_callback(). code <- "..." state <- "..." browser_token <- "..." # Handle callback, exchanging code for token and validating state token <- handle_callback(client, code, state, browser_token) }
Use a refresh token to obtain a new access token without sending the user
through login again. Call this when your application manages token lifetime
itself, for example before continuing API requests with an expiring token.
Assign the returned OAuthToken to keep the updated credentials.
oauth_module_server() can manage refresh during a Shiny session with
refresh_proactively = TRUE when a refresh token is available.
refresh_token( client, token, async = FALSE, introspect = NULL, shiny_session = NULL, oauth_client = NULL )refresh_token( client, token, async = FALSE, introspect = NULL, shiny_session = NULL, oauth_client = NULL )
client |
OAuthClient object |
token |
OAuthToken object containing the refresh token |
async |
If |
introspect |
|
shiny_session |
Optional captured Shiny session details for audit events.
Normally supplied by the module; leave |
oauth_client |
Compatibility alias for |
The provider may replace the refresh token too; otherwise the old refresh token is kept. Required userinfo is fetched again, and configured client introspection must succeed before the refreshed token is returned.
For ordinary OAuth clients, refresh explicitly requests the token's retained
granted_scopes when known. This preserves prior scope reductions and makes
an omitted response scope refer to that requested set. Tokens without known
scopes omit the request parameter. SMART uses its own original-grant rules.
OIDC refresh responses may omit the ID token, in which case the original is
kept. If a new ID token is returned, an original must be available and the
subject, issuer, and audience must remain consistent, as must auth_time
and nonce when applicable. Full signature and
claim validation runs when id_token_validation = TRUE. Userinfo is checked
against a validated ID token when both are available;
userinfo_id_token_match = TRUE requires that baseline.
Refresh does not establish a new interactive login. Use the module's
reauth_after_seconds argument when a fresh login is required.
A returned ID token must have an iat at or after the refresh request start,
allowing the provider's configured clock leeway and same-second issuance.
Within one R process, overlapping asynchronous calls for the same client and refresh token share one promise and result. Token snapshots, client settings, and validation options must match; conflicting calls fail before dispatch. A synchronous or reentrant call while that refresh is pending raises an error; await the existing promise instead. Separate R processes require coordination by the application. The input token is a value object: store the returned token for subsequent refreshes, and use an application generation check when assigning results after logout or a new login. Completed results are not cached.
Refresh errors carry a non-secret refresh_credential_outcome field:
"not_consumed", "consumed", "possibly_consumed", or "rejected".
Only "not_consumed" permits retrying the input refresh credential. After
any other outcome (including an unavailable worker result), discard that
credential and require a new login. This does not accept unvalidated access
or identity data. The module applies this rule even with indefinite sessions.
An updated OAuthToken object with refreshed credentials.
What changes:
access_token: Always updated to the fresh token
expires_at: Computed from expires_in when provided; otherwise a
fallback lifetime set by shinyOAuth.default_expires_in (3600 seconds by default)
refresh_token: Updated if the provider rotates it; otherwise preserved
id_token: Updated only if the provider returns one (and it validates);
otherwise the latest stored ID token is preserved
original_id_token: Retained from login for continuity checks, even if
intermediate refresh ID tokens omit nonce or auth_time
userinfo: Refreshed if userinfo_required = TRUE; otherwise preserved
extra_fields: Replaced by the additional parameters in the refresh
response, or an empty list if none are returned. Not merged with earlier
responses; explicit JSON null values remain named NULL entries.
initial_extra_fields: Preserved from the initial code exchange. This
historical snapshot does not establish current access permissions.
cnf: Updated from the token response when present, and may be
backfilled from refresh-time introspection when enabled. When the
refresh response omits new observable cnf, shinyOAuth does not carry
forward a prior x5t#S256 thumbprint onto the refreshed token; mTLS
sender-constrained state is kept only when the new token or its
introspection response supplies fresh cnf
Validation failures cause errors: If the provider returns a new ID
token that fails validation (wrong issuer, audience, expired, or subject
mismatch with original), or if userinfo subject doesn't match the new ID
token, the refresh fails with an error. In oauth_module_server(), this
clears the session and sets authenticated = FALSE, unless
indefinite_session = TRUE keeps it with token_stale = TRUE.
# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }
Build an httr2 request that uses the user's access token. Use this when
you want to inspect or customize a request before sending it. To build and
send in one step, use perform_resource_req().
resource_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, resource_hosts = NULL, oauth_client = NULL )resource_req( token, url, method = "GET", headers = NULL, query = NULL, follow_redirect = FALSE, check_url = TRUE, client = NULL, token_type = NULL, dpop_nonce = NULL, resource_hosts = NULL, oauth_client = NULL )
token |
Either an OAuthToken object or a raw access token string. |
url |
The absolute URL to call. |
method |
Optional HTTP method (character). Defaults to "GET". When
the effective token type is |
headers |
Optional named list or named character vector of extra
headers to set on the request. Header names are case-insensitive.
Names must be unique HTTP field-name tokens, and values must not contain
prohibited control characters. DPoP requests reject |
query |
Optional named list of query parameters to append to the URL. |
follow_redirect |
Logical or |
check_url |
Logical. If |
client |
Optional OAuthClient. Required when the effective
token type is |
token_type |
Optional override for the access token type when |
dpop_nonce |
Optional DPoP nonce to embed in the proof for this
request. This is primarily useful after a resource server challenges with
|
resource_hosts |
Optional non-empty character vector of trusted resource
host patterns, using |
oauth_client |
Compatibility alias for |
Only send a token to an API you intend to authorize. The package applies its
URL policy, timeouts, and redirect defaults. It supports Bearer authentication
and tokens tied to a key (DPoP) or certificate (mTLS). For DPoP or mTLS,
also supply client so the request uses the matching key or
certificate.
Managed Authorization credentials cannot be combined with an access_token
query parameter or form field. Inspection covers URL/query inputs and prebuilt
httr2 form, raw and string bodies labelled application/x-www-form-urlencoded.
JSON business fields are unaffected. File, multipart and streaming bodies are
not parsed; callers must ensure these contain no additional OAuth credential
transport. Later request changes outside these helpers require a new check.
An httr2 request object, ready to be performed with
httr2::req_perform(). Callers may still add headers or query
parameters, but when the effective token type is DPoP they must not
change the request method or base URL after calling
resource_req() because the proof is already bound to those values.
DPoP proofs bind the current HTTP method and target URI (without query or
fragment). Literal . and .. path segments are removed before signing
and sending; percent-encoded path segments remain escaped.
Use the query argument to preserve encoded resource paths;
external URL modifiers can decode reserved path characters. Changing the
method, scheme, host, or path invalidates the proof.
Automatic redirects are unsupported. Request a trusted final URL directly
so the helper can create a proof bound to that URL.
# Make request using OAuthToken object # (code is not run because it requires a real token from user interaction) if (interactive()) { # Inside reactive server code, after login has succeeded: token <- auth[["token"]] # Recommended for most callers: build + perform in one step. response <- perform_resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Build only when you need to inspect the request yourself. request <- resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Inspect request settings without printing authentication headers. # httr2::req_perform(request) sends it when ready. # Or start from your own httr2 request and still let shinyOAuth perform it # so DPoP nonce retries remain available. custom_request <- httr2::request("https://api.example.com/resource") |> httr2::req_headers(Accept = "application/json") |> httr2::req_url_query(limit = 5) response <- perform_resource_req(token, custom_request) # Constrain dynamic URLs before attaching a token. check_url alone does not # restrict HTTPS hosts unless a global allowed_hosts policy is configured. response <- perform_resource_req( token, input[["resource_url"]], resource_hosts = "api.example.com", follow_redirect = FALSE ) }# Make request using OAuthToken object # (code is not run because it requires a real token from user interaction) if (interactive()) { # Inside reactive server code, after login has succeeded: token <- auth[["token"]] # Recommended for most callers: build + perform in one step. response <- perform_resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Build only when you need to inspect the request yourself. request <- resource_req( token, "https://api.example.com/resource", query = list(limit = 5) ) # Inspect request settings without printing authentication headers. # httr2::req_perform(request) sends it when ready. # Or start from your own httr2 request and still let shinyOAuth perform it # so DPoP nonce retries remain available. custom_request <- httr2::request("https://api.example.com/resource") |> httr2::req_headers(Accept = "application/json") |> httr2::req_url_query(limit = 5) response <- perform_resource_req(token, custom_request) # Constrain dynamic URLs before attaching a token. check_url alone does not # restrict HTTPS hosts unless a global allowed_hosts policy is configured. response <- perform_resource_req( token, input[["resource_url"]], resource_hosts = "api.example.com", follow_redirect = FALSE ) }
Ask the provider to invalidate an access or refresh token, for example when
a user disconnects their account or your application disposes of stored
credentials. The provider must support token revocation. The Shiny module
calls this during logout; use auth[["logout"]]() to also clear its local session.
Revocation does not end the user's login session at the provider.
revoke_token( client, token, token_kind = c("refresh", "access"), async = FALSE, shiny_session = NULL, oauth_client = NULL, oauth_token = NULL, which = NULL )revoke_token( client, token, token_kind = c("refresh", "access"), async = FALSE, shiny_session = NULL, oauth_client = NULL, oauth_token = NULL, which = NULL )
client |
OAuthClient object |
token |
OAuthToken object containing tokens to revoke |
token_kind |
Which token to revoke: "refresh" (default) or "access" |
async |
If |
shiny_session |
Optional captured Shiny session details for audit events.
Normally supplied by the module; leave |
oauth_client |
Compatibility alias for |
oauth_token |
Compatibility alias for |
which |
Compatibility alias for |
Uses the client's configured credentials and token_auth_style. Check the
returned status: an absent endpoint or token, or an unsuccessful HTTP
response, leaves the revocation result unknown. A successful response means
the provider accepted the request; local logout does not depend on it.
A list with fields:
supported: logical, TRUE when a revocation endpoint is configured.
revoked: logical or NA, TRUE when the provider accepted the
revocation request, NA when revocation could not be attempted or the
result is unknown.
status: machine-readable status such as "ok", "missing_token",
"revocation_unsupported", or "http_<code>".
# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }# get_userinfo(), introspect_token(), and refresh_token() are typically # called by oauth_module_server() according to your provider/client and # module settings, rather than directly by application code. The module # also calls revoke_token() during logout when the provider supports it. # These helpers are exported for custom login flows, on-demand profile or # token checks, and applications that manage token lifetime themselves. # # The examples below require a real token from a completed login. # Inside a reactive expression in server(), after creating auth with # oauth_module_server() and confirming auth[["authenticated"]]: if (interactive()) { token <- auth[["token"]] user_info <- get_userinfo(client, token) # Requires an introspection endpoint. NA means activity is unknown. result <- introspect_token(client, token) isTRUE(result[["active"]]) # Requires a refresh token. Keep the returned replacement. token <- refresh_token(client, token) # Requires a revocation endpoint to invalidate the token at the provider. result <- revoke_token(client, token, token_kind = "refresh") }
Combine a reviewed smart_discover() snapshot with an existing app
registration. The result is an OAuthClient for oauth_connections(), with
the resource ID "fhir". Discovery does not register the app or grant access.
smart_client( discovery, client_id, redirect_uri, scopes, required_scopes = scopes, launch = c("standalone", "ehr"), identity = c("none", "openid", "fhirUser"), allow_v1_scopes = FALSE, online_access_policy = c("online_only", "allow_offline"), token_auth_style = c("public", "header", "private_key_jwt"), client_secret = character(), client_assertion_private_key = NULL, client_assertion_private_key_kid = NULL, client_assertion_alg = "RS384", authorization_method = "GET", response_mode = NULL, authorization_server_mode = "single", authorization_server_redirect_uris = character(), initial_expires_in_fallback = NULL, label = "FHIR server", state_store = cachem::cache_mem(max_age = 300), state_key = random_urlsafe(128), state_payload_max_age = 300 )smart_client( discovery, client_id, redirect_uri, scopes, required_scopes = scopes, launch = c("standalone", "ehr"), identity = c("none", "openid", "fhirUser"), allow_v1_scopes = FALSE, online_access_policy = c("online_only", "allow_offline"), token_auth_style = c("public", "header", "private_key_jwt"), client_secret = character(), client_assertion_private_key = NULL, client_assertion_private_key_kid = NULL, client_assertion_alg = "RS384", authorization_method = "GET", response_mode = NULL, authorization_server_mode = "single", authorization_server_redirect_uris = character(), initial_expires_in_fallback = NULL, label = "FHIR server", state_store = cachem::cache_mem(max_age = 300), state_key = random_urlsafe(128), state_payload_max_age = 300 )
discovery |
A plain snapshot returned by |
client_id, redirect_uri
|
App registration values. The callback must use HTTPS, except for the snapshot's explicit HTTP loopback development policy. |
scopes |
Permissions to request, without automatic wildcard/offline
access. Standalone patient scopes require |
required_scopes |
Minimum permissions, defaulting to |
launch |
|
identity |
|
allow_v1_scopes |
Explicit compatibility flag enabling |
online_access_policy |
|
token_auth_style |
Registration type: |
client_secret |
Secret for a symmetric registration; otherwise omit. |
client_assertion_private_key, client_assertion_private_key_kid
|
Private
signing key and registered key ID for asymmetric authentication; see
|
client_assertion_alg |
|
authorization_method |
|
response_mode |
|
authorization_server_mode, authorization_server_redirect_uris
|
See
|
initial_expires_in_fallback |
Optional positive lifetime in seconds, supplied by
the authorization server out of band for initial access tokens. Used only
when an initial response omits |
label |
Display label, default |
state_store, state_key, state_payload_max_age
|
See |
This constructor selects SMART 2.2 scope rules, S256 PKCE and the exact FHIR
base as the authorization request's aud. Identity is opt-in: "openid"
requests and requires openid, signed ID-token validation and nonce binding.
"fhirUser" additionally requests and requires the fhirUser scope and claim.
"none" does not enable OIDC because an issuer happens to be present.
A patient in context is independent of the authenticated user and local owner.
Only direct authorization requests and query/form POST callbacks are supported.
Claims requests (including auth_time), JAR, PAR, JARM, DPoP, mTLS and remote
freshness requirements are not supported by this constructor.
EHR clients require a fresh registered launch transaction.
Configure standalone and EHR registrations as separate clients when both are
needed. No launch handle is stored in shared provider configuration.
Local usability policy requires a positive lifetime. An initial response may
omit expires_in only when initial_expires_in_fallback is configured explicitly;
refresh responses must include it. The generic assumed lifetime is not used.
SMART back-channel and resource requests require TLS 1.2 or newer. A stronger
configured TLS minimum is preserved; ordinary clients keep their defaults.
An OAuthClient, usable by the existing module or the separate
connection manager. @resource_bases contains the approved fhir base;
@required_scopes and @label use the ordinary client properties. @smart
describes the selected profile. The client contains registration settings,
never a user's token or launch context. Configure it outside server().
SMART 2.2 launch and client authentication.
## Not run: site <- smart_discover("https://ehr.example/fhir/R4") client <- smart_client(site, "registered-app", "https://app.example/callback", scopes = c("launch/patient", "patient/Patient.r"), required_scopes = "patient/Patient.r", token_auth_style = "public") client@smart[["launch"]] ## End(Not run)## Not run: site <- smart_discover("https://ehr.example/fhir/R4") client <- smart_client(site, "registered-app", "https://app.example/callback", scopes = c("launch/patient", "patient/Patient.r"), required_scopes = "patient/Patient.r", token_auth_style = "public") client@smart[["launch"]] ## End(Not run)
Return interpreted patient/encounter and validated user references for the current accepted token. This is sensitive data; keep it out of general status tables and logs. Raw token extensions remain available separately on tokens.
smart_context(connection) smart_patient(connection) smart_fhir_user(connection)smart_context(connection) smart_patient(connection) smart_fhir_user(connection)
connection |
An OAuthConnection for a |
smart_patient() fetches the contextual Patient only when the current grant
covers patient/Patient.r or user/Patient.r. smart_fhir_user() fetches the
identity reference only from a validated ID token. It accepts openid fhirUser,
a matching user read scope, or patient read permission when the identity is
the contextual Patient. Both stay inside the FHIR base and refuse redirects.
Both request JSON with Accept: application/fhir+json.
A foreign fhirUser reference is reported as context but is never fetched
with this connection's token. FHIR search, write, batch and pagination helpers
are outside these two convenience methods.
Patient IDs and user identity are different: the first identifies a chart,
the second the authenticated user. Neither changes the local connection owner.
General summaries omit all of this data. The application remains responsible
for displaying patient identity clearly and discarding data from an older
context revision. Experimental fhirContext and styling extensions remain
raw data and are not automatically fetched or interpreted.
Location-specific authorization_details apply scope, patient and encounter
overrides only for the configured FHIR base, with omitted fields falling back
to this response's top-level values. Other locations never add destinations.
Malformed details or multiple entries matching this base are rejected.
smart_context() returns a list with version, fhir_base, revision,
changed, patient, encounter, fhirUser and need_patient_banner.
Absent values are NULL. revision increases when interpreted context
changes; key patient-dependent application data by connection ID and this
revision. Refresh omission carries context forward, except that a changed
or cleared patient clears an omitted encounter. Explicit null clears a
field, unless patient access still requires patient context. An initial
launch query never establishes this context.
smart_patient() and smart_fhir_user() return an httr2 response.
smart_client(), OAuthConnection
# Call these helpers with an authorized connection in its owning Shiny session. # Reading the patient requires the corresponding patient or user read scope. read_current_patient <- function(connection) { context <- smart_context(connection) if (is.null(context[["patient"]])) return(NULL) httr2::resp_body_json(smart_patient(connection)) } # The signed-in user can be different from the patient whose chart is open. read_signed_in_user <- function(connection) { httr2::resp_body_json(smart_fhir_user(connection)) }# Call these helpers with an authorized connection in its owning Shiny session. # Reading the patient requires the corresponding patient or user read scope. read_current_patient <- function(connection) { context <- smart_context(connection) if (is.null(context[["patient"]])) return(NULL) httr2::resp_body_json(smart_patient(connection)) } # The signed-in user can be different from the patient whose chart is open. read_signed_in_user <- function(connection) { httr2::resp_body_json(smart_fhir_user(connection)) }
Read and validate the SMART App Launch STU 2.2 discovery document for a configured FHIR server. This returns server metadata for application setup; it does not register an app, choose client credentials, or start authorization.
smart_discover(fhir_base, endpoint_hosts = NULL, allow_http_loopback = FALSE)smart_discover(fhir_base, endpoint_hosts = NULL, allow_http_loopback = FALSE)
fhir_base |
Trusted FHIR base URL, including its complete path, for
example |
endpoint_hosts |
Character vector of exact permitted hostnames for
recognized metadata URLs. |
allow_http_loopback |
Logical, default |
Call once during application setup, outside server(). The request appends
/.well-known/smart-configuration to the full FHIR base, removing its terminal
slash first. The supplied base and returned endpoint/issuer strings are
retained exactly, so discovery does not change protocol identifiers.
This is separate from oauth_provider_oidc_discover(). OAuth-only SMART
metadata need not contain OIDC issuer or signing-key information. Advertised
sso-openid-connect requires both issuer and jwks_uri; their presence
does not validate an identity or fetch OIDC metadata or keys.
Required SMART fields, conditional launch/SSO fields, and advertised
asymmetric-authentication metadata are checked. S256 must be advertised and
plain PKCE is rejected. Client-specific capability, scope, key and algorithm
selection belongs to the application's registration configuration.
scopes_supported is informative, not an exhaustive permission allowlist.
Its array may be empty. An empty token_endpoint_auth_methods_supported
array permits public registrations; confidential registrations still require
their selected method whenever that array is present.
A plain named list with fhir_base (the supplied identifier),
discovery_url (the requested URL), smart_version ("2.2.0", the
validation baseline, not a detected server version), metadata (the parsed
document, with JSON arrays as lists), endpoint_hosts (the normalized
policy), and allow_http_loopback. No client, token, or live cache is stored
in the result. Invalid input or endpoint policy raises a
shinyOAuth_config_error; malformed metadata raises a
shinyOAuth_parse_error; failed HTTP requests raise a
shinyOAuth_http_error.
Supply a trusted, deployment-configured base, never an arbitrary browser query parameter. The base must be an absolute HTTPS URL without userinfo, query, fragment, or ambiguous path syntax. Discovered URLs must be absolute; this reader does not repair relative URLs from legacy servers.
endpoint_hosts applies to the issuer and these top-level URL fields when
present: authorization_endpoint, token_endpoint, jwks_uri,
registration_endpoint, management_endpoint, introspection_endpoint,
revocation_endpoint, userinfo_endpoint,
pushed_authorization_request_endpoint, smart_app_state_endpoint, and
user_access_brand_bundle. Matching uses exact hostnames, independently of
port. This is a discovery policy; it does not authorize resource requests.
The generic shinyOAuth.allowed_hosts option can further restrict these URLs.
The request carries no OAuth credentials. Redirects are refused even when the generic redirect option is enabled. Existing package TLS, timeout, response-size, and retry protections apply. JSON must be an object with no duplicate members; arrays and recognized URL fields have bounded validation. Errors do not include response bodies or returned metadata values.
Unknown extensions, including associated_endpoints, are retained as data
only. Their URLs are not fetched or approved for credential use. No automatic
metadata cache is used: every call reads a new snapshot. Applications should
review metadata changes before replacing configuration; do not rediscover
endpoints during a pending authorization or to reinterpret a retained grant.
SMART STU 2.2 discovery and asymmetric client metadata.
oauth_provider_oidc_discover(), smart_client()
## Not run: site <- smart_discover( "https://ehr.example/fhir/R4", endpoint_hosts = c("ehr.example", "login.example") ) site[["metadata"]][["token_endpoint"]] site[["metadata"]][["capabilities"]] ## End(Not run)## Not run: site <- smart_discover( "https://ehr.example/fhir/R4", endpoint_hosts = c("ehr.example", "login.example") ) site[["metadata"]][["token_endpoint"]] site[["metadata"]][["capabilities"]] ## End(Not run)
Declare which approved SMART clients an EHR launch URL may select. Pass the
result in launch_routes to oauth_connections_ui(). The route is separate
from OAuth callbacks: iss means the FHIR base only here. Both iss and
launch are required, and callback parameters are rejected on this route.
smart_launch_route(path, client_names, max_age = 120)smart_launch_route(path, client_names, max_age = 120)
path |
Absolute application path, such as |
client_names |
Character vector of names from the manager's |
max_age |
Launch handoff lifetime in seconds, 30 to 300, default 120.
The handoff must be consumed and its authorization parameters prepared before
this deadline. Once prepared, login uses the client's |
Initial entry is untrusted. It selects an already configured client by exact FHIR base, performs no discovery, and establishes no healthcare identity. A short-lived encrypted record binds the opaque launch handle to the browser owner and client. A clean continuation proves that owner before a fresh OAuth state/PKCE transaction can begin. Consumed handles cannot be reused to reconnect.
EHR entry supports top-level GET navigation with browser retention in one R
process. Account/session-only retention and iframe deployments are not supported
for EHR entry. Standalone launch supports all manager retention choices.
Up to eight unconsumed launch records are retained per browser owner across all
of a manager's launch routes, with a total limit of 1,000 per manager. Further
entries are rejected without evicting existing tickets. Consuming or expiring
a ticket releases its capacity.
Raw query size is limited to 8 KiB, launch handles to
2 KiB, and the only allowed initial parameters are iss and launch.
Apply ingress rate limits to both launch entry and ordinary pages that create browser owners. The owner quota isolates an existing browser's pending work; it is not a per-person or per-network-client rate limit, since unauthenticated callers can obtain additional browser owners. Configure these controls at a trusted reverse proxy using its verified client address, not arbitrary inbound forwarded headers. Size the owner registry for the admitted traffic window.
The HTTP response uses no-store and no-referrer policy and redirects to an opaque, owner-bound continuation. The package's external JavaScript removes that ticket from browser history before Shiny connects. The ticket alone cannot authorize a connection. No inline script permission or additional CSP nonce is needed for the handoff. Application access logs must also avoid recording raw launch query strings. Normal callback issuer checks and single-use browser/state checks still apply.
A plain route configuration list, with no credentials or live state.
smart_client(), oauth_connections_ui()
## Not run: # hospital is an EHR-mode SMART client; manager uses browser retention. ui <- oauth_connections_ui(app_ui, "health", manager, launch_routes = list(smart_launch_route("/smart/launch", "hospital"))) ## End(Not run)## Not run: # hospital is an EHR-mode SMART client; manager uses browser retention. ui <- oauth_connections_ui(app_ui, "health", manager, launch_routes = list(smart_launch_route("/smart/launch", "hospital"))) ## End(Not run)
For ordinary apps, prefer oauth_ui(ui, id = "auth", client = client);
it includes this dependency, callback handling, and response headers. Use
oauth_form_post_ui() for POST callbacks or oauth_connections_ui() with
a connection manager. No separate use_shinyOAuth() call is needed with
these wrappers.
Add shinyOAuth's JavaScript to a page so oauth_module_server() can redirect
the browser and manage its temporary login cookie. Use this inside an
existing fluidPage() or tagList() when you integrate the browser
dependency directly and configure response headers elsewhere, such as in
your web server or another UI wrapper.
use_shinyOAuth(inject_referrer_meta = TRUE)use_shinyOAuth(inject_referrer_meta = TRUE)
inject_referrer_meta |
If TRUE (default), adds a meta tag to the page:
an instruction asking the browser not to share the page's address when
loading images, scripts, or other files. Some files may start loading
before the browser reads this instruction. Use |
oauth_ui() combines this browser setup with the HTTP header
Referrer-Policy: no-referrer, which prevents callback URLs from being
sent as referrers when page resources load. When using use_shinyOAuth()
directly, set that header in your HTTP response configuration for protection
from the start of page loading; the optional meta tag takes effect later.
oauth_ui(), oauth_form_post_ui(), and oauth_connections_ui() already
include this dependency.
The dependency alone does not provide a callback bridge. Do not load
application or third-party scripts on raw OAuth callback pages; use
oauth_ui(ui, id, client) or a dedicated equivalent endpoint to redirect to
a clean URL before rendering the app. Callback responses must also send
Cache-Control: no-store and Pragma: no-cache.
A tagList that loads the browser code once.
oauth_ui(), oauth_form_post_ui(), oauth_connections_ui(),
oauth_module_server()
# Low-level browser setup for a custom HTTP integration. # Supply equivalent callback handling and response headers separately. # For ordinary apps, use oauth_ui(ui, id = "auth", client = client). ui <- shiny::fluidPage( use_shinyOAuth() # ... )# Low-level browser setup for a custom HTTP integration. # Supply equivalent callback handling and response headers separately. # For ordinary apps, use oauth_ui(ui, id = "auth", client = client). ui <- shiny::fluidPage( use_shinyOAuth() # ... )