shinyOAuth runs the OAuth 2.0 Authorization Code flow inside a Shiny app. The browser visits the provider for login and consent, then returns with an authorization code. shinyOAuth exchanges that code for tokens, validates the response, and updates the module’s reactive authentication state.
The provider object describes the service, the client object holds
the app’s registration and policy, and
oauth_module_server() manages each Shiny session. The UI
wrapper handles the HTTP callback and browser setup. With OpenID Connect
(OIDC), the flow also validates identity through a signed ID token.
OAuth-only providers such as GitHub and Spotify supply profile
information through their own APIs.
The usage vignette contains the application setup. The sequence below describes the work performed by the package after that setup.
With auto_redirect = TRUE, the module starts login for
an unauthenticated session. With auto_redirect = FALSE,
auth[["request_login"]]() initiates the same flow. Both
paths wait for browser setup before preparing the request.
For each login transaction, the module establishes a fresh random browser binding. The browser holds the binding token in origin- and tab-scoped session storage, with a separate temporary cookie marker. On return, the module checks this binding against the pending transaction. This connects the callback to the browser tab that started it, rather than accepting state from any browser presenting the same callback URL.
The binding is separate from provider-issued access and ID tokens. Browser storage and cookie setup happen through the UI dependency; unavailable browser facilities produce a browser-setup error before login proceeds.
oauth_ui() processes query callbacks before application
HTML or scripts load. It validates the callback address and response
envelope, stores a sealed, short-lived response, and redirects to a
clean continuation URL carrying a one-time handle.
oauth_form_post_ui() provides the corresponding path for
POST callbacks. The wrappers also set no-store and no-referrer response
headers.
After the HTTP continuation, the Shiny module checks the initiating browser and consumes the pending transaction before code exchange.
The module checks that the state is authentic and within its lifetime, that the stored client/provider policy still matches, and that the browser binding agrees with the pending record. The callback route and any required issuer identifier are checked against trusted client configuration. Missing, expired, already-consumed or mismatched records reject the callback.
A provider error response goes through state and browser validation too. Only a valid response for this transaction can become the session’s login error.
After callback validation, shinyOAuth sends the code to the token
endpoint, along with the PKCE verifier and the authentication required
by the app’s registration. token_auth_style selects that
authentication, such as a client secret, signed assertion or client
certificate.
The token response is still untrusted at this point. shinyOAuth
parses it, validates the access token and token type, calculates expiry,
and reconciles reported scopes with the requested permissions.
scope_validation determines whether a reduced grant
produces a warning or rejects login. The token records
granted_scopes and whether the response explicitly verified
that grant in granted_scopes_verified.
For ordinary OAuth responses that omit expires_in, the
package uses shinyOAuth.default_expires_in, defaulting to
3,600 seconds. SMART clients apply their separate lifetime policy.
Code exchange and refresh are not automatically retried after ordinary network or HTTP failures, because the provider may already have consumed the code or rotated a refresh credential. DPoP nonce challenges have their own one-retry handling.
With OIDC validation enabled, shinyOAuth verifies the ID token using the provider’s public signing keys. Key retrieval and caching follow the provider’s configured issuer and JWKS policy. Decoding claims is followed by signature and claim validation before the identity is accepted.
| Check | What it establishes |
|---|---|
| Signature and allowed algorithm | The token was signed with an acceptable provider key. |
iss |
The token came from the configured issuer. |
aud and, when present, azp |
The token is intended for this client; additional audiences require explicit trust. |
sub |
The token identifies a subject within that issuer. |
exp, iat and optional
nbf |
The token is within its validity window. |
| Nonce, when enabled | The ID token belongs to this login request. |
auth_time, when freshness was requested |
The provider login satisfies the requested maximum age. |
at_hash, when present or required |
The ID token agrees with the returned access token. |
Time checks allow the configured clock leeway, defaulting to 30
seconds. The package also applies its configured maximum ID-token
lifetime, defaulting to 24 hours. Requested claim requirements follow
claims_validation, while required_acr_values
checks the validated authentication-context claim against the client’s
accepted values.
With introspect = TRUE, shinyOAuth asks the provider
whether the access token is active. The response must report
active = TRUE and satisfy the configured
introspection_checks. Introspection runs before profile
retrieval, allowing it to supply binding information for opaque
certificate- or DPoP-bound tokens. Checks involving the profile finish
after that profile becomes available.
When userinfo_required = TRUE, the package fetches the
provider’s UserInfo endpoint or profile API. A failed required fetch
rejects login. For OIDC, when both a validated ID token and UserInfo are
available, their sub values must match. The provider’s
subject-binding policy can also require a validated ID token to be
present for the profile fetch.
UserInfo can be JSON or a signed JWT. Signed responses go through
signature and claim validation, and
userinfo_signed_jwt_required rejects ordinary JSON when the
registration requires signing. OAuth-only helpers obtain their profiles
through the service’s API without creating an OIDC identity
assertion.
After all configured checks pass, the module publishes the
OAuthToken as auth[["token"]] and sets
auth[["authenticated"]] to TRUE. Reactive
consumers then see the accepted token and available profile. A rejected
login instead populates auth[["error"]] and
auth[["error_description"]].
The token holds the access and refresh credentials, expiry, granted
scopes, validated ID token claims and fetched profile. Additional
response parameters are available in extra_fields;
initial_extra_fields preserves the initial code-exchange
values across refreshes.
The module’s connection() method resolves the current
credentials through an OAuthConnection. Connection methods
apply session ownership, current access and configured permission checks
when obtaining tokens or making requests.
With async = TRUE and mirai workers or a non-sequential
future plan, the module dispatches network work to background R
processes while the main process continues servicing Shiny. The module
updates authentication state after the worker result passes validation.
It checks that a result belongs to the current authorization, so work
completing after logout or replacement cannot restore the old login.
State-store access and hosted Request Object publication remain in the main process. Signed-request preparation, PAR and query-JARM verification can run in workers. Setup-time discovery and POST-wrapper JARM verification remain synchronous. This describes the module’s work; API calls made by application code have their own execution context.
The module watches token expiry. With
refresh_proactively = TRUE and an available refresh token,
it schedules refresh before expiry. Connection token acquisition can
also trigger refresh on demand. Overlapping asynchronous refreshes
coordinate use of the same credential.
A refresh response passes token validation, configured introspection and any required profile fetch before replacing the current token. A rotated refresh token replaces the previous one. If the provider omits a new ID token, the stored identity token remains; a new ID token is checked for continuity with the original subject, issuer and audience, plus nonce and authentication time when applicable. Certificate-bound tokens require fresh binding data.
Ordinary refresh requests preserve the retained scope limit. SMART clients and API-specific token targets apply their corresponding scope rules. Refresh updates credentials without establishing a new interactive login.
By default, token expiry or refresh failure clears the local token
and makes authenticated false. An in-progress refresh has a
grace period before expiry clearing. reauth_after_seconds
independently limits the age of the interactive login; refresh does not
restart that timer. With automatic redirects enabled, the module starts
authorization again when appropriate.
For OIDC age-based reauthentication, the request includes
max_age = 0 and the returned auth_time is
checked. OAuth-only providers receive an ordinary new authorization
request, which may reuse their existing login session.
indefinite_session = TRUE changes local clearing
behavior: the module retains login state after expiry or refresh
failure, reports token_stale, and ignores the
maximum-login-age limit. With token targets, individual API tokens can
expire while the shared local authorization remains; connection methods
check the selected target before use.
auth[["logout"]]() clears local credentials and resets
the browser binding. It also attempts token revocation when the provider
supports it. With revoke_on_session_end = TRUE, session
termination and automatic clearing also attempt revocation. These
requests follow the module’s sync or async mode; remote failure does not
prevent local cleanup.
Provider account sessions are separate, so local logout can be followed by another authorization without a provider password prompt. Retained connections have their own lifetime and ownership rules, described in Multiple authorizations.
Configured extensions add these steps to the sequence:
| Extension | Package behavior |
|---|---|
| JAR | Signs the authorization parameters as a Request Object before browser navigation. Optional encryption hides the contents. |
| PAR | Sends the request directly to the provider first, then redirects the browser with the returned reference. |
| Form Post | Receives the authorization result by POST and bridges it into the Shiny session. |
| JARM | Verifies the signed authorization response before processing its callback fields; configured encryption adds decryption first. |
| mTLS | Presents the client certificate and uses configured endpoint aliases; certificate-bound token checks apply when enabled. |
| DPoP | Signs proofs and handles nonce challenges for token requests and resource requests made through the configured connection or request helpers. |
Advanced security describes these configurations. Detailed contracts are documented under oauth_client(), oauth_provider(), handle_callback() and refresh_token().
The flow emits events for request preparation, callback checks, token
operations and authentication-state changes. Related events share a
trace_id, and failed checks include operation or phase
context. The configured audit hook receives these events; OpenTelemetry
can export the same events and operation timings. The audit logging and OpenTelemetry vignettes describe those
records.