--- title: "Authentication flow" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Authentication flow} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ## 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 (with PKCE) and optional OpenID Connect 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. It helps bind the start and end of the OAuth flow to the same browser (a "double-submit" style CSRF defense) and prevents cross-tab confusion. ### 2. Decide whether to start login If `oauth_module_server(auto_redirect = TRUE)`, an unauthenticated session triggers immediate redirection. If `oauth_module_server(auto_redirect = FALSE)`, you manually call `$request_login()` (e.g., via a button) to begin the flow. ### 3. Build the authorization URL (`prepare_call()`) The authorization request contains multiple high-entropy and contextual elements: - State: high-entropy random string to prevent CSRF; *sealed* before being sent - 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 - Server-side one-time state store: keyed by a cache-safe hash of the raw state (derived from the state to satisfy cache backends); stores: browser token, code_verifier, nonce. Purged after callback - Sealed state payload: instead of sending raw state, the package seals (AES-GCM AEAD: encrypts + authenticates) a payload containing: - state, client_id, redirect_uri - requested scopes - provider fingerprint (issuer/auth/token URLs) - issued_at timestamp This sealing prevents tampering, stale callbacks, and mix-ups with other providers/clients. ### 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=`, 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 back 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()`) - Wait for the browser token input if not yet visible (page load race) before proceeding - Decrypt + verify sealed state: integrity (auth tag), authenticity, and freshness (issued_at window) - Check 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 - Strict semantics: 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`) with redacted context - Verify browser token equality with stored value - Ensure PKCE components are available when required Note: In asynchronous token exchange mode, the module may pre‑decrypt the sealed state and prefetch+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 A POST request is made to the token endpoint with `grant_type=authorization_code`, code, redirect_uri, and `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. (Optional) Fetch userinfo If `oauth_provider(userinfo_required = TRUE)`, the module calls the userinfo endpoint with the access token and stores returned claims. Failure aborts the flow. ### 10. (OIDC) Validate ID token 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` REQUIRED, `nbf` optional) are evaluated with a small configurable leeway; tokens issued in the future are rejected - Nonce: if used, matches stored value - Subject match: If `oauth_provider(userinfo_id_token_match = TRUE)`, ensure `sub` in userinfo equals ID token `sub` ### 11. Materialize the `OAuthToken` Constructs an S7 `OAuthToken` capturing `access_token`, optional `refresh_token`, expiry time, `id_token`, `userinfo`, and verification results. `$authenticated` becomes TRUE only after all requested verifications pass. ### 12. Clean URL & tidy UI Removes OAuth query parameters (`code`, `state`, etc.) from the browser's address bar and optionally adjusts the page title. Clears the browser token cookie to allow a fresh future flow. ### 13. Keeping the session alive - Proactive refresh: if enabled and a refresh token exists, refresh before expiry. - Expiration & reauth: expired tokens are cleared. Optional `oauth_module_server(reauth_after_seconds = ...)` forces periodic re-authentication. ### 14. Errors, logging, and safety rails - Structured errors surface short codes and (optionally) detailed descriptions (avoid exposing full descriptions to users) - If the browser cannot set the session-binding cookie or the Web Crypto API is unavailable, the module surfaces `browser_cookie_error` with a concise description and halts login attempts until resolved - Host and HTTPS constraints enforced early - State is single‑use and time‑limited; sealed state + cookie binding mitigate CSRF/state injection - State store access is strict: lookup or removal failures cause the flow to abort with a `shinyOAuth_state_error` to prevent replay/mix‑up - Hooks (`trace_hook`, `audit_hook`) provide structured telemetry without exposing raw tokens