‘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 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)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()).
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)For an example application which fetches data from the Spotify web
API, see:
vignette("example-spotify", package = "shinyOAuth").
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’.
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 streamSee vignette("audit-logging", package = "shinyOAuth")
for details about audit and trace hooks.
options(shinyOAuth.leeway = 30) – default ID token
exp/iat leeway secondsoptions(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 = 300L) –
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 300 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").
options(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.skip_browser_token = TRUE) – skip
browser cookie bindingoptions(shinyOAuth.skip_id_sig = TRUE) – skip ID token
signature verificationDon’t enable these in production. They disable key security checks
and are intended for local testing 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.
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 logsWhile 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).