This vignette provides a step-by-step description of what happens
during an authentication flow when using the
oauth_module_server() Shiny module. It maps protocol
concepts (OAuth 2.0 Authorization Code + PKCE, OpenID Connect) to the
concrete implementation details in the package.
For a concise quick-start (minimal and manual button examples,
options, and security checklist) see:
vignette("usage", package = "shinyOAuth").
For an explanation of logging key events during the flow, see:
vignette("audit-logging", package = "shinyOAuth").
The package implements the OAuth 2.0 ‘Authorization Code’ flow and optional ‘OpenID Connect’ (OIDC) checks end‑to‑end. Below is the sequence of operations and the rationale behind each step.
On the first load of your app, the module sets a small random cookie in the user’s browser (SameSite=Strict; Secure when over HTTPS). This browser token is mirrored to Shiny as an input. Its purpose is to ensure that the same browser that starts the OAuth flow is the one that finishes it (a “double-submit” style CSRF defense).
If oauth_module_server(auto_redirect = TRUE), an
unauthenticated session triggers immediate redirection to the provider
authorization endpoint.
If oauth_module_server(auto_redirect = FALSE), you
manually call $request_login() (e.g., via a button) to do
so.
The browser of the app user will be redirected to the provider’s
authorization endpoint with the following parameters:
response_type=code, client_id,
redirect_uri, state=<sealed state>, PKCE
parameters, nonce (OIDC), scope, plus any
configured extra parameters.
The provider redirects the user’s browser back to your Shiny app
(your redirect_uri), including the code and
state parameters (and optionally error and
error_description on failure).
handle_callback())Once the user is redirected back to the app, the module processes the callback. This consists of the following steps:
issued_at
window)shinyOAuth_state_erroraudit_state_store_lookup_failed,
audit_state_store_removal_failed)Note: in asynchronous token exchange mode, the module may pre‑decrypt the sealed state and prefetch plus remove the state store entry on the main thread before handing work to the async worker, preserving the same single‑use and strict failure behavior.
If userinfo is requested via
oauth_provider(userinfo_required = TRUE) (for which you
should have a userinfo_url configured), the module calls
the userinfo endpoint with the access token and stores returned claims.
If this request fails, the flow aborts with an error.
When using oauth_provider(id_token_validation = TRUE),
the following verifications are performed:
iss matches expected issuer; aud
vector contains client_id; sub present;
iat is required and must be a single finite numeric;
time-based claims (exp is required, nbf
optional) are evaluated with a small configurable leeway; tokens issued
in the future are rejectedoauth_provider(userinfo_id_token_match = TRUE), it is
checked that sub in userinfo equals sub in the
ID tokenOAuthToken objectNow that all verifications have passed, the module builds the final
token object. This is an S7 OAuthToken object which
contains:
access_token (string)refresh_token (optional string)expires_at (POSIXct; optional)id_token (optional string)userinfo (optional list)The $authenticated value as returned by
oauth_module_server() now becomes TRUE, meaning all
requested verifications have passed.
The user’s browser was redirected to your app with OAuth query
parameters (code, state, etc.). To improve UX
and avoid leaking sensitive data, these values are removed from the
address bar with JavaScript. Optionally, the page title may also be
adjusted (see the tab_title_ arguments in
oauth_module_server()).
The browser token cookie is also cleared to allow a fresh future flow.
Now that the flow is complete, the module will manage the token lifetime during the active session. This may consist of:
$authenticated flag to FALSEoauth_module_server(reauth_after_seconds = ...) can force
periodic re-authentication