Usage

Overview

Use shinyOAuth to let users sign in with an existing service and grant your app permission to call its API, without collecting their password. OAuth controls API access; OpenID Connect (OIDC) adds identity checks so the app can verify who signed in.

In R, configure the service and your app registration, then add the Shiny UI wrapper and server module. The module manages login and tokens; oauth_ui() handles the browser’s return to the app.

The examples use a GitHub OAuth App. Install shinyOAuth with install.packages("shinyOAuth"). For the protocol flow and validation rules, see Authentication flow.

Provider, client, and token objects

A provider describes the service users sign in with. A client describes your app’s registration with that service. After login, a token holds the credentials your app uses to call the service and any available user information.

Concept In your app
Provider A helper such as oauth_provider_github() supplies the service’s settings.
Client oauth_client() combines those settings with your app’s credentials and permissions.
Token auth[["token"]] contains the result of a successful login.

The redirect URI, also called the callback URL, is where the browser returns after login. Register the same address with the provider and in oauth_client(). Scopes name the permissions to request, such as read:user; the provider defines which names are available.

Create your provider and client outside server() so they remain available when the browser returns from login. Create the module inside server() so each user has their own login state.

GitHub app registration

  1. Register an OAuth App in GitHub’s developer settings. Set both the homepage URL and authorization callback URL to http://127.0.0.1:8100 for this local example.

  2. Store the app’s client ID and client secret in your R environment. You can open your user .Renviron with file.edit(path.expand("~/.Renviron")) and add:

    GITHUB_OAUTH_CLIENT_ID=your-client-id
    GITHUB_OAUTH_CLIENT_SECRET=your-client-secret

    Restart R after saving so the environment variables become available.

Minimal Shiny module example

Save the following code as app.R and run it. Open http://127.0.0.1:8100 in a regular browser. Use the registered address; switching between localhost and 127.0.0.1 can interrupt login.

Deploying to Posit Connect or Connect Cloud? Test login at the direct app URL in a new browser tab or window. The dashboard’s embedded preview can prevent OAuth redirects. See Posit deployment instructions.

library(shiny)
library(shinyOAuth)

# Configure these once, outside server().
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 <- oauth_ui(fluidPage(
  h2("My app"),
  textOutput("greeting")
), id = "auth", client = client)

server <- function(input, output, session) {
  auth <- oauth_module_server("auth", client)

  output[["greeting"]] <- renderText({
    req(auth[["authenticated"]])
    paste("Hello,", auth[["token"]]@userinfo[["login"]])
  })
}

runApp(shinyApp(ui, server), port = 8100, launch.browser = FALSE)

The browser opens GitHub’s login or permission page, then returns to your app and displays your GitHub username. Use a regular browser: IDE viewers may prevent the redirects needed for login.

Authentication state and user information

auth is a Shiny reactiveValues object. Read it inside render*(), reactive(), or observe*(), as you would read other reactive values.

The token is an S7 object: access its properties with @, as in auth[["token"]]@userinfo. See OAuthToken for the available properties.

req(auth[["authenticated"]]) suspends the example’s output while login is incomplete.

Manual login and logout buttons

The default is to start login automatically. To let users choose when to sign in, replace the example’s UI and server with:

ui <- oauth_ui(fluidPage(
  actionButton("login", "Sign in"),
  actionButton("logout", "Sign out"),
  textOutput("status")
), id = "auth", client = client)

server <- function(input, output, session) {
  auth <- oauth_module_server("auth", client, auto_redirect = FALSE)

  observeEvent(input[["login"]], auth[["request_login"]]())
  observeEvent(input[["logout"]], auth[["logout"]]())

  output[["status"]] <- renderText({
    if (isTRUE(auth[["authenticated"]])) {
      paste("Signed in as", auth[["token"]]@userinfo[["login"]])
    } else {
      "You are signed out. Use Sign in to continue."
    }
  })
}

auth[["logout"]]() clears the app’s local login and attempts to revoke its tokens if the provider supports revocation. It does not sign the user out of their GitHub, Google, or other provider account. The provider may therefore remember them on their next visit.

Authenticated API requests

Once the user has granted permission, your app can request data from the service. perform_resource_req() attaches the access token for you. Add tableOutput("repositories") to the UI and this output to server():

output[["repositories"]] <- renderTable({
  req(auth[["authenticated"]])

  repos <- tryCatch({
    response <- perform_resource_req(
      auth[["token"]],
      "https://api.github.com/user/repos",
      query = list(per_page = 10)
    )
    httr2::resp_check_status(response)
    httr2::resp_body_json(response, simplifyVector = TRUE)
  }, error = function(e) NULL)

  validate(need(!is.null(repos), "Could not load repositories. Try again later."))
  validate(need(length(repos) > 0, "No repositories to show."))
  repos[, c("name", "private"), drop = FALSE]
})

This example loads one page; further pages depend on the API. For another complete app, see the Spotify example. The request-helper reference covers prepared requests and transport settings.

Asynchronous execution

By default, the module’s network requests run in the app’s R process, so a slow provider can block other sessions sharing that process. To run those requests in background R processes, install mirai and promises, then configure workers before server():

