Overview
‘shinyOAuth’ helps a Shiny app send users to an OAuth 2.0 or OpenID Connect (OIDC) provider, handle the return callback, and keep the flow secure by default. It takes care of:
- Building the login URL and redirecting users when needed
- Creating and checking state, nonce, and PKCE values
- Exchanging the authorization code for tokens and validating the result
- Optionally loading user info and validating ID token signatures/claims
- Optionally refreshing tokens before expiry or triggering re-login
For a full step-by-step protocol breakdown, see the separate
vignette:
vignette("authentication-flow", package = "shinyOAuth").
For advanced security profiles such as mTLS, JAR, PAR, JARM, and
DPoP, see:
vignette("advanced-security", package = "shinyOAuth").
For a detailed explanation of audit logging key events during the
flow, see:
vignette("audit-logging", package = "shinyOAuth").
For a dedicated description of OpenTelemetry support in ‘shinyOAuth’,
see: vignette("opentelemetry", package = "shinyOAuth").
Minimal Shiny module example
Below is a minimal example using a GitHub OAuth app (the same setup
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)use_shinyOAuth() must be included once in your UI. It
loads the JavaScript helper that the login flow depends on. Place it
near the top of your UI, for example inside fluidPage(),
tagList(), or bslib::page().
Open the app in a regular browser, not an IDE viewer. Embedded viewers in tools like RStudio or Positron usually cannot complete the required redirects.
Manual login button variant
This version does the same thing, but waits for the user to click a button before starting login.
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(),
actionButton("login_btn", "Login"),
uiOutput("login")
)
server <- function(input, output, session) {
auth <- oauth_module_server(
"auth",
client,
auto_redirect = FALSE
)
observeEvent(input$login_btn, {
auth$request_login()
})
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)Making authenticated API calls
After login succeeds, you can use the access token to call an API on
the user’s behalf. perform_resource_req() is the easiest
option for most call sites: it builds an authorized httr2
request, performs it, and when the token type is DPoP it
also handles a one-time DPoP-Nonce challenge retry. Use
resource_req() when you need to inspect or customize the
httr2 request before sending it yourself.
The example below calls the GitHub API to fetch 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)
resp <- perform_resource_req(
auth$token,
"https://api.github.com/user/repos"
)
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").
Async mode to keep UI responsive
By default, oauth_module_server() performs network
operations (authorization-code exchange, refresh, userinfo) on the main
R thread. That keeps setup simple, but a slow provider or retry delay
can temporarily block the Shiny worker handling the session.
To avoid blocking, enable async mode and configure an async backend.
‘shinyOAuth’ supports both mirai and future
and auto-detects whichever one you have configured. If both are set up,
mirai takes precedence.
For the future backend, use a non-sequential plan such
as future::multisession() or
future::multicore() where available.
future::sequential() still runs in the same R process, so
it does not move network work off the main R thread.
If you need to keep async = FALSE, you may consider
reducing retry behaviour to limit blocking during provider incidents.
See the global options section for timeout and retry settings.
‘mirai’ async backend (recommended)
# Set up daemons at the top of your app (or in global.R)
mirai::daemons(2)
# Clean up daemons when the app stops
onStop(function() mirai::daemons(0))
server <- function(input, output, session) {
auth <- oauth_module_server(
"auth",
client,
auto_redirect = TRUE,
async = TRUE # Run token exchange & refresh off the main thread
)
# ...
}‘future’ async backend
# Set up workers at the top of your app
future::plan(future::multisession, workers = 2)
server <- function(input, output, session) {
auth <- oauth_module_server(
"auth",
client,
auto_redirect = TRUE,
async = TRUE # Run token exchange & refresh off the main thread
)
# ...
}Logout
To log out the user, call auth$logout(). This clears the
local session, sets auth$error to
"logged_out", reissues a fresh browser token for the next
login attempt, and attempts to revoke tokens at the provider (if a
revocation endpoint is available):
observeEvent(input$logout_btn, {
auth$logout()
})Using response_mode = "form_post"
The response mode determines how the provider returns the
authorization response to the app after the user authenticates. The
effective default is the normal query callback flow, which means the
provider redirects back to the app with query parameters (e.g.,
?code=...&state=...) and shinyOAuth does not send a
response_mode parameter unless you configure one.
For most Shiny apps, query is the preferred response mode because it
works seamlessly with Shiny’s routing and does not require any special
UI handling. It is the default and does not require setting
response_mode explicitly.
For some apps, when your provider explicitly requires or recommends
response_mode = "form_post", you can configure that on the
client. Because Shiny apps do not handle POST callbacks by default, you
need to enable this by wrapping your UI with
oauth_form_post_ui(). This allows the provider to POST the
authorization response back to the app. That wrapper also injects the
shinyOAuth browser dependency automatically, so you do not need a
separate use_shinyOAuth() call in the wrapped UI. The
/callback path below is only an example sub-route; using
the app root is also fine as long as the provider redirect URI matches
the path handled by oauth_form_post_ui(). Here’s how you
can set it up:
This is the plain OAuth/OIDC Form Post Response Mode: the POST body
contains parameters such as code, state,
error, and iss. JWT Secured Authorization
Response Mode (JARM) values such as form_post.jwt use the
same POST bridge, but the body carries a compact JWT
response value instead. In that mode,
oauth_form_post_ui() validates the JARM payload and the
inner sealed state before issuing the one-time callback handle.
library(shiny)
library(shinyOAuth)
provider <- oauth_provider_keycloak(
base_url = "http://localhost:8080",
realm = "shinyoauth"
)
client <- oauth_client(
provider = provider,
client_id = "shiny-public",
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) {
tagList(
tags$p("You are logged in!"),
tags$pre(paste(capture.output(str(auth$token@userinfo)), collapse = "\n"))
)
} else {
tags$p("You are not logged in.")
}
})
}
runApp(
shinyApp(ui, server, uiPattern = ".*"),
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)If your redirect_uri is the app root (like
http://127.0.0.1:8100), uiPattern = ".*" is
usually harmless. If your redirect_uri is a sub-route (like
http://127.0.0.1:8100/callback), use
uiPattern = ".*" so Shiny routes that POST request through
oauth_form_post_ui() before the app returns to its normal
GET flow.
Deploying on Posit Connect Cloud (avoiding embedded deployment)
To be able to handle OAuth callbacks properly, your Shiny app needs to run in a top-level browser context where the provider can redirect back to it with query parameters or a POST body. When your app is embedded inside another page, this does not work.
If you deploy a shinyOAuth app on Posit Connect Cloud,
publish and test it via a top-level (custom) app URL, not the default
embedded content URL (which has the app embedded inside another web
page).
At Posit Connect Cloud (the successor of shinyapps.io), you can configure a top-level URL like so:
- In Posit Connect Cloud, go to the app, then Settings -> URL
- Configure a custom app URL. This can be a claimed Posit URL (free) or your own custom domain
- Use that top-level URL as your redirect URI in your
OAuthClient - Register that same top-level URL as the callback URL at your OAuth provider
- Open the app via that top-level URL
Global options
The package provides several global options to customize behavior. Most apps can stay with the defaults; this section is mainly for cases where you want to tune logging, networking, or a specific advanced behavior.
Observability/logging
-
options(shinyOAuth.audit_hook = function(event){ ... })– receive structured audit and error events -
options(shinyOAuth.audit_include_http = FALSE)– exclude HTTP request details from audit events (default:TRUE) -
options(shinyOAuth.audit_include_raw_session_token = TRUE)– include the rawshiny_session$tokenin native audit-hook payloads. By default, hooks receive onlyshiny_session$session_token_digest -
options(shinyOAuth.audit_redact_http = FALSE)– disable automatic redaction of sensitive data in audit events (default:TRUE). Debug only: raw mode can expose cookies, authorization headers, codes, state values, and client IP addresses -
options(shinyOAuth.audit_digest_key = ...)– shared key for HMAC-SHA256 digests used in audit/OTel attributes. By default, ‘shinyOAuth’ generates a random per-process key when this is not configured -
options(shinyOAuth.otel_tracing_enabled = FALSE)– disable ‘shinyOAuth’ OpenTelemetry span creation and async trace-context propagation. Default:TRUE -
options(shinyOAuth.otel_logging_enabled = FALSE)– disable ‘shinyOAuth’ OpenTelemetry log emission. Default:TRUE
See vignette("audit-logging", package = "shinyOAuth")
for details about audit hooks, and
vignette("opentelemetry", package = "shinyOAuth") for more
details about logs and traces via OpenTelemetry.
Networking/security
-
options(shinyOAuth.leeway = 30)– default clock skew leeway (seconds) for ID tokenexp/iat/nbfchecks and state payloadissued_atfuture check -
options(shinyOAuth.max_id_token_lifetime = 86400)– maximum allowed ID token lifetime in seconds (exp - iat). Tokens whose lifetime exceeds this cap are rejected (OIDC Core §3.1.3.7 rule 9). Default86400(24 hours). Set toInfto disable the check -
options(shinyOAuth.allowed_non_https_hosts = c("localhost", "127.0.0.1", "::1", "[::1]"))- allows hosts to usehttp://scheme instead ofhttps:// -
options(shinyOAuth.allowed_hosts = c())– when non‑empty, restricts accepted hosts to this whitelist -
options(shinyOAuth.allow_hs = TRUE)– opt‑in HMAC validation for ID tokens (HS256/HS384/HS512). Requires a strictly server‑sideclient_secret -
options(shinyOAuth.client_assertion_ttl = 120L)– lifetime in seconds for JWT client assertions used withclient_secret_jwtorprivate_key_jwttoken endpoint authentication. Finite values below 60 seconds are coerced to 60 seconds, finite values above 300 seconds are clamped to 300 seconds, andNAor non-finite values fall back to the 120-second default -
options(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 modes
Note 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").
Extra parameter overrides
Most users can ignore this section. By default, ‘shinyOAuth’ blocks
certain security-critical parameters from being passed via
extra_auth_params, extra_token_params, and
extra_token_headers. This helps prevent accidental
misconfiguration that could break state binding, PKCE, or client
authentication.
response_mode now has a dedicated client argument via
oauth_client(..., response_mode = ...). Prefer that
first-class API over setting
extra_auth_params$response_mode manually.
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,request_uri,request,scope,code_challenge,code_challenge_method,nonce,claims -
requestandrequest_uristay blocked by default because ‘shinyOAuth’ manages them internally for PAR and Request Object flows; leave them reserved unless you are intentionally taking responsibility for a fully custom advanced flow. -
options(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_type -
options(shinyOAuth.unblock_token_headers = c("authorization"))– allows overriding the specified token exchange headers (case-insensitive). Default blocked:Authorization,Cookie
Async timeout (mirai)
-
options(shinyOAuth.async_timeout = 10000)– per-task timeout in milliseconds for mirai async tasks. When using mirai with dispatcher (the default), timed-out tasks are automatically cancelled and resolve as a mirai error. Default isNULL(no timeout). Ignored when falling back to the ‘future’ backend
Async condition replay
-
options(shinyOAuth.replay_async_conditions = FALSE)– whenFALSE, warnings and messages captured from async workers are silently discarded instead of being re-emitted on the main R process. Default isTRUE(replay all captured conditions). Useful if worker diagnostics are too noisy or handled separately viaaudit_hook
Token lifetime fallback
-
options(shinyOAuth.default_expires_in = 3600)– fallback token lifetime (in seconds) when the provider omitsexpires_infrom the token response
HTTP settings (timeout, retries, user agent)
-
options(shinyOAuth.timeout = 5)– default HTTP timeout (seconds) applied to all outbound requests (discovery, JWKS, token exchange, userinfo). Increase if your provider/network is slow -
options(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 jitter -
options(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 retried -
options(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 format -
options(shinyOAuth.allow_redirect = FALSE)– whenFALSE(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. Set toTRUEonly when you deliberately accept that redirect-following risk for a specific deployment; this opt-in is honored in all sessions -
options(shinyOAuth.max_body_bytes = 1048576)– maximum response body size (bytes, default 1 MiB) accepted from OAuth endpoints (token, introspection, userinfo, discovery, JWKS). Curl aborts the transfer early whenContent-Lengthexceeds this limit; a post-download guard catches chunked responses. Increase if a provider legitimately returns larger payloads
State store
-
options(shinyOAuth.allow_non_atomic_state_store = TRUE)– allow non-atomic$get()+$remove()fallback for shared state stores (e.g.,cachem::cache_disk()) that do not implement$take(). By default, ‘shinyOAuth’ errors when a non-cachem::cache_mem()store lacks$take(), because the non-atomic fallback cannot guarantee single-use state consumption under concurrent access (TOCTOU replay window). Setting this option toTRUEdowngrades the error to a one-time warning and allows the fallback to proceed. Not recommended for production without additional replay protection.
Size caps
State envelope
-
options(shinyOAuth.state_max_token_chars = 8192)– maximum allowed length of the base64url-encodedstatequery parameter -
options(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 wrapper -
options(shinyOAuth.state_max_ct_bytes = 8192)– maximum decoded byte size of the ciphertext before attempting AES-GCM decrypt
These prevent maliciously large state parameters from causing excessive CPU or memory usage during decoding and decryption.
Callback query
-
options(shinyOAuth.callback_max_code_bytes = 4096)– maximum byte length of thecodequery parameter -
options(shinyOAuth.callback_max_state_bytes = 8192)– maximum byte length of thestatequery parameter (outer token string) -
options(shinyOAuth.callback_max_error_bytes = 256)– maximum byte length of theerrorquery parameter -
options(shinyOAuth.callback_max_error_description_bytes = 4096)– maximum byte length of theerror_descriptionquery parameter -
options(shinyOAuth.callback_max_error_uri_bytes = 2048)– maximum byte length of theerror_uriquery parameter -
options(shinyOAuth.callback_max_iss_bytes = 2048)– maximum byte length of theissquery parameter (RFC 9207 issuer identification) -
options(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 thebrowser_tokenargument accepted byhandle_callback() -
options(shinyOAuth.callback_max_form_post_body_bytes = <derived>)– maximum byte length of the rawform_postcallback body before parsing -
options(shinyOAuth.callback_max_form_post_handle_bytes = 128)– maximum byte length of the transientshinyOAuth_form_posthandle query parameter -
options(shinyOAuth.callback_max_form_post_id_bytes = 256)– maximum byte length of the transientshinyOAuth_form_post_idmodule-id query parameter
These apply before any hashing/auditing/state parsing, and exist to
prevent memory/log amplification from extremely large callback URLs or
form_post bodies.
Development/debugging
-
options(shinyOAuth.skip_browser_token = TRUE)– skip browser cookie binding in tests or interactive sessions -
options(shinyOAuth.skip_id_sig = TRUE)– skip ID token signature verification in tests or interactive sessions -
options(shinyOAuth.allow_unsigned_userinfo_jwt = TRUE)– accept unsigned (alg=none) UserInfo JWTs in tests or interactive sessions; outside those contexts ‘shinyOAuth’ errors instead of honoring it -
options(shinyOAuth.debug = TRUE)– re‑raise errors during token exchange -
options(shinyOAuth.expose_error_body = TRUE)– include sanitized HTTP bodies (may reveal details)
Don’t enable these options in production. They disable key security checks or alter error behavior, and are intended for local testing/debugging only.
Browser cookie & preventing XSS
oauth_module_server() binds the browser and server
session with a short‑lived cookie that must be readable from client‑side
JavaScript to bridge values into Shiny.
The cookie ensures that the same browser which initiated login is the one receiving the callback. This specifically prevents an attack where an attacker tricks a user into clicking a link which initiates login for the attacker’s account, confusing the user into logging in as the attacker (login confusion).
The cookie is set with the HttpOnly flag disabled so
that it can be read by JavaScript. This is necessary to bridge the
cookie value into Shiny. However, this means that if your app has XSS
vulnerabilities, an attacker could read the cookie too.
While this is a relatively limited attack vector, you should still
take care to prevent XSS vulnerabilities in your app. An important
mitigation is to sanitize user inputs before rendering them in the UI
(e.g., using htmltools::htmlEscape()).
Multi‑process deployments: share state store, key, and policy
When you run multiple Shiny R processes (e.g., multiple workers, Shiny Server Pro, RStudio Connect, Docker/Kubernetes replicas, or any non‑sticky load balancer), you must ensure that:
- All workers share the same state store with atomic single-use
semantics. Use
custom_cache()with an atomic$take()method backed by a shared store (e.g., RedisGETDEL, SQLDELETE ... RETURNING). Plaincachem::cache_disk()is not safe as a shared state store because its$get()+$remove()are not atomic and may allow replay attacks under concurrent access; the defaultcachem::cache_mem()is per‑process only and is not shared. See?custom_cachefor details on implementing$take(); - All workers share the same state key (e.g., read from environment variable; by default, a random key is generated per client instance which is then not shared);
- All workers use the same effective
OAuthClient/OAuthProvidersettings which are included in the fingerprint used for state binding.
This is because during the authorization code + PKCE flow,
‘shinyOAuth’ creates an encrypted “state envelope” which is stored in a
cache (the state_store) and echoed back via the state query
parameter. The envelope is sealed with AES‑GCM using your state_key. If
the callback lands on a different worker than the one that initiated
login, that worker must be able to both read the cached entry and
decrypt the envelope using the same key. If workers have different keys,
decryption will fail and the login flow will abort with a state
error.
When providing a custom state key, please ensure it has high entropy (minimum 32 characters or 32 raw bytes; recommended 64–128 characters) to prevent offline guessing attacks against the encrypted state. Do not use short or human‑memorable passphrases.
Security checklist
Below is a checklist of things you may want to think about when bringing your app to production:
- Use HTTPS everywhere in production
- Verify issuer used in your provider is correct
- In your
OAuthClientandOAuthProvider, set as many of the security options as your provider supports - Have your
OAuthClientrequest the minimum scopes necessary; give your app registration only the permissions it needs - Do not show
$error_descriptionto your users; never expose tokens in UI or logs - Keep secrets safe in environment variables (e.g.,
OAUTH_CLIENT_ID,OAUTH_CLIENT_SECRET) - Sanitize user inputs before rendering them in the UI (e.g., using
htmltools::htmlEscape()) - Make use of audit logging (see
vignette("audit-logging", package = "shinyOAuth")) and monitor these logs - Use a provider which enforces strong authentication (e.g., multi-factor authentication)
- Set Content Security Policy (CSP) headers to restrict resource loading and mitigate XSS attacks; (requires middleware; can’t be done in Shiny)
- Log IP addresses of those accessing your app (requires middleware; can’t be done in Shiny)
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).