Authentication flow

Overview

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").

What happens during the authentication flow?

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.

1. First page load: set a browser token

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

2. Decide whether to start login

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.

3. Build the authorization URL (prepare_call())

To redirect the user to the provider, the module constructs an authorization request URL. The URL is built from the provider’s authorization endpoint and includes various query parameters to ensure security and proper context tracking:

  • State: this is a high-entropy random string to prevent CSRF; this package seals the state to enhance security (see below)
  • PKCE: a code_verifier (random) and code_challenge (S256 hash) proving the same party finishes the flow
  • Nonce (OIDC): random string echoed back in the ID token, mitigating replay attacks

This package seals the state, meaning it encrypts and authenticates (AES-GCM AEAD) a payload containing:

  • state, client_id, redirect_uri
  • requested scopes
  • provider fingerprint (issuer/auth/token URLs)
  • issued_at timestamp

Sealing the state prevents tampering, stale callbacks, and mix-ups with other providers/clients.

On the server side, the package will store the sealed state (as a cache-safe hash key) in the state store (e.g., a ‘cachem’ backend) along with the following data:

  • browser token
  • code_verifier
  • nonce (OIDC)

All this data will be used for validation during the callback processing.

4. App redirects to the provider

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.

5. User authenticates and authorizes

Once at the provider’s authorization page, the user is prompted to log in and authorize the app to access the requested scopes.

6. Provider redirects user back to the app

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

7. Callback processing & state verification (handle_callback())

Once the user is redirected back to the app, the module processes the callback. This consists of the following steps:

  • Wait for the browser token input if not yet visible
  • Decrypt and verify the sealed state, ensuring integrity, authenticity, and freshness (using the issued_at window)
  • Check that embedded context matches expected client/provider (defends against misconfiguration/multi-tenant mix-ups)
  • Fetch and immediately delete the one-time state entry from the configured state store
    • If the entry is missing, malformed, or deletion fails, the flow aborts with a shinyOAuth_state_error
    • Audit events are emitted on failures (e.g., audit_state_store_lookup_failed, audit_state_store_removal_failed)
  • Verify that user’s browser token matches the previously stored browser token
  • Ensure PKCE components are available when required

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.

8. Exchange authorization code for tokens

Once the callback is verified, the module proceeds to exchange the authorization code for tokens.

A POST request is made to the token endpoint with grant_type=authorization_code, the code, the redirect_uri, and the code_verifier (PKCE). Client authentication method depends on provider style: HTTP Basic header (client_secret_basic), body params (client_secret_post), or JWT-based assertions (client_secret_jwt, private_key_jwt) when configured. The response must include at least access_token. Malformed or error responses abort the flow.

When successful, the package also applies two safety rails:

  • If the token response includes scope, all scopes requested by the client must be present in the granted set; otherwise the flow fails fast to avoid downstream surprises
  • If the token response includes token_type, and the provider was configured with allowed_token_types, the token_type must be present in the response and be one of the allowed types (e.g., Bearer). Failure aborts the flow

9. Fetch userinfo (optional)

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.

10. Validate ID token (OIDC only)

When using oauth_provider(id_token_validation = TRUE), the following verifications are performed:

  • Signature: verified against provider JWKS (with optional thumbprint pinning) for RS256/ES256; HS256 only with explicit opt-in and server-held secret
  • Claims: 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 rejected
  • Nonce: must match the previously stored value (if configured)
  • Subject match: if oauth_provider(userinfo_id_token_match = TRUE), it is checked that sub in userinfo equals sub in the ID token

11. Build the OAuthToken object

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

12. Clean URL & tidy UI; clear browser token

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.

13. Post-flow session management

Now that the flow is complete, the module will manage the token lifetime during the active session. This may consist of:

  • Proactive refresh: if enabled and a refresh token exists, the access token is refreshed before expiry
  • Expiration: expired tokens are cleared automatically, setting the $authenticated flag to FALSE
  • Re-authentication: optionally, oauth_module_server(reauth_after_seconds = ...) can force periodic re-authentication

AltStyle によって変換されたページ (->オリジナル) /