Skip to contents

Manage login for a Shiny server module. The module sends the browser to the provider, validates the return, and publishes reactive login status and user information. An oauth_client() supplies the registration and policy; oauth_ui() handles query callbacks and oauth_form_post_ui() handles POST callbacks. The matching UI wrapper includes browser setup.

With an OpenID Connect (OIDC) provider, it also validates the signed-in user's identity.

Usage

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
)

Arguments

id

A name for this Shiny module, such as "auth".

client

The app configuration created with oauth_client().

auto_redirect

If TRUE (default), start login automatically for unauthenticated sessions. If FALSE, call auth[["request_login"]]() to start it.

async

If TRUE, run the module's network work through a background backend. Configure mirai daemons or a non-sequential future plan first; mirai takes priority when both are configured. Default FALSE. future::sequential() runs in the main process. See Asynchronous execution for operations that remain synchronous.

indefinite_session

If TRUE, the module will not automatically clear the token due to access-token expiry or the reauth_after_seconds window, and it will not trigger automatic reauthentication when a token expires or a refresh fails.

reauth_after_seconds

Maximum local login age in seconds, or NULL (default) for no forced reauthentication. Refresh does not reset this timer; indefinite_session = TRUE ignores it. When the limit is reached, local access is cleared and a new login is required. With token targets, API-token expiry alone need not end local login; this argument limits the whole authorization. SMART clients using OIDC require NULL unless indefinite_session = TRUE. See Session lifetime for provider behavior.

refresh_proactively

If TRUE, obtain a replacement access token before expiry when a refresh token is available. Default FALSE. The module schedules refresh at approximately expires_at - refresh_lead_seconds.

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

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, attempt to revoke provider tokens when the Shiny session ends or the module clears the login after expiry, the maximum login age or a refresh failure. A retained login keeps its grant. Default FALSE. Requires a provider revocation_url. Revocation is best effort and runs in the background only with async = TRUE; local cleanup still happens if the provider is unavailable.

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 tab_title_cleaning

request_uri_base_url

Optional absolute base URL used when request_object_mode = "request_uri" publishes Request Objects through Shiny. By default (NULL), shinyOAuth derives the base URL from the current browser-visible app origin, but only when options(shinyOAuth.allowed_hosts = ...) pins the permitted public host. Set this when the authorization server must fetch the published Request Object through a different public host or proxy address than the browser uses, or when you prefer to declare the public origin explicitly. The value must use HTTPS and contain no query string or fragment. Caller-published Request Object URLs require HTTPS even when the ordinary is_ok_host() policy permits HTTP for that host (RFC 9101 Section 5.2). Wrap the app in oauth_ui(ui, id, client) or oauth_form_post_ui() to serve these URLs. Handles contain no Shiny session token and expire within 120 seconds. Shared workers require a shared state store with atomic take().

URL path covered by the login cookie. Default NULL uses "/", covering all app routes. An explicit path, such as "/app", must cover both the starting page and callback, start with /, and contain no semicolons or control characters. On HTTPS the path is always "/" and the cookie always uses the __Host- prefix to prevent sibling-domain cookie injection. Module identifiers isolate cookie names. Custom paths apply only to HTTP development; HTTP cannot provide this protection.

Cookie setting controlling when the browser sends the login cookie on requests from other sites. One of "Strict" (default), "Lax", or "None". "Lax" allows the cookie on top-level cross-site navigations, which some proxy arrangements require. "None" also allows cross-site cookie use in other contexts; it requires HTTPS and sets the cookie's Secure attribute.

refresh_check_interval

Compatibility alias for refresh_check_interval_ms. Supply only one spelling.

Value

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.

  • 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. A custom observer's promise handler runs after browser acknowledgment. Returning invisible(NULL) from that observer lets Shiny process the acknowledgment; awaiting the pending promise in the observer blocks it.

  • auth[["has_browser_token"]](): reports browser-binding readiness, independently of authentication state. The normal login methods manage browser setup automatically.

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

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

Details

Login starts automatically by default. With auto_redirect = FALSE, auth[["request_login"]]() starts the same flow explicitly. The module publishes auth[["authenticated"]] and the accepted token after its configured checks pass.

After login, auth$connection() returns a session-bound reference to the current credentials. connection$request() calls a configured API, while connection$access_token() supplies a token for another R package. See vignette("external-integrations", package = "shinyOAuth") (online guide) for examples.

See the usage vignette for a complete app and instructions for registration, API calls, and deployment.

Asynchronous execution

With async = TRUE and configured mirai workers or a non-sequential future plan, provider requests run outside the main R process. Synchronous network waits otherwise delay Shiny sessions sharing that process.

Browser setup

The UI wrapper handles the registered callback URL. IDE viewers and embedded dashboard previews can block the browser navigation used during login. On Posit Connect and Connect Cloud, the direct app URL provides top-level navigation; Connect Cloud exposes it under Settings > URL.

The module establishes browser binding through cookies, tab-scoped session storage and Web Crypto, and checks it on return. The advanced security vignette describes these browser boundaries and their deployment implications.

Session lifetime

Token refresh does not extend the configured maximum login age. A new login after that age expires requests the client's configured permissions again; explicit auth$reauthorize() keeps any previously reduced permissions.

For OIDC, age-based reauthentication requests max_age = 0 and requires a valid auth_time in the returned identity token. OAuth-only providers cannot guarantee an active provider login, so the age limit ends local access and starts ordinary authorization. With token targets, authenticated describes the retained login, while connection methods check the selected API token.

Advanced worker behavior

Signed-request preparation, pushed authorization requests (PAR) and query JARM verification can run in workers. State-store access and publication of hosted Request Objects stay in the main process. Setup-time discovery, standalone prepare_call() and JARM verification in oauth_form_post_ui() remain synchronous. Configure timeouts for those operations; see the package options reference.

Examples

# 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
    )
  }
}