This vignette describes what happens when a user signs in through
oauth_module_server(): how shinyOAuth builds the
authorization request, validates the callback and tokens, and manages
the authenticated Shiny session. For application setup and code
examples, see the usage vignette.
The package implements the OAuth 2.0 Authorization Code flow. The app
redirects the browser to the provider, where the user signs in and
authorizes the requested access. The provider returns an authorization
code to the app’s registered redirect_uri (the callback
URL). shinyOAuth exchanges this temporary code for tokens and completes
the configured checks before setting
auth[["authenticated"]] = TRUE.
OpenID Connect (OIDC) adds identity verification to this flow through a signed ID token. OAuth-only integrations such as GitHub and Spotify obtain user information from provider-specific APIs; an access token alone is not an identity assertion.
The steps below follow a login from the first page load through token refresh and logout. Each step describes the usual OAuth or OIDC behavior first, then the additional behavior for the relevant optional features.
On first page load, the module asks the browser to create a random token in origin- and tab-scoped session storage and pass it to Shiny as a private input. A separate random cookie marker must match the stored record. This lets shinyOAuth check later that the browser returning from the provider is the one that started login. This browser token is separate from the access and ID tokens issued by the provider.
The cookie contains only the marker, so a service on another port cannot read the binding token from a Cookie header or establish a binding by replacing the cookie. Origin-scoped storage separates schemes, hosts, and ports. Treat the whole hostname as trusted nonetheless: other services can disrupt cookies, and same-origin scripts can access session storage. Use a dedicated hostname for authentication when co-hosted services are untrusted.
Each new transaction has its own cookie marker; separate tabs and application callback routes keep independent records. Complete login in the tab that started it.
The browser must support cookies, session storage, and Web Crypto. If
either storage mechanism cannot be written and read, the module reports
browser_cookie_error and stops login. Missing, expired, or
mismatched binding records are replaced; callbacks using the old binding
then fail validation. Cookies created by older package versions cannot
restore a binding without the origin record, so pending logins must be
restarted after upgrading.
Before each authorization request the server selects a fresh binding
token. The browser stores it and acknowledges the request without
duplicating the token in another input. The server rejects
client-selected replacements. Starting a second login for the same
module replaces the first pending binding. Private inputs are excluded
from URL and server bookmarks. Applications must not copy
auth[["browser_token"]] into their own bookmark values or
logs.
Wrap your UI in oauth_ui()
with the module ID and client
(oauth_ui(ui, id = "auth", client = client)). Query
callbacks are validated and stored before a redirect to a one-time URL;
only then does the application UI load. Responses disable caching and
referrer disclosure. Logical state still requires browser binding before
consumption.
With auto_redirect = TRUE,
oauth_module_server() starts authorization for an
unauthenticated session. With auto_redirect = FALSE, the
application starts it by calling auth[["request_login"]](),
for example from a button observer.
The module sends the browser to the authorization URL. For a regular
OAuth or OIDC request, its query string carries the parameters prepared
in the previous step. With JAR or PAR, it instead carries the Request
Object or request_uri, along with the outer parameters
required by the provider’s configuration.
If you set response_mode, the request also tells the
provider how to return the response. Leaving it unset uses the usual
query-string callback described in step 6.
The provider redirects the browser to the registered
redirect_uri. A successful response normally carries
code and state in the URL’s query string, and
may include iss to identify the provider. If authorization
fails or the user declines access, the response carries error fields
instead of a code. The module validates the response before acting on
either result.
form_post)With response_mode = "form_post", the provider sends the
callback fields in an HTTP POST body. Because this arrives before a
Shiny session exists, wrap the UI with oauth_form_post_ui().
The wrapper checks the incoming request, stores the accepted callback
temporarily, and redirects the browser to a normal Shiny page using a
short-lived, single-use handle. The module then resumes callback
processing. The wrapper includes the oauth_ui() setup from
step 1.
The module checks that the callback belongs to the pending login before exchanging the code for tokens. It checks the app address, verifies that state is authentic and fresh, compares the stored client and provider settings, and checks that the browser token matches. It waits for the browser-token input to reach Shiny if necessary, and rejects malformed or oversized callback values.
The default authorization-code budget is 8192 decoded bytes across direct, GET/POST bridge, module and JARM processing. Explicit field and aggregate limits still apply; oversized values are rejected without truncation. Large codes, state or JARM values can exceed a deployment’s proxy limits. Check the whole encoded callback when configuring those limits.
The state-store entry is consumed once. Missing, expired, or already-used entries cannot complete login. A Form Post handle must also be valid and unused; passing the earlier POST checks does not replace the browser-binding check in the Shiny session.
For JARM, shinyOAuth first verifies the response’s signature, issuer,
audience, expiry, and configured encryption requirements. Only then does
it process the contained callback fields. Use
oauth_module_server() for JARM; handle_callback()
accepts direct code/state callbacks only.
Even when the provider returns an error such as
access_denied, shinyOAuth checks the login state before
accepting that error as part of this session. Use
auth[["error"]] and
auth[["error_description"]] for diagnostics, and display an
application-specific message in the UI.
Browser binding links the response to the browser that started login; it does not establish which person should sign in at the provider. If someone obtains the complete login URL, they may complete that same request with another account and deliver the response to the initiating browser. Protect login URLs from disclosure and your app against cross-site scripting (XSS). Check the validated issuer and subject against your app’s expected account when that is part of your access policy.
An OIDC ID token is a signed JSON Web Token (JWT) containing identity
fields, called claims. For example, sub identifies the user
within the issuer, iss identifies the issuer, and
aud identifies the intended recipient. Decoding these
fields does not verify the token’s authenticity.
OIDC helpers enable ID token validation. shinyOAuth checks the ID token before fetching userinfo or marking login complete. It checks that the provider signed it, that it was issued for this client, that it is still valid, and that it belongs to the login request.
The provider publishes public signing keys as a JSON Web Key
Set (JWKS). shinyOAuth fetches and caches these keys using the
provider’s configured issuer and key settings; provider
helpers supply the usual defaults.
iss must match the expected issuer; aud
must include the client ID; sub must identify a user.
Additional audiences must be explicitly trusted. If azp is
present, it must equal the client ID.exp (expiry) and iat (issue time) are
required. nbf (valid from) is checked when present. Small
clock differences are allowed by leeway, defaulting to 30
seconds. exp - iat is capped at 24 hours by
shinyOAuth.max_id_token_lifetime.typ is present in
the token header, it must indicate a JWT, rather than another token
format.max_age was requested, auth_time must
show a sufficiently recent login and must not be in the future beyond
the clock allowance.at_hash is present, it must match the access token.
Set id_token_at_hash_required = TRUE to require it.See oauth_provider()
for allowed signature algorithms and key policies. Unsigned tokens are
not accepted by the normal configuration.
Scopes request broad permissions. The OIDC claims
argument can request particular profile fields or values.
claims_validation controls whether an unmet request raises
a warning, stops login, or is ignored. When omitted, it defaults to
"warn" if claims contains enforceable
requirements and to "none" otherwise. Use
"strict" when your app requires those fields or values.
Checks on claims[["id_token"]] require validated ID token
content.
To require a particular login policy, such as multi-factor
authentication (MFA), set required_acr_values to your
provider’s Authentication Context Class Reference (ACR) identifiers. The
package sends the request and requires the validated token’s
acr claim to match one of them. The outgoing
acr_values parameter is a hint to the provider; validation
of the returned claim enforces the client’s requirement.
OAuthToken objectThe module stores the validated token response in an OAuthToken
object. It still completes any configured introspection and UserInfo
checks before making this the authenticated session’s
auth[["token"]].
The object holds:
access_token, token_type, and any
refresh_token for API access and renewal.expires_at, the access token’s expiry time. When the
provider omits expires_in, shinyOAuth uses a finite
fallback of 3,600 seconds, configurable through
shinyOAuth.default_expires_in.id_token, its decoded id_token_claims, and
id_token_validated, which indicates whether the ID token
has been verified.userinfo, populated when profile fields are fetched in
step 12. Your app can then access them as
auth[["token"]]@userinfo.granted_scopes and
granted_scopes_verified, describing the permissions
associated with the token and whether the token response explicitly
confirmed them.cnf, any observed key or certificate binding data for
DPoP or mTLS.Token introspection (RFC 7662) asks the provider whether a token is
still active. It is an optional request, enabled with
oauth_client(introspect = TRUE), and requires a configured
introspection_url. Login and subsequent module refreshes
then require a successful response with active = TRUE. The
request runs before the UserInfo fetch so it can supply binding data for
opaque mTLS or DPoP tokens.
introspection_checks can require additional fields such
as the subject, client ID, scopes, or token type; your provider must
return the fields you need. Checks that use the retrieved user profile
are completed after the next step, before the session becomes
authenticated.
The provider’s UserInfo endpoint or profile API supplies fields such
as the user’s name and email address. The available fields depend on the
provider and the permissions granted. When
userinfo_required = TRUE, shinyOAuth fetches these fields
from userinfo_url using the access token. A failed required
fetch stops login.
With OIDC, this happens after ID token validation. When a validated
ID token and userinfo are both available, their sub values
must match. userinfo_id_token_match = TRUE also requires a
validated ID token to be available for that comparison. Requests in
claims[["userinfo"]] are checked according to
claims_validation, as with the ID token claims in step
9.
Most providers return userinfo as JSON. Signed JWT userinfo is also
supported and verified against the provider’s public keys; encrypted
userinfo is not supported. Use userinfo_signed_jwt_required
to require this format and
userinfo_jwt_required_time_claims, for example
"exp", to require time claims. Present time claims are
checked even when not required. Signed userinfo uses the asymmetric
algorithms in userinfo_allowed_algs.
Discovery records whether the provider supports signed UserInfo, but
does not enable userinfo_signed_jwt_required: support at
the provider does not mean your app registration requests that response
format. Set the requirement explicitly when login must reject an
ordinary JSON profile response.
For certificate-bound access tokens, the UserInfo request uses the configured certificate and any mTLS UserInfo endpoint alias, after checking the token’s certificate binding. For DPoP tokens, the module attaches a proof signed with the client’s key. A resource-server nonce challenge permits one retry with a fresh proof; resource-server and token-server nonces are kept separately.
After all required checks pass, the module sets
auth[["authenticated"]] = TRUE and exposes the token as
auth[["token"]]. Your reactive code can now use
shiny::req(auth[["authenticated"]]) before accessing the
profile or making API requests on the user’s behalf.
Pass the token to perform_resource_req()
for API requests. Also supply client = client when using
mTLS or DPoP so the helper can apply the certificate or proof required
by the token.
The module removes callback parameters from the browser address and
renews the browser cookie, so a future login starts with a fresh browser
token. The tab_title_ arguments in oauth_module_server()
can also remove a query-string suffix from the page title or set a
replacement title after the callback.
While the Shiny session is active, the module monitors token expiry and can refresh the token or require another login according to your settings.
The module can refresh access tokens before expiry when
refresh_proactively = TRUE and a refresh token is
available. A refreshed token replaces auth[["token"]], so
reactive code sees the new value. The provider can rotate refresh
tokens; shinyOAuth keeps the replacement when supplied. The replacement
token gets a new expiry time, using the same fallback as in step 10 if
expires_in is omitted.
Refresh uses the OAuth 2.0 refresh-token grant. Required UserInfo is
fetched again, and configured token introspection must succeed before
the module replaces the session token. See refresh_token()
for the fields that are updated and the validation rules applied to the
response.
An OIDC refresh response may omit the ID token, in which case the
original is kept. When a new ID token is returned, shinyOAuth checks
continuity with the original subject, issuer, and audience, as well as
auth_time and nonce continuity when applicable. A new ID
token without an original to compare against causes refresh to fail.
Full signature and claim validation also runs when
id_token_validation = TRUE.
Refresh requests use the configured certificate or DPoP proof as in the initial token exchange. A DPoP nonce challenge allows one fresh-proof retry; ordinary network or HTTP failures do not trigger a token-request retry.
A refreshed certificate-bound token needs fresh binding data from the new token or its introspection response. shinyOAuth does not carry the previous certificate thumbprint forward when the refresh response omits it.
reauth_after_seconds sets a maximum time since
interactive login that token refresh cannot extend. This limit applies
when indefinite_session = FALSE (the default). With OIDC,
the module requests a fresh login using max_age=0 and
checks auth_time. With OAuth alone, it can end the local
session and request authorization again, but cannot require a fresh
provider login.
By default, expiry or refresh failure clears the token and makes
auth[["authenticated"]] false. A refresh failure exposes
auth[["error"]] == "token_refresh_error" and diagnostic
details. With indefinite_session = TRUE, the module keeps
the token and authenticated state; auth[["token_stale"]]
signals expiry or a failed refresh. The provider may still reject API
calls with that token.
auth[["logout"]]() clears the local login and attempts
to revoke both access and refresh tokens if the provider supports it. It
does not end the user’s login session at the provider.
revoke_on_session_end = TRUE also attempts revocation when
the Shiny session ends and requires a configured
revocation_url. Revocation can fail, for example if the
provider is unavailable; local cleanup still happens. These requests run
in the background only with async = TRUE.
The module also clears and renews the browser token for subsequent
logins. For direct token management, revoke_token()
lets you request revocation of an access or refresh token.
For a record of successful and failed steps throughout this flow, see Audit logging.