---
title: "Usage"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Usage}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

## 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:

- Building authorization URLs and redirecting unauthenticated users
- State, nonce, and PKCE generation, sealing, and verification
- Authorization code exchange and token validation
- Optional userinfo retrieval & ID token signature/claims validation
- Proactive token refresh and re‑authentication triggers

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 2.0 application at <https://github.com/settings/developers> and set
environment variables `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_CLIENT_SECRET`.

```{r, eval = FALSE}
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)
```

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

Note also that you must access the app in a regular browser.
This is because the necesarry redirects that the browser must perform can usually
not be handled inside embedded viewers of IDEs like RStudio or Positron.

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

```{r, eval = FALSE}
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

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.

```{r, eval = FALSE}
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,
  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")`.

## Token introspection (optional)

By default, `oauth_module_server()` considers a login complete once the callback has been validated
and token retrieval plus any configured OIDC checks have succeeded.

If your provider supports RFC 7662 token introspection, you can optionally add an extra
login-time validation step by enabling `introspect = TRUE` when creating your `oauth_client()`.

When enabled, the module calls the provider introspection endpoint during callback processing and
requires the response to indicate `active = TRUE`. If introspection is unsupported by the provider
or the introspection request fails, the login is aborted and `$authenticated` is not set to `TRUE`.

You can optionally request additional checks via `introspect_elements`:

- `"sub"` – require the introspected `sub` to match the session subject (from ID token `sub` when
  available; otherwise userinfo `sub` when available)
- `"client_id"` – require the introspected `client_id` to match your OAuth client id
- `"scope"` – validate returned scopes against requested scopes; this follows the client's
  `scope_validation` mode (`"strict"` errors, `"warn"` warns, `"none"` skips scope checks)

(Note that not all providers may return each of these fields in introspection responses.)

```r
# Example with introspection enabled
client <- oauth_client(
  provider = provider,
  client_id = Sys.getenv("OAUTH_CLIENT_ID"),
  client_secret = Sys.getenv("OAUTH_CLIENT_SECRET"),
  redirect_uri = "http://127.0.0.1:8100",
  introspect = TRUE,
  introspect_elements = c("sub", "client_id", "scope")
)

auth <- oauth_module_server("auth", client, auto_redirect = TRUE)
```

## 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:

```r
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'.

## Logout

To log out the user, call `auth$logout()`. This clears the local session and
attempts to revoke tokens at the provider (if a revocation endpoint is available):

```r
observeEvent(input$logout_btn, {
  auth$logout()
})
```

Revocation uses RFC 7009 and runs asynchronously when `oauth_module_server(async = TRUE)`.
See `?revoke_token` for programmatic use outside the module.

### Automatic revocation on session end

To revoke tokens when the Shiny session ends (e.g., browser tab closed, timeout), 
set `revoke_on_session_end = TRUE`:

```r
auth <- oauth_module_server(
  "auth",
  client = client,
  revoke_on_session_end = TRUE
)
```

Note: this is a best-effort operation; network failures or provider unavailability may prevent revocation. 
Combine with appropriate token lifetimes on the provider side for defense in depth.
  
## Global options

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

### Observability/logging

- `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 stream
- `options(shinyOAuth.audit_include_http = FALSE)` – exclude HTTP request details from audit events (default: `TRUE`)
- `options(shinyOAuth.audit_redact_http = FALSE)` – disable automatic redaction of sensitive data in audit events (default: `TRUE`)
- `options(shinyOAuth.audit_digest_key = ...)` – key for HMAC-SHA256 audit digests

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

### Networking/security

- `options(shinyOAuth.leeway = 30)` – default clock skew leeway (seconds) for ID token `exp`/`iat`/`nbf` checks and state payload `issued_at` future check
- `options(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 whitelist
- `options(shinyOAuth.allow_hs = TRUE)` – opt‑in HMAC validation for ID tokens (HS256/HS384/HS512). Requires a strictly server‑side `client_secret`
- `options(shinyOAuth.client_assertion_ttl = 120L)` – 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 120 seconds
- `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

By default, shinyOAuth blocks certain security‑critical parameters from being passed 
via `extra_auth_params`, `extra_token_params`, and `extra_token_headers`. This prevents
accidental misconfiguration that could break state binding, PKCE integrity, or client authentication.

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`,
  `scope`, `code_challenge`, `code_challenge_method`, `nonce`
- `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`

### 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)` –  when `FALSE` (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. Only set to `TRUE` if your provider legitimately requires redirect-following

### Development/debugging

- `options(shinyOAuth.skip_browser_token = TRUE)` – skip browser cookie binding
- `options(shinyOAuth.skip_id_sig = TRUE)` – skip ID token signature verification
- `options(shinyOAuth.debug = TRUE)` – re‑raise errors during token exchange

Don't enable these in production. They disable key security checks or alter error behavior, and are intended for local testing/debugging only.
Use `error_on_softened()` at startup to fail fast if softening flags are enabled in an environment where they should not be.

### Size caps

#### State envelope

- `options(shinyOAuth.state_max_token_chars = 8192)` – maximum allowed length of the base64url-encoded `state` query 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 the `code` query parameter
- `options(shinyOAuth.callback_max_state_bytes = 8192)` – maximum byte length of the `state` query parameter (outer token string)
- `options(shinyOAuth.callback_max_error_bytes = 256)` – maximum byte length of the `error` query parameter
- `options(shinyOAuth.callback_max_error_description_bytes = 4096)` – maximum byte length of the `error_description` query parameter
- `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 the `browser_token` argument accepted by `handle_callback()`

These apply before any hashing/auditing/state parsing, and exist to prevent memory/log amplification from extremely large callback URLs.

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

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 (e.g., `cachem::cache_disk()` pointing at a shared directory, or a custom cachem backend; 
the default `cachem::cache_mem()` is per‑process only and is then not shared)
- 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)

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 `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)
- Have your `OAuthClient` request the minimum scopes necessary; give
your app registration only the permissions it needs
- Do not show `$error_description` to 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
- Consider enabling automatic revocation on session end (`revoke_on_session_end = TRUE`)
- 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).
