‘shinyOAuth’ implements provider‑agnostic OAuth 2.0 and OpenID Connect (OIDC) authorization/authentication for Shiny apps, with modern S7 classes and secure defaults. It streamlines the full authorization/authentication flow, including:
For a full step-by-step protocol breakdown, see the separate
vignette:
vignette("authentication-flow", package = "shinyOAuth").
For a detailed explanation of audit logging key events during the
flow, see:
vignette("audit-logging", package = "shinyOAuth").
Below is a minimal example using a GitHub’s OAuth 2.0 app (same as
shown in the README). Register an OAuth 2.0 application at https://github.com/settings/developers and set
environment variables GITHUB_OAUTH_CLIENT_ID and
GITHUB_OAUTH_CLIENT_SECRET.
library(shiny)
library(shinyOAuth)
provider <- oauth_provider_github()
client <- oauth_client(
provider = provider,
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")
)
ui <- fluidPage(
# Include JavaScript dependency:
use_shinyOAuth(),
# Render login status & user info:
uiOutput("login")
)
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),
port = 8100,
launch.browser = FALSE
)
# Open the app in your regular browser at http://127.0.0.1:8100
# (viewers in RStudio/Positron/etc. cannot perform necessary redirects)Note that ui includes use_shinyOAuth() to
load the necessary JavaScript dependency. Always place
use_shinyOAuth() in your UI; otherwise, the module will not
function. You may place it near the top-level of your UI (e.g., inside
fluidPage(), tagList(), or
bslib::page()).
Note also that you must access the app in a regular browser. This is because the necesarry redirects that the browser must perform can usually not be handled inside embedded viewers of IDEs like RStudio or Positron.
Once authenticated, you may want to call an API on behalf of the user
using the access token. Use client_bearer_req() to quickly
build an authorized ‘httr2’ request with the correct Bearer token. See
the example app below; it calls the GitHub API to obtain the user’s
repositories.
library(shiny)
library(shinyOAuth)
provider <- oauth_provider_github()
client <- oauth_client(
provider = provider,
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")
)
ui <- fluidPage(
use_shinyOAuth(),
uiOutput("ui")
)
server <- function(input, output, session) {
auth <- oauth_module_server(
"auth",
client,
auto_redirect = TRUE
)
repositories <- reactiveVal(NULL)
observe({
req(auth$authenticated)
# Example additional API request using the access token
# (e.g., fetch user repositories from GitHub)
req <- client_bearer_req(auth$token, "https://api.github.com/user/repos")
resp <- httr2::req_perform(req)
if (httr2::resp_is_error(resp)) {
repositories(NULL)
} else {
repos_data <- httr2::resp_body_json(resp, simplifyVector = TRUE)
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 (!is.null(repos)) {
tags$ul(
Map(function(url, name) {
tags$li(tags$a(href = url, target = "_blank", name))
}, repos$html_url, repos$full_name)
)
} else {
tags$p("Loading repositories...")
}
))
}
return(tags$p("You are not logged in."))
})
}
runApp(
shinyApp(ui, server),
port = 8100,
launch.browser = FALSE
)
# Open the app in your regular browser at http://127.0.0.1:8100
# (viewers in RStudio/Positron/etc. cannot perform necessary redirects)For an example application which fetches data from the Spotify web
API, see:
vignette("example-spotify", package = "shinyOAuth").
By default, oauth_module_server() considers a login
complete once the callback has been validated and token retrieval plus
any configured OIDC checks have succeeded.
If your provider supports RFC 7662 token introspection, you can
optionally add an extra login-time validation step by enabling
introspect = TRUE when creating your
oauth_client().
When enabled, the module calls the provider introspection endpoint
during callback processing and requires the response to indicate
active = TRUE. If introspection is unsupported by the
provider or the introspection request fails, the login is aborted and
$authenticated is not set to TRUE.
You can optionally request additional checks via
introspect_elements:
"sub" – require the introspected sub to
match the session subject (from ID token sub when
available; otherwise userinfo sub when available)"client_id" – require the introspected
client_id to match your OAuth client id"scope" – validate returned scopes against requested
scopes; this follows the client’s scope_validation mode
("strict" errors, "warn" warns,
"none" skips scope checks)(Note that not all providers may return each of these fields in introspection responses.)
# Example with introspection enabled
client <- oauth_client(
provider = provider,
client_id = Sys.getenv("OAUTH_CLIENT_ID"),
client_secret = Sys.getenv("OAUTH_CLIENT_SECRET"),
redirect_uri = "http://127.0.0.1:8100",
introspect = TRUE,
introspect_elements = c("sub", "client_id", "scope")
)
auth <- oauth_module_server("auth", client, auto_redirect = TRUE)By default, oauth_module_server() performs network
operations (authorization code exchange, refresh, userinfo) on the main
R thread. During transient network errors the package retries with
backoff, and sleeping on the main thread can block the Shiny event loop
for the worker process.
To avoid blocking, enable async mode and configure a future backend:
future::plan(future::multisession)
server <- function(input, output, session) {
auth <- oauth_module_server(
"auth",
client,
auto_redirect = TRUE,
async = TRUE # Run token exchange & refresh off the main thread
)
# ...
}If you need to keep async = FALSE, you may consider
reducing retry behaviour to limit blocking during provider incidents.
See ‘Global options’ and then ‘HTTP timeout/retries’.
To log out the user, call auth$logout(). This clears the
local session and attempts to revoke tokens at the provider (if a
revocation endpoint is available):
Revocation uses RFC 7009 and runs asynchronously when
oauth_module_server(async = TRUE). See
?revoke_token for programmatic use outside the module.
To revoke tokens when the Shiny session ends (e.g., browser tab
closed, timeout), set revoke_on_session_end = TRUE:
Note: this is a best-effort operation; network failures or provider unavailability may prevent revocation. Combine with appropriate token lifetimes on the provider side for defense in depth.
The package provides several global options to customize behavior. Below is a list of all available options.
options(shinyOAuth.print_errors = TRUE) – concise error
lines (interactive / tests only)options(shinyOAuth.print_traceback = TRUE) – include
backtraces (interactive / tests only)options(shinyOAuth.expose_error_body = TRUE) – include
sanitized HTTP bodies (may reveal details)options(shinyOAuth.trace_hook = function(event){ ... })
– structured events (errors, http, etc.)options(shinyOAuth.audit_hook = function(event){ ... })
– separate audit streamoptions(shinyOAuth.audit_include_http = FALSE) –
exclude HTTP request details from audit events (default:
TRUE)options(shinyOAuth.audit_redact_http = FALSE) – disable
automatic redaction of sensitive data in audit events (default:
TRUE)options(shinyOAuth.audit_digest_key = ...) – key for
HMAC-SHA256 audit digestsSee vignette("audit-logging", package = "shinyOAuth")
for details about audit and trace hooks.
options(shinyOAuth.leeway = 30) – default clock skew
leeway (seconds) for ID token
exp/iat/nbf checks and state
payload issued_at future checkoptions(shinyOAuth.allowed_non_https_hosts = c("localhost", "127.0.0.1", "::1"))
- allows hosts to use http:// scheme instead of
https://options(shinyOAuth.allowed_hosts = c()) – when
non‑empty, restricts accepted hosts to this whitelistoptions(shinyOAuth.allow_hs = TRUE) – opt‑in HMAC
validation for ID tokens (HS256/HS384/HS512). Requires a strictly
server‑side client_secretoptions(shinyOAuth.client_assertion_ttl = 120L) –
lifetime in seconds for JWT client assertions used with
client_secret_jwt or private_key_jwt token
endpoint authentication. Values below 60 seconds are coerced up to a
safe minimum; default is 120 secondsoptions(shinyOAuth.state_fail_delay_ms = c(10, 30)) –
adds a small randomized delay (in milliseconds) before any state
validation failure (e.g., malformed token, IV/tag/ciphertext issues, or
GCM authentication failure). This helps reduce timing side‑channels
between different failure modesNote on allowed_hosts: patterns support globs
(*, ?). Using a catch‑all like
"*" matches any host and effectively disables endpoint host
restrictions (scheme rules still apply). Avoid this unless you truly
intend to accept any host; prefer pinning to your domain(s), e.g.,
c(".example.com").
By default, shinyOAuth blocks certain security‑critical parameters
from being passed via extra_auth_params,
extra_token_params, and extra_token_headers.
This prevents accidental misconfiguration that could break state
binding, PKCE integrity, or client authentication.
If you have a specific, advanced use case where you need to override one of these blocked parameters, you can unblock them using the following options:
options(shinyOAuth.unblock_auth_params = c("redirect_uri"))
– allows overriding the specified authorization URL parameters. Default
blocked: response_type, client_id,
redirect_uri, state, scope,
code_challenge, code_challenge_method,
nonceoptions(shinyOAuth.unblock_token_params = c(...)) –
allows overriding the specified token exchange parameters. Default
blocked: grant_type, code,
redirect_uri, code_verifier,
client_id, client_secret,
client_assertion, client_assertion_typeoptions(shinyOAuth.unblock_token_headers = c("authorization"))
– allows overriding the specified token exchange headers
(case-insensitive). Default blocked: Authorization,
Cookieoptions(shinyOAuth.timeout = 5) – default HTTP timeout
(seconds) applied to all outbound requests (discovery, JWKS, token
exchange, userinfo). Increase if your provider/network is slowoptions(shinyOAuth.retry_max_tries = 3L) – maximum
attempts for transient failures (network errors, 408, 429, 5xx)options(shinyOAuth.retry_backoff_base = 0.5) – base
backoff in seconds used for exponential backoff with jitteroptions(shinyOAuth.retry_backoff_cap = 5) – per‑attempt
cap on backoff seconds (before jitter)options(shinyOAuth.retry_status = c(408L, 429L, 500:599))
– HTTP statuses considered transient and retriedoptions(shinyOAuth.user_agent = "shinyOAuth/<version> R/<version> httr2/<version>")
– override the default User‑Agent header applied to all outbound
requests. By default this string is built dynamically from the installed
package/runtime versions; set a custom string here if your organization
requires a specific formatoptions(shinyOAuth.allow_redirect = FALSE) – when
FALSE (default), all sensitive HTTP requests (token
exchange, refresh, introspection, revocation, userinfo, OIDC discovery,
JWKS) refuse to follow redirects and reject 3xx responses. This prevents
authorization codes, tokens, and PKCE verifiers from leaking to redirect
targets. Only set to TRUE if your provider legitimately
requires redirect-followingoptions(shinyOAuth.skip_browser_token = TRUE) – skip
browser cookie bindingoptions(shinyOAuth.skip_id_sig = TRUE) – skip ID token
signature verificationoptions(shinyOAuth.debug = TRUE) – re‑raise errors
during token exchangeDon’t enable these in production. They disable key security checks or
alter error behavior, and are intended for local testing/debugging only.
Use error_on_softened() at startup to fail fast if
softening flags are enabled in an environment where they should not
be.
options(shinyOAuth.state_max_token_chars = 8192) –
maximum allowed length of the base64url-encoded state query
parameteroptions(shinyOAuth.state_max_wrapper_bytes = 8192) –
maximum decoded byte size of the outer JSON wrapper (before
parsing)options(shinyOAuth.state_max_ct_b64_chars = 8192) –
maximum allowed length of the base64url-encoded ciphertext inside the
wrapperoptions(shinyOAuth.state_max_ct_bytes = 8192) – maximum
decoded byte size of the ciphertext before attempting AES-GCM
decryptThese prevent maliciously large state parameters from causing excessive CPU or memory usage during decoding and decryption.
options(shinyOAuth.callback_max_code_bytes = 4096) –
maximum byte length of the code query parameteroptions(shinyOAuth.callback_max_state_bytes = 8192) –
maximum byte length of the state query parameter (outer
token string)options(shinyOAuth.callback_max_error_bytes = 256) –
maximum byte length of the error query parameteroptions(shinyOAuth.callback_max_error_description_bytes = 4096)
– maximum byte length of the error_description query
parameteroptions(shinyOAuth.callback_max_query_bytes = <derived>)
– maximum total byte length of the raw callback query string (pre-parse
guard)options(shinyOAuth.callback_max_browser_token_bytes = 256)
– maximum byte length of the browser_token argument
accepted by handle_callback()These apply before any hashing/auditing/state parsing, and exist to prevent memory/log amplification from extremely large callback URLs.
Below is a checklist of things you may want to think about when bringing your app to production:
OAuthProvider, set as many of the security
options as possible; for instance, set
jwks_host_issuer_match/jwks_host_allow_only
(if your provider uses a different host for JWKS)OAuthClient request the minimum scopes
necessary; give your app registration only the permissions it needs$error_description to your users; never
expose tokens in UI or logsOAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET)htmltools::htmlEscape())vignette("audit-logging", package = "shinyOAuth")) and
monitor these logsrevoke_on_session_end = TRUE)While this R package has been developed with care and the OAuth 2.0/OIDC protocols contain many security features, no guarantees can be made in the realm of cybersecurity. For highly sensitive applications, consider a layered (‘defense-in-depth’) approach to security (for example, adding an IP whitelist as an additional safeguard).