---
title: "Usage"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Usage}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

## Overview

shinyOAuth provides a Shiny module for OAuth 2.0 authorization and OpenID Connect
(OIDC) authentication. `oauth_module_server()` manages redirects, callback
validation, token exchange, and session state. `oauth_ui()` supplies the browser
setup required by the module.

This vignette covers provider and client configuration, manual login buttons,
authenticated API calls, token refresh, and deployment. The examples use a
GitHub OAuth App. Install shinyOAuth with `install.packages("shinyOAuth")`.
For the protocol flow and validation rules, see
[Authentication flow](authentication-flow.html).

## GitHub app registration

1. Register an **OAuth App** in [GitHub's developer settings](https://github.com/settings/developers).
   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:

   ```text
   GITHUB_OAUTH_CLIENT_ID=your-client-id
   GITHUB_OAUTH_CLIENT_SECRET=your-client-secret
   ```

   Restart R after saving. Keep the secret out of app source files and Git.

## 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.

```{r, eval = FALSE}
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.

## Provider, client, and token objects

shinyOAuth represents the flow with three S7 classes. An `OAuthProvider` holds
the service's endpoint URLs and protocol settings; a provider helper such as
`oauth_provider_github()` creates it. An `OAuthClient`, created with
`oauth_client()`, holds your app's credentials, redirect URI, and requested
scopes. After authentication, the module returns an `OAuthToken` as
`auth[["token"]]`, containing tokens and available user information.

The **redirect URI**, also called the callback URL, is the address where the
provider sends the browser back. Register it with the provider and use the
same value in `oauth_client()`, including scheme, host, port, path, and fixed query parameters.
Fixed query names must not use OAuth/OIDC response fields such as `state`,
`code`, `error`, `iss`, `response`, `scope`, token fields, or the
`shinyOAuth_form_post` and `shinyOAuth_form_post_id` bridge fields, or the
`shinyOAuth_request_object` retrieval field. These names
are reserved for callback processing and are rejected when configuring the
client. Application parameters such as `tenant=one&tag=a&tag=b` are supported.
**Scopes** are named permissions, 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.

## Authentication state and user information

`auth` is a Shiny `reactiveValues` object. Read it inside `render*()`,
`reactive()`, or `observe*()`, just as you would read other reactive values.

- `auth[["authenticated"]]` tells you whether login passed the configured checks.
- `auth[["token"]]@userinfo` contains the user's profile, when fetched. Fields
  depend on the provider: GitHub uses `login` for the username.
- `auth[["error"]]` and `auth[["error_description"]]` describe a failed login. Show a
  simple message to users and use [audit logging](audit-logging.html) to investigate.

The token is an S7 object: access its properties with `@`, as in
`auth[["token"]]@userinfo`. See [`OAuthToken`](https://lukakoning.github.io/shinyOAuth/reference/OAuthToken.html) for the available properties.

Use `req(auth[["authenticated"]])` before server code reads private data or performs
an action that requires login. Hiding a UI element alone does not protect the
server code behind it. Your app must also check any access rules, such as
which accounts or groups may view a report. A successful login by itself does
not grant access to everything in your app.

## 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:

```{r, eval = FALSE}
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

Use `perform_resource_req()` to send an API request with the authenticated
user's access token. For example, add `tableOutput("repositories")` to the UI and this
output to `server()`:

```{r, eval = FALSE}
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]
})
```

Only send tokens to an API you intend to authorize. The example requests one
page of results; fetching more pages depends on the API. Use `resource_req()`
to build an `httr2` request without sending it, or pass a prepared `httr2`
request to `perform_resource_req()`. See the
[Spotify example](example-spotify.html) for another complete app.

### Additional token response fields

Some providers return extra parameters alongside the access and ID tokens.
Read these through `auth[["token"]]@extra_fields`, using the field names documented
by your provider.

`extra_fields` contains only the additional parameters from the latest
successful token response. A successful refresh replaces the entire list,
even if the provider returns no extra parameters. The separate
`initial_extra_fields` list preserves the initial successful code-exchange
response's extra parameters across refreshes. A new login starts a new
snapshot; clearing the session removes both lists with the token.

For a provider that returns a parameter named `custom_field`:

```{r, eval = FALSE}
# Inside reactive server code, after a successful login:
token <- auth[["token"]]
token@extra_fields[["custom_field"]]
token@initial_extra_fields[["custom_field"]]

# Distinguish an absent field from one explicitly returned as null.
"custom_field" %in% names(token@extra_fields)
```

The package preserves additional parameters without merging or interpreting
them, or fetching resources automatically. Nested JSON objects, arrays, and
explicit `null` entries remain available; form-encoded values remain strings.
Use your provider's documentation to decide how to handle omitted or changed
fields after refresh. The initial snapshot is historical response data, not
proof of current access permissions.

These parameters are separate from ID token claims. The `id_token_validated`
flag applies to the ID token, not these response fields. Keep the complete
lists out of logs and UI output because they can contain sensitive data.
Normal `print()` and `format()` output redacts both lists.

## 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 with a discovery
URL, you can let shinyOAuth look up the service's settings:

```{r, eval = FALSE}
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 describes the
service's capabilities, not the settings of your individual registration.

OpenID Connect (OIDC) is a login protocol: it supplies a signed **ID token**
that shinyOAuth checks to identify the user. OAuth 2.0 grants permission to
call APIs using an **access token**. GitHub and Spotify use OAuth without
OIDC; their helpers fetch profile information through their own APIs. With
OIDC, read validated identity details from `auth[["token"]]@id_token_claims` and
check `auth[["token"]]@id_token_validated`. An access token alone is not proof of
identity. The [authentication guide](authentication-flow.html) explains more.

For a local Keycloak server using HTTP, opt in before creating the provider:

```{r, eval = FALSE}
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.

## Assessing OAuth 2.1 configuration

shinyOAuth supports configurable OAuth 2.0 behavior and an optional assessment
of the authorization-code/refresh client role against
[OAuth 2.1 draft 16](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-16),
published 3 September 2026. This is an Internet-Draft, not a published RFC.
`check_oauth21()` currently implements only that revision, with ruleset `1.1.0`.
It makes no network requests, creates no login state and never enables automatic
enforcement. Existing provider examples and applications remain usable.

This example constructs an OAuth-only public client with S256 PKCE and HTTPS:

```{r, eval = FALSE}
options(shinyOAuth.tls_min_version = "1.2") # Set before discovery or login.
provider <- oauth_provider(
  name = "Example authorization server",
  auth_url = "https://auth.example/authorize",
  token_url = "https://auth.example/token",
  token_auth_style = "public",
  use_pkce = TRUE,
  pkce_method = "S256"
)
client <- oauth_client(
  provider,
  client_id = "registered-client",
  redirect_uri = "https://app.example/callback",
  scopes = "read"
)
assessment <- check_oauth21(client)
assessment[["configuration_compliant"]]
assessment[["checks"]]
```

Choose credentials and endpoints to match your actual registration. Basic and
body-secret authentication remain available; JWT authentication also supports
the issuer-audience configuration described in [advanced security](advanced-security.html#jwt-client-authentication).
The checker does not require optional DPoP, mTLS, PAR, JAR and JARM together, nor
does it require OIDC metadata for an OAuth-only provider.

The result records the draft, ruleset, package version, assessment time and
operation scope. Its verdict has three meanings:

| `configuration_compliant` | Meaning |
| --- | --- |
| `FALSE` | At least one applicable mandatory configuration check failed. This takes precedence over unknown findings. |
| `NA` | No known mandatory failure, but at least one mandatory configuration prerequisite is unresolved. Provider-only assessments are partial. |
| `TRUE` | Applicable mandatory checks in the recorded configuration scope passed. External behavior remains unverified. |

Every finding has a stable ID, status, requirement strength, evidence source,
remediation, reference and `affects_verdict` flag. Unmet recommendations do not
turn the verdict into `FALSE`. Code and refresh, enabled PAR, required UserInfo
and enabled introspection are included. Include optional operations explicitly:

```{r, eval = FALSE}
assessment <- check_oauth21(
  client,
  context = list(operations = c("introspection", "revocation"))
)
```

The checker does not contact a server to establish PKCE enforcement, refresh
rotation/replay protection, secret custody, registered redirect matching or
browser/proxy TLS. It cannot assess future resource URLs or later arbitrary
request mutations. Actual application scope enforcement and estimated token
expiry also require review. A positive report is a bounded configuration result,
not certification. Rerun it after changing options, objects or the runtime.

Applications choose how to use the result. For example, a deployment script
may decide to block a known failure and separately flag unresolved prerequisites:

```{r, eval = FALSE}
if (identical(assessment[["configuration_compliant"]], FALSE)) {
  stop("Resolve the mandatory configuration findings before deployment.")
}
if (is.na(assessment[["configuration_compliant"]])) {
  message("Review unresolved configuration prerequisites before deployment.")
}
```

S256 is the straightforward PKCE choice. Legacy `plain` remains constructible
but fails this draft assessment. Omitting PKCE has a narrow exception under
draft section 7.5.1.1: confidential client authentication, correct OIDC nonce
validation, and server assurance for the particular deployment and request.
Missing local prerequisites fail; unestablished server assurance is unresolved.
`context = list(nonce_exception = TRUE)` records an explicit declaration of that
assurance after it has been established. It cannot supply missing local settings
and is never treated as observed server evidence. S256 remains recommended.

## Multiple authorization servers

Create one client and module for each registration. Give each authorization
server a distinct registered callback path and list the same clients in the UI
wrapper. Module IDs alone do not distinguish browser callback routes.

```{r, eval = FALSE}
redirects <- c("https://app.example/oauth/a", "https://app.example/oauth/b")
# provider_a and provider_b are independently configured OAuth/OIDC providers.
clients <- list(
  auth_a = oauth_client(
    provider_a, client_id = Sys.getenv("SITE_A_CLIENT_ID"),
    client_secret = Sys.getenv("SITE_A_CLIENT_SECRET"),
    redirect_uri = redirects[[1]], scopes = c("read"),
    authorization_server_mode = "multi_redirect_uri",
    authorization_server_redirect_uris = redirects
  ),
  auth_b = oauth_client(
    provider_b, client_id = Sys.getenv("SITE_B_CLIENT_ID"),
    client_secret = Sys.getenv("SITE_B_CLIENT_SECRET"),
    redirect_uri = redirects[[2]], scopes = c("read"),
    authorization_server_mode = "multi_redirect_uri",
    authorization_server_redirect_uris = redirects
  )
)
ui <- oauth_ui(fluidPage(
  actionButton("connect_a", "Connect A"),
  actionButton("connect_b", "Connect B")
), clients = clients)
server <- function(input, output, session) {
  a <- oauth_module_server("auth_a", clients[["auth_a"]], auto_redirect = FALSE)
  b <- oauth_module_server("auth_b", clients[["auth_b"]], auto_redirect = FALSE)
  observeEvent(input[["connect_a"]], a[["request_login"]]())
  observeEvent(input[["connect_b"]], b[["request_login"]]())
}
shinyApp(ui, server, uiPattern = ".*")
```

Register those exact redirect URIs with the respective services and configure
the web host to serve the app at both paths. Shared routes require independent
authorization-server identification (`multi_issuer`, using RFC 9207 or JARM);
the static registry rejects multiple clients with the same issuer on one route.
See [OAuth Security BCP section 4.4.2](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.4.2).

This pattern routes multiple modules; it does not retain their credentials
across navigation. Following the authorization redirect for B can end the Shiny
session holding A. A new session starts with empty module credentials even if
the clients share a pending-state store. To preserve authorizations across
navigation, use the separate connection manager with browser or account
retention. See [Keep and use multiple OAuth authorizations](multiple-authorizations.html)
for a complete two-service example.

For healthcare APIs, [Use SMART on FHIR from Shiny](smart-on-fhir.html) shows
discovery, client registration settings, patient requests and EHR launch using
the same manager.

Additional token fields are already available server-side. For example,
`a[["token"]]@initial_extra_fields[["patient"]]` reads an initial SMART patient value,
while `a[["token"]]@extra_fields` contains only the latest successful response's
extras. Refresh may omit `patient`. Neither list is validated identity data or
evidence of permission to read a resource. Ordinary clients do not infer SMART
discovery, launch handling, scope equivalence, or a FHIR destination from these
fields. Keep the matching client, token and approved resource together when
making requests, and keep raw context out of logs and session summaries.

### Requests bound to a client and resource

These optional helpers group the client, API address and current credentials
used for a request. They are package API choices; OAuth and SMART on FHIR do not
require these classes.

| Object | What it represents | Example |
| --- | --- | --- |
| `OAuthClient` | Registration settings and optional approved API addresses | Hospital A's client ID, scopes and `https://a.example/fhir` |
| `OAuthConnection` | Access to one Shiny session's current credentials through that configuration | This session's authorized requests to Hospital A |

The client holds shared configuration, with no user's token. A connection reads
the current token for each request. The object refers to credentials that
can change instead of keeping a token copy. The existing reactive token already
updates on refresh; the connection adds client selection, API-address restrictions
and session checks. It does not implement refresh or retention by itself.

Set `resource_bases` on `oauth_client()` outside `server()`, then create
a connection for that client's module inside `server()`:

```{r, eval = FALSE}
client <- oauth_client(
  provider, client_id = "registered-app",
  redirect_uri = "https://app.example/callback", scopes = "read",
  resource_bases = c(records = "https://api.example/v1"),
  required_scopes = "read" # A subset of the requested scopes.
)
server <- function(input, output, session) {
  auth <- oauth_module_server("auth", client)
  connection <- oauth_connection(client, reactive(auth[["token"]]))
  records <- reactive({
    req(connection[["is_usable"]]())
    connection[["request"]]("records", "records", query = list(limit = 20)) |>
      httr2::resp_body_json()
  })
}
```

The reference reads the latest token after refresh and becomes unusable after
logout, token expiry, or session end. A reference from another Shiny session
cannot read its summary or make requests. This adapter still uses the legacy
module's lifecycle and does not retain credentials across navigation.

`resource_bases` restricts the token's destinations; it does not request an
audience or prove an opaque token's audience. Wire the token expression to the
module using `client`. Resource indicators remain an explicit client
setting ([RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html)).

Each request selects a resource ID and resolves a path against its full base.
An absolute pagination or resource reference must stay within that base too.
For example, `/v1-other` is outside `/v1`, even on the same hostname. Ambiguous
path syntax is rejected before URL normalization, and redirects are disabled.
General URL options cannot relax this policy. These comparisons use URI origin
and path rules from [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html).

Optional permissions can produce a limited connection. Declare an operation's
extra permissions with `[["request"]](..., required_scopes = "write")`; the current
grant must cover them. No generic mapping from API paths to OAuth scopes is
assumed. `[["summary"]]()` contains the opaque connection ID, client label, local
status, expiry, and resource IDs. It excludes credentials and raw context.

## Asynchronous execution

By default, network work runs in the app's R process. A slow provider can make
other sessions on that process wait too. To run the module's network work in
background R processes, install `mirai` and `promises`, then configure workers
before `server()`:

```{r, eval = FALSE}
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`. 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:

```{r, eval = FALSE}
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.
}
```

This setting covers the module's operations; API calls you write in your own
outputs still run where you call them. Discovery during app setup also stays
synchronous. See [`oauth_module_server()`](https://lukakoning.github.io/shinyOAuth/reference/oauth_module_server.html) for advanced exceptions and the
[options reference](package-options.html) 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.

Keep `indefinite_session = FALSE` unless you deliberately want the local
session to continue with an expired token or after refresh fails. Setting it
to `TRUE` also disables the `reauth_after_seconds` limit; it does not extend
the token's validity at the provider.

## 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. On Posit
Connect Cloud, use the app's direct URL as described in its
[URL settings guide](https://docs.posit.co/connect-cloud/user/manage/content_settings.html#url).

### Multiple R processes

The default configuration stores pending logins in one R process. If a login
can start on one process and return to another, those processes need:

- A shared `state_store` with an atomic `[["take"]]()` operation: reading and
  deleting a pending login must happen as one indivisible operation.
- The same secret `state_key`, so each process can read the encrypted login
  details. Supply at least 32 random bytes, stored in your deployment's secret manager.
- Matching provider and client settings.

Use `custom_cache()` to connect a shared database or Redis store. Plain
`cachem::cache_disk()` is unsuitable for shared login state because separate
reads and deletes can let two requests use the same entry. See [`custom_cache()`](https://lukakoning.github.io/shinyOAuth/reference/custom_cache.html)
for the backend contract.

Hosted Request Objects (`request_object_mode = "request_uri"`) also use this
store, in separate records. Their JWT claims are readable when signed without
JWE encryption; the pending-login record's `state_key` sealing does not cover
them. Apply the store's access controls and expiry to both record types.

### Security checklist

- Use HTTPS in production and keep credentials on the server.
- Request only the scopes your app needs.
- Check login and app-specific access rules in server code before using private data.
- Keep tokens, complete login URLs, and detailed provider errors out of the UI and logs.
- Render untrusted text through Shiny's ordinary text/tag functions. Do not
  pass it to `HTML()` as markup; keep table escaping enabled.
- Wrap the UI with `oauth_ui()` (or `oauth_form_post_ui()` when required).
  These send a browser privacy header that keeps callback URLs out of referrers.
- If repeated login requests affect responsiveness, your hosting provider can
  help with optional rate limiting.
- Keep the provider's validation defaults and choose any extra protections
  to match your provider and deployment requirements.

The module links a returning login to the browser using an origin-scoped token
in tab-scoped session storage and an independent, short-lived cookie marker.
Each transaction has its own marker. Cookies and session storage must both be
available; complete login in the tab that started it. JavaScript reads this binding, so
preventing injected scripts (cross-site scripting, or XSS) in your app matters.
This link cannot establish which account you expected to sign in: check that
account yourself when your app has such a requirement.

Treat the entire hostname as a trust boundary and use a dedicated hostname if
other services are untrusted. Cookies are shared across ports, even with
`__Host-`, `Secure`, or `HttpOnly`. The session-storage check prevents another
port from adopting a cookie as a binding, but cannot prevent cookie disruption.
Application callback routes distinguish records within a tab. Same-origin scripts
can still access session storage.

## Troubleshooting

Use [audit logging](audit-logging.html) to identify the failing operation.
Common configuration issues include:

- **Rejected redirect URI:** check that the registered callback URL and
  `redirect_uri` match, including scheme, host, port, and path.
- **Callback validation failure:** use the registered app address in a regular
  browser and create the client outside `server()`. For multiple R processes,
  check the shared state store and key.
- **Rejected client credentials:** check the environment variables after
  restarting R and match `token_auth_style` to the app registration.
- **Missing profile fields:** check requested scopes and the provider's profile
  format. Some providers return identity information only in ID token claims.
- **Rejected API request:** check permissions, token expiry, and whether the API
  accepts tokens from the configured provider.

Keep token and callback validation enabled while diagnosing configuration errors.

## Further configuration

The [`oauth_module_server()` examples](https://lukakoning.github.io/shinyOAuth/reference/oauth_module_server.html#ref-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](https://lukakoning.github.io/shinyOAuth/reference/oauth_provider.html#ref-examples)
cover manual OAuth/OIDC setup, discovery, and named providers; the
[Microsoft example](https://lukakoning.github.io/shinyOAuth/reference/oauth_provider_microsoft.html#ref-examples)
also shows authentication-state summaries and error handling.

- [Advanced security](advanced-security.html): form POST callbacks, certificates
  (mTLS), signed requests (JAR) and responses (JARM), pushed requests (PAR),
  and tokens tied to a key (DPoP).
- [Authentication flow](authentication-flow.html): what the module checks and why.
- [Package options](package-options.html): logging, network limits, and debugging settings.
- [OpenTelemetry](opentelemetry.html): exporting logs and timing information.