mirai::daemons(2)
shiny::onStop(function() mirai::daemons(0))

server <- function(input, output, session) {
  auth <- oauth_module_server("auth", client, async = TRUE)
  # Add your outputs and observers here.
}

Alternatively, configure future::plan(future::multisession, workers = 2) and use async = TRUE. This backend requires promises version 1.3.3 or newer. If both backends are configured, mirai takes priority. future::sequential() runs in the same R process and does not avoid blocking.

For an app using future instead of mirai, configure its worker plan before starting the server and release the workers when the app stops:

future::plan(future::multisession, workers = 2)
shiny::onStop(function() future::plan(future::sequential))

server <- function(input, output, session) {
  auth <- oauth_module_server("auth", client, async = TRUE)
  # Add your outputs and observers here.
}

async = TRUE covers the module’s operations. API calls in your own outputs still run where you call them, and discovery during app setup stays synchronous. See oauth_module_server() for advanced exceptions and the options reference for timeouts and retries.

Token expiry and session length

Access tokens usually expire. If the provider supplies a refresh token, the module can obtain a replacement before expiry with refresh_proactively = TRUE. Otherwise, users need to sign in again when their token expires.

Use reauth_after_seconds to set a maximum time since interactive login; refreshing a token does not restart that timer. For OIDC, the module also requests a fresh provider login and checks its time. OAuth-only providers can only be given an ordinary authorization request.

indefinite_session = TRUE retains local login state after token expiry or refresh failure, and disables the reauth_after_seconds limit.

Provider configuration and OIDC discovery

Use a built-in helper when available, such as oauth_provider_google(), oauth_provider_microsoft(), or oauth_provider_keycloak(). Each helper’s help page describes its setup. For an OpenID Connect service, discovery reads its published settings:

provider <- oauth_provider_oidc_discover(
  issuer = "https://login.example.com"
)
client <- oauth_client(
  provider = provider,
  client_id = Sys.getenv("OAUTH_CLIENT_ID"),
  client_secret = Sys.getenv("OAUTH_CLIENT_SECRET"),
  redirect_uri = "https://my-app.example.com",
  scopes = c("openid", "profile", "email")
)

Replace the example URLs and credentials with your own registration. An issuer is the provider’s identifier URL; copy it from the provider’s configuration. Discovery makes a network request, so run it during app setup. If your registration specifies a client authentication method, supply the matching token_auth_style to the provider helper. Discovery lists the service’s capabilities; your registration determines which method and credentials to use.

For OIDC, read validated identity from auth[["token"]]@id_token_claims and check auth[["token"]]@id_token_validated. GitHub and Spotify instead fetch profiles through their own APIs. An access token alone does not verify identity; see Authentication flow.

Deployment

Replace the local callback URL with the app’s public HTTPS URL, and register that same URL with the provider. Open the app directly in a browser tab. An app embedded in another page may not be able to complete login.

Callback and browser behavior

oauth_ui() and oauth_form_post_ui() process callback data before rendering application UI, with no-store and no-referrer response headers. The server module manages credentials and browser binding automatically. The advanced security guide describes the browser and hosting boundaries in more detail.

Advanced deployment

Posit Connect and Connect Cloud

Test OAuth login in a new browser tab or window, outside the dashboard’s embedded app preview.

  1. On Connect Cloud, copy the app’s sharing URL from Settings > URL, such as https://<content-id>.share.connect.posit.cloud, or your configured custom URL. See the URL settings guide. On Posit Connect, use the app’s direct content URL.
  2. Paste that URL into a new browser tab or window and start login there. The administrative page at connect.posit.cloud/.../content/... is not the direct app URL or an OAuth callback URL.
  3. Set redirect_uri to the exact public callback URL registered with your provider. In the minimal example this is the app URL. With a dedicated route such as /callback, retain that callback path in the registration, but open the app’s entry URL to start login.

Multiple R processes

A login can start on one R process and return to another. Those processes need the same provider/client settings, a shared pending-login store, and the same secret state_key to read the saved details. Load that key from deployment secret storage; it needs at least 32 random bytes.

The shared store must read and remove a pending record as one operation, so two processes cannot accept the same login response. Plain disk caching does not provide that guarantee. Use custom_cache() to connect an appropriate backend, following its contract for POST callbacks and hosted Request Objects.

Local Keycloak development

For a local Keycloak server using HTTP, opt in before creating the provider:

options(shinyOAuth.allow_insecure_oidc_loopback = TRUE)
provider <- oauth_provider_keycloak(
  base_url = "http://localhost:8080", realm = "shinyoauth"
)

This option is for local development only. Production provider URLs need HTTPS. The ordinary HTTP host option allows local app addresses; it does not relax OIDC discovery.

Further configuration

The oauth_module_server() examples include complete apps for automatic login, a manual login button, and fetching GitHub repositories with the user’s access token. The oauth_provider() examples cover manual OAuth/OIDC setup, discovery, and named providers; the Microsoft example also shows authentication-state summaries and error handling.