Usage

Overview

‘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").

Minimal Shiny module example

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()).

Manual login button variant

Below is an example where the user clicks a button to start the login process instead of being redirected immediately on page load.

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)

Making authenticated API calls

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").

Async mode to keep UI responsive

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’.

Global options

The package provides several global options to customize behavior. Below is a list of all available options.

Observability/logging

See vignette("audit-logging", package = "shinyOAuth") for details about audit and trace hooks.

Networking/security

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").

HTTP settings (timeout, retries, user agent)

Development softening

Don’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.

State envelope size caps

These prevent maliciously large state parameters from causing excessive CPU or memory usage during decoding and decryption.

Multi‑process deployments: share state store & key

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:

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:

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).