Skip to contents

shinyOAuth (development version)

  • Updated the missing browser-setup warning and UI documentation to recommend oauth_ui() with the module ID and client, or the appropriate form-post or connection-manager wrapper. These wrappers include use_shinyOAuth() setup.

  • Made direct-browser deployment guidance more prominent. The Shiny module now uses Posit Connect / Connect Cloud environment markers to print a specific reminder once per R process to open the direct app URL outside the dashboard’s embedded preview.

  • Added provider helpers for ORCID (production and sandbox), GitLab (hosted and self-managed), Amazon Cognito, Globus Auth, Hugging Face, SURFconext (production and test), and authentik.

shinyOAuth 0.6.1

CRAN release: 2026-09-18

  • Fix to accept an empty scope response when requesting no scopes, preventing token exchange parse errors. Empty grants remain explicit during refresh and never inherit prior scopes.

shinyOAuth 0.6.0

CRAN release: 2026-09-17

  • Added optional connections for apps using one or more OAuth authorizations:

    • oauth_connection() combines a module’s current token with the client’s approved resource_bases and required_scopes. Its $request() method uses refreshed credentials and restricts calls to those APIs; status and selected identity information are available without exposing credentials.
    • oauth_connections(), with oauth_connections_ui() and oauth_connections_server(), manages separate authorizations, including repeated logins to the same provider. Connections normally end with the Shiny session; optional browser or local-account retention restores them after navigation.
    • oauth_connection_store_memory() stores encrypted credentials in one R process. oauth_browser_owner() and oauth_account_owner() configure who can restore them and for how long. Retention does not survive R restarts.
    • Managed $refresh(scopes = ...) can reduce permissions while preserving required scopes. Later refreshes retain the reduction; widening requires new authorization. Disconnect removes local access before optional remote revocation. Refresh and revocation account for credentials shared across connections.
    • callback_policy = "issuer" or "shared_routes" permits shared callback URLs with issuer validation; distinct routes remain the default and are required for encrypted JARM. Ordinary OAuth modules can share the UI through additional_clients.
  • Added SMART on FHIR STU 2.2 app support:

    • smart_discover() reads metadata from an approved FHIR server; smart_client() combines it with an existing public, secret-based, or asymmetric app registration for standalone or EHR launch. It checks server capabilities and permissions, requires S256 PKCE and TLS 1.2 or newer, and enables signed user identity only when requested.
    • smart_launch_route() accepts EHR launches for configured clients using browser retention. SMART supports direct authorization with query or form POST callbacks.
    • smart_context() reports the current patient, encounter, and validated user reference, with a revision for detecting context changes after refresh. smart_patient() and smart_fhir_user() fetch the corresponding FHIR resources only when authorized and within the configured server.
    • Required permissions remain enforced for reduced grants and refreshes. Offline access negotiation requires opt-in. Token responses must supply scopes and a positive lifetime; initial_expires_in_fallback can provide an explicit fallback for the initial response.
  • OAuthToken exposes additional token response parameters in extra_fields. Each successful refresh replaces that list; initial_extra_fields preserves the initial response. Both retain nested values and explicit nulls and are redacted when printed.

  • Added oauth_ui() to handle URL-based callbacks before the app UI loads. Wrap the UI with the same id and client used by oauth_module_server(). Both oauth_ui() and oauth_form_post_ui() accept named client registries for apps using multiple OAuth modules or providers.

  • Added check_oauth21(), a read-only assessment of client and provider configuration against OAuth 2.1 draft 16. It reports configuration problems, external prerequisites, and recommendations without making requests or changing settings, including the effective TLS policy for SMART clients.

  • Browser bindings are isolated by origin, tab, login, and app/module and stay out of URLs and bookmarks. Callbacks enforce the registered path and response transport, preserve escaped paths, and reject oversized input before routing. Shared callback state is encrypted and requires atomic storage. oauth_form_post_ui() supports trusted-proxy URI resolution. New visitors cannot evict retained connections or browsers with pending authorizations.

  • Added signed and encrypted authorization responses (JARM) through response_mode = "jwt", "query.jwt", or "form_post.jwt" with oauth_module_server(). Algorithms can be configured or discovered; jarm_max_lifetime defaults to 600 seconds. Malformed claims and headers are rejected. handle_callback() continues to accept only classic callbacks.

  • authorization_server_mode supports multiple authorization servers through validated issuer responses or distinct registered callback routes. compare_callback_issuer can check a supplied issuer without requiring older providers to send one; required issuer and JARM checks remain enforced.

  • Function argument names are more consistent. CRAN 0.5.0 argument names and positions remain supported, as do the existing S7 property names:

    • Login, token, userinfo, resource-request, and mTLS registration helpers consistently accept client and token.
    • handle_callback() accepts callback state as state; introspection and revocation helpers select access or refresh tokens with token_kind.
    • Provider constructors and discovery use id_token_allowed_algs for ID-token signing algorithms. Clients use introspection_checks for extra introspection requirements.
    • Client assertion keys, mTLS, Request Object, and JARM settings use client_assertion_*, mtls_*, request_object_*, and jarm_* names. See ?oauth_client and ?oauth_provider for the current arguments.
    • oauth_module_server() uses refresh_check_interval_ms to make the polling interval’s millisecond unit explicit.
  • Client and provider configuration changes:

    • Client credentials no longer default from OAUTH_CLIENT_ID or OAUTH_CLIENT_SECRET; an omitted secret is treated as absent. Public clients must use PKCE. Printed objects redact credentials and sensitive URL components.
    • endpoint_auth configures authentication separately for PAR, introspection, and revocation, including credentials, assertions, mTLS, and headers. Discovered authentication methods and algorithms are negotiated per endpoint.
    • client_assertion_typ supports JWT and client-authentication+jwt, including per-endpoint overrides.
    • Low-level S7 constructors use the same OIDC and DPoP security defaults as helper constructors and reject malformed security and assurance settings.
  • Provider configuration changes:

    • infer_oidc_from_issuer = FALSE allows generic OAuth issuer validation without enabling OIDC; the default remains TRUE.
    • An explicit jwks_uri overrides discovery across signing-key consumers. Missing or disallowed required key locations fail during configuration.
    • userinfo_allowed_algs configures signed UserInfo algorithms independently from ID-token algorithms, with corresponding discovery support.
    • allow_missing_token_type = TRUE explicitly permits Bearer-only providers that omit the required token type. The default remains strict; the fallback does not apply to DPoP clients or replace invalid values.
  • Provider helper updates:

    • Added oauth_provider_apple() and oauth_client_secret_apple() for Apple sign-in. Validated Apple email_verified strings become logical values, including in claim checks and id_token_claims.
    • oauth_provider_microsoft() rejects unrecognized tenants instead of silently disabling OIDC. Use a directory GUID or a supported tenant alias; OAuth-only operation requires explicit id_token_validation = FALSE.
    • oauth_provider_slack(profile = ...) supports "confidential" and "public_pkce" registrations.
    • Spotify uses immutable account_id. Migrate stored id mappings and audit digests from an authenticated profile; allow_legacy_id = TRUE permits a temporary fallback when account_id is absent.
    • Okta’s org server is supported, Auth0’s trailing-slash issuer is preserved, and oauth_provider_oidc() respects its authentication-style override.
  • OIDC discovery accepts an issuer or its standard discovery URL, preserves the exact issuer and endpoint paths, and requires an exact issuer match. Required metadata and URL syntax are validated; optional capability arrays may be empty but cannot be null or malformed. HTTPS is required unless shinyOAuth.allow_insecure_oidc_loopback = TRUE permits loopback development. Endpoints may use other hosts subject to shinyOAuth.allowed_hosts; JWKS retains its separate host policy. Transport failures identify the attempted discovery URL and underlying error.

  • JWKS handling rejects malformed keys and inconsistent key usage, while allowing unrelated public extensions. Discovery preserves escaped issuer paths and enforces current host and pinning policies. Caching respects HTTP freshness and no-store directives; rotation works when a provider reuses a key ID. RSA keys require at least 2048 bits and a trusted algorithm choice: unlabelled keys use RS256 unless a single RSA algorithm is configured. Generic OAuth discovery can fall back to OIDC discovery locations.

  • Ed25519 keys support both EdDSA and Ed25519 for verification, client assertions, Request Objects, and DPoP. Outbound RSA signing also supports explicitly selected RS384; RS256 remains the default.

  • OIDC claim validation consistently checks standard and optional claim types in ID tokens and UserInfo, including refreshed nonces. UserInfo must match the validated ID token’s sub; additional audiences require explicit trust and multiple audiences require the correct azp. Future authentication times are rejected, and requested claim objects compare independently of member order. JSON profile arrays retain their existing vector and data-frame structure.

  • Authentication deadlines are not extended by refresh. Token lifetimes are calculated conservatively, and long lifetimes and future timestamps avoid integer overflow; invalid lifetime settings are rejected. Concurrent refreshes are combined, stale results are ignored, and the login ID token remains the continuity baseline. Refresh credentials are retired after potentially consuming failures. Ordinary OAuth refreshes explicitly request known retained scopes so earlier reductions are preserved.

  • DPoP-bound tokens, including opaque tokens, work consistently with UserInfo and resource helpers and cannot be downgraded to Bearer. Proofs preserve the actual request path, and retries use fresh nonces from intermediate responses. The nonce limit defaults to 4096 bytes, configurable with shinyOAuth.dpop_nonce_max_bytes; oversized responses report a clear error. Malformed or conflicting token-binding thumbprints are rejected.

  • Authorization request, JAR, and PAR improvements:

    • authorization_method = "POST" supports long browser requests when the provider supports it; SMART requires advertised authorize-post. Shiny submits the form automatically. prepare_authorization_request() returns the URL, method, and form fields for custom browser navigation.
    • Authorization URLs reject conflicting or repeated managed parameters while preserving valid fixed queries and repeated resource indicators. Configure policy such as scopes and max_age through client properties or extra_auth_params, not the endpoint URL; policy applies across transports.
    • Hosted Request Objects use HTTPS and one-time handles through oauth_ui(). Encryption accepts public PEM keys and honors key metadata, size, and pins, with one refresh on key-selection failure and hardened decryption checks.
    • PAR avoids generic retries that could duplicate authorization requests; bounded DPoP nonce retries remain available.
  • mTLS improvements:

    • mtls_require_observed_cnf defaults to TRUE. For server-enforced opaque tokens, keep mtls_certificate_bound_access_tokens = TRUE and set it to FALSE; any observed binding must still match.
    • Certificate chains must be leaf-first. Registration validates subject/SAN identifiers, issuer ordering, IPv6 SANs, and HTTPS registration key URLs. Certificate rotation no longer causes unbounded cache growth.
    • Unsupported PEM-based mTLS on Windows Schannel reports a targeted error.
  • Token, introspection, and HTTP handling validates credentials, media types, response sizes, activity, and scopes before use. Form encoding is consistent, and supported parameter overrides replace existing values. Resource helpers preserve escaped paths when adding queries, reject competing credentials and inherited authentication/cache/retry policies, and support per-call resource_hosts. Method-changing curl options and HEAD requests with bodies are rejected to prevent unsafe retries and incorrect DPoP proofs; configure methods with httr2::req_method(). Retries honor bounded Retry-After values. Invalid numeric retry/assertion options use safe defaults, and URL-check flags must be logical. shinyOAuth.tls_min_version can require TLS 1.2 or 1.3 across package requests and async workers.

  • Audit and OpenTelemetry output is safer by default:

    • Conditions, displays, hooks, and exports redact or bound sensitive provider data, URLs, and header parameters. Unknown OAuth errors become unknown; diagnostic text is literal.
    • Native hooks receive shiny_session$session_token_digest; raw tokens require shinyOAuth.audit_include_raw_session_token = TRUE. Authorization details require shinyOAuth.otel_include_authorization_details = TRUE.
    • Configured audit digest keys require at least 32 bytes; invalid values fail clearly. Callback rejection and connection disconnect/revocation outcomes are reported consistently.
  • The Cloud Run example uses pinned deployment dependencies. Spotify examples handle remote content safely and fix playback formatting; the dashboard is installed as examples/spotify-dashboard.R.

  • Async use requires mirai >= 2.5.1; the future_promise() fallback uses parallel-safe random-number generation.

shinyOAuth 0.5.0

CRAN release: 2026-05-23

  • Added mutual-TLS (‘mTLS’, RFC 8705) support, including mTLS client authentication, certificate-bound access tokens, mTLS endpoint aliases, and the exported oauth_client_mtls_registration() helper for RFC 8705 client metadata.

  • Added Demonstrating Proof-of-Possession (‘DPoP’, RFC 9449) support. Clients configured with dpop_private_key now send DPoP proofs on token and protected-resource requests, include dpop_jkt in authorization requests, can require token_type = "DPoP" plus cnf.jkt binding, and replay one DPoP-Nonce challenge on token and protected-resource requests.

  • Added JWT-Secured Authorization Request (‘JAR’, RFC 9101) support. oauth_client() can now send signed and encrypted Request Objects via request_object_mode = "request" (sent as parameter) or via request_object_mode = "request_uri" (served from your Shiny app).

  • Added Pushed Authorization Request (‘PAR’, RFC 9126) support. Providers can now configure par_url directly or pick it up from OIDC discovery, and login flows will push the authorization request and redirect with the returned request_uri.

  • Added response_mode = "form_post" support for authorization-code callbacks. Apps can wrap their UI with oauth_form_post_ui() so Shiny accepts the provider POST, stores the callback server-side under a one-time handle, and lets oauth_module_server() finish the existing state, issuer, and token exchange flow. Stored callback handles are bounded by the effective state/store TTL and the shinyOAuth.callback_max_form_post_* size-cap options.

  • Added OpenTelemetry (‘OTel’) support (using the ‘otel’ package). ‘shinyOAuth’ now emits OTel logs from existing audit events and traces key OAuth operations such as module initialization, login/callback handling, token exchange/refresh, userinfo/introspection/revocation, and session-end cleanup. See vignette("opentelemetry", package = "shinyOAuth") for more information. The span catalog includes form-post callback state consumption and documents the opt-in policy for exception messages.

  • Observability and audit logging improvements:

    • Improved observability correlation for existing audit flows. Interactive login now reuses a single flow trace_id across redirect issuance, callback validation, token exchange, and login outcome events, making it easier to correlate the pre-redirect and post-redirect Shiny sessions for a single login round-trip; async work also carries more accurate originating Shiny session/process context into worker-emitted events.
    • audit_login_success$sub_source = "id_token" now reflects the OAuthToken@id_token_validated result for the returned token, so telemetry no longer overstates ID-token validation when tests or debug options skip signature verification.
    • Sanitized HTTP audit context now redacts remote_addr as well as proxy headers, so default audit events no longer export raw client IP addresses.
    • Improved existing audit event types. audit_token_exchange and audit_token_refresh now include expires_in_synthesized, indicating that the provider did not return a usable expires_in and shinyOAuth had to synthesize one; audit_login_failed now distinguishes async payload-validation and state-store-lookup failures from async token-exchange failures; audit_userinfo distinguishes missing sub and JWT/JWKS validation failures; and error-state consumption events use the logical state digest when available for better correlation. See vignette("audit-logging", package = "shinyOAuth") for more information.
    • options(shinyOAuth.trace_hook = ...) is no longer treated as a separate documented event sink. Prefer options(shinyOAuth.audit_hook = ...); the old trace_hook option now remains only as a backward-compatible alias when audit_hook is unset.
    • Removed the documented global shinyOAuth.print_errors / shinyOAuth.print_traceback options. Internal console error logging now uses explicit internal flags instead of package-wide option fallbacks.
    • http_error audit events now omit raw provider oauth_error_description text by default and keep only oauth_error, oauth_error_uri, and body_digest. The raw description is emitted only when options(shinyOAuth.expose_error_body = TRUE) is enabled for debugging.
    • err_http() now strips query strings, fragments, and userinfo from response URLs before surfacing them through condition messages and emitted events, reducing leakage of authorization codes, state, request URIs, and similar URL-borne secrets.
  • oauth_module_server() now:

    • Explicitly ignores new login requests while a session is already authenticated.
    • Fails cleanly at startup when revoke_on_session_end = TRUE but the provider does not expose a revocation_url, instead of crashing while formatting that configuration error.
    • Applies the browser-token double-submit check to OAuth error callbacks too, deferring ?error=... handling until the browser token is available and treating browser-token mismatches as invalid_state instead of surfacing provider-controlled error text.
    • Forwards oauth_client(introspect = TRUE) to its proactive refresh path, so proactive refresh now follows the same refresh-time token introspection policy as direct refresh_token(..., introspect = TRUE) calls.
    • Preserves invalid_state in its callback error state for CSRF/state/browser-token validation failures instead of flattening those paths into token_exchange_error.
    • Drops provider error_uri values unless they are absolute HTTPS URLs, so unsafe schemes like javascript: are no longer surfaced through values$error_uri.
    • Validates browser_cookie_path more strictly, requiring a leading / and rejecting semicolons or control characters so unsafe cookie attributes cannot be injected through the configured cookie Path.
  • OAuthToken and OAuthClient now print with redacted token/secret/key previews instead of exposing full credential material in default console output.

  • OAuthToken now tracks normalized granted_scopes plus granted_scopes_verified, so apps can distinguish between scope sets that were explicitly returned and ones that were carried forward when the provider omitted scope. Refresh now preserves prior granted scopes instead of widening back to the client’s configured scopes when a refresh response omits scope.

  • oauth_client() (OAuthClient) now:

    • Accepts custom state-store entries that omit unused pkce_code_verifier and nonce fields when the provider does not require them, while still rejecting missing PKCE or nonce values when those checks are enabled.
    • Supports enforce_callback_issuer = TRUE to require the RFC 9207 iss callback parameter for shared-redirect multi-issuer deployments. Relatedly, handle_callback() now accepts iss, so advanced callers building around prepare_call() can supply the callback issuer and get the same client-level RFC 9207 check before token exchange.
    • Auto-enables RFC 9207 callback issuer enforcement when the caller leaves enforce_callback_issuer unset and the provider advertises authorization_response_iss_parameter_supported = TRUE.
    • Has native RFC 8707 resource support, so authorization, token exchange, and refresh requests can request audience-restricted tokens without dropping down to manual extra params.
    • Defaults scope_validation to "warn", so RFC-compliant reduced grants surface as warnings unless you opt into scope_validation = "strict".
    • Warns when list-based OIDC claims requests use a single-element values entry without I(...), because jsonlite::toJSON(auto_unbox = TRUE) would otherwise serialize that OIDC array constraint as a scalar.
    • Rejects impossible JOSE alg/private-key combinations for JWT client assertions and DPoP proofs before emitting invalid JOSE headers.
    • Also enforces OIDC claim request value and values constraints when claims_validation is enabled, not just presence of essential = TRUE claims.
    • Defaults enforceable OIDC claim requests to claims_validation = "warn" when callers request essential or value-constrained claims and do not set claims_validation explicitly, so claim mismatches are surfaced by default unless callers opt out with claims_validation = "none".
    • Carries the same effective requested scopes through the whole login flow. If openid is auto-added to the authorization request, the sealed state payload, token-response scope validation, and introspection scope validation now use that same effective scope set.
    • Uses a validated ID token sub for introspect_elements = "sub" before falling back to userinfo, so unvalidated ID token payloads no longer anchor the introspection subject check.
    • Binds the sealed callback state to the effective provider and client security policy used after redirect. Multi-worker deployments must now keep callback/login validation settings aligned across workers; otherwise callbacks fail fast with invalid_state instead of resuming under a different worker policy.
  • oauth_provider() (OAuthProvider) now:

    • Allows response_mode = "query" and response_mode = "form_post" for authorization-code callbacks, while still rejecting unsupported modes such as "fragment". When provider metadata advertises response_modes_supported, shinyOAuth also fails fast if an explicit response mode is not advertised.
    • Treats nonce-enabled OIDC flows as sufficient validation context for userinfo_id_token_match, and shinyOAuth now always binds userinfo to a validated ID token subject when that baseline exists.
    • Raises typed shinyOAuth_input_error conditions for malformed constructor inputs such as vector endpoint URLs or empty discovery-helper domains, so apps can trap provider validation failures consistently.
    • Validates custom jwks_cache$get() signatures without calling the cache during construction, avoiding side effects in duck-typed cache backends.
    • Validates allowed_algs against shinyOAuth’s actual inbound verifier support and no longer accepts RSA-PSS (PS256, PS384, PS512) entries, which were previously present in the generic helper’s defaults despite lacking verifier support. Older configs that explicitly allowed those algorithms must switch to supported verifier algorithms.
    • Reserves the refresh_token parameter name in extra_token_params to prevent duplicate refresh-token parameters during token refresh requests.
  • oauth_provider_oidc_discover() now:

    • Fails fast when metadata advertises PKCE methods but omits S256. shinyOAuth keeps S256 as the default and only allows a downgrade to plain when you pass pkce_method = "plain" explicitly.
    • Fails fast when the discovery document omits jwks_uri but the selected policy still needs signing keys, including id_token_validation = TRUE, nonce-enabled OIDC flows, and signed UserInfo JWT validation. These misconfigurations now fail during provider setup instead of later during a JWKS fetch.
    • Validates discovered jwks_uri values against the same absolute-URL, scheme, and host policy used for other discovery endpoints, so invalid or disallowed JWKS URLs now fail during discovery instead of later during the first JWKS fetch.
    • Rejects issuer inputs that contain query strings or fragments before building the discovery URL, matching the stricter issuer validation already used by manually configured providers.
    • Rejects non-scalar endpoint metadata values with a configuration error.
    • Honors jwks_host_allow_only during its early jwks_uri host check, so explicitly pinned cross-host JWKS endpoints no longer require disabling issuer-host matching.
    • Maps token_endpoint_auth_methods_supported = ["none"] to a distinct public token auth style that never sends client_secret, even when oauth_client() picks one up from OAUTH_CLIENT_SECRET.
  • oauth_provider_oidc() now trims trailing slashes from base_url before deriving endpoint URLs and the configured issuer, avoiding valid ID tokens being rejected on a strict OIDC iss comparison when the helper was configured with a URL like https://issuer.example/.

  • oauth_provider_microsoft() no longer drops the Microsoft alias tenants to OAuth 2.0 plus userinfo identity by default. common and organizations now validate ID tokens using Microsoft’s tenant-independent issuer and signing-key issuer rules, and consumers now validates against the stable consumer tenant issuer.

  • validate_id_token() now properly rejects auth_time claims set in the future (beyond leeway). Previously, a future auth_time produced a negative elapsed value that always passed the max_age freshness check.

  • introspect_token() now uses provider@userinfo_id_selector consistently when it checks the authenticated subject against fetched UserInfo data, and now fails closed on malformed introspection JSON. Non-object responses are rejected instead of being normalized from the first parsed element.

  • refresh_token() now:

    • Refuses to update userinfo unless it can verify the refreshed identity against a new or preserved ID token subject, preventing identity confusion when providers omit id_token from refresh responses.
    • When called with introspect = TRUE, enforces token introspection as a hard policy check instead of best-effort metadata enrichment. Refresh now fails when introspection is unsupported, inactive, malformed, or missing required introspect_elements such as sub, client_id, or scope.
  • get_userinfo() now:

    • Applies the same hard JWK alg compatibility checks to signed UserInfo JWT verification as ID token verification, rejecting JWKS keys that advertise a different algorithm even if signature verification would otherwise succeed.
    • Always requires a non-empty sub claim in userinfo responses from OIDC providers (those with an issuer configured), per OIDC Core section 5.3. Previously, a non-compliant response without sub could be accepted if userinfo_id_token_match was not enabled. The signed-JWT path (validate_signed_userinfo_claims()) also now checks sub alongside the existing iss/aud validation.
  • Signed UserInfo JWT validation now:

    • Enforces exp, iat, and nbf when those temporal claims are present, rejecting expired or not-yet-valid UserInfo JWT responses instead of accepting them based only on signature/issuer/audience. oauth_client() can also require specific UserInfo JWT temporal claims to be present via userinfo_jwt_required_time_claims.
    • Uses internal JWS verifier instead of jose::jwt_decode_sig(), so EdDSA UserInfo JWTs can verify correctly, provider leeway is honored consistently, and invalid typ headers are rejected.
  • Successful token and refresh responses now always require token_type, even when allowed_token_types = character(). An empty allowlist still disables value allowlisting, but it no longer waives the RFC-required field.

  • Refreshed OIDC ID tokens now enforce full continuity for auth_time, refresh-time nonce, and azp in addition to the existing iss / sub / aud checks.

  • Token exchange and refresh requests no longer retry on transport errors or transient HTTP statuses (408/429/5xx). Authorization codes are single-use and refresh tokens may be rotated on each use; retrying after the server has already committed the first request would replay an invalidated credential, causing invalid_grant errors or triggering refresh-token replay detection.

  • Hardened runtime JWKS discovery by validating the discovery issuer before trusting jwks_uri. This policy is now stored on OAuthProvider via issuer_match, so both provider discovery and runtime JWKS fetches apply the same rule: url for exact issuer URL matching, host for scheme-and-host matching, or none to skip the discovery issuer check.

  • JWKS caching now respects global host policy immediately. Cached entries are scoped to the current allowed_hosts / allowed_non_https_hosts settings, and cache hits re-check the stored jwks_uri before a JWKS is trusted.

  • Scope validation now treats an omitted scope in the initial token response as unchanged from the requested scope, matching RFC 6749 section 5.1 instead of rejecting otherwise compliant authorization servers by default.

  • Strict token-response and introspection scope validation now treats commas as part of a single scope token, matching RFC 6749 instead of splitting scope = "read,write" into separate read and write scopes.

  • Missing expires_in values now default to a finite 3600-second fallback rather than an effectively indefinite session. Override this with options(shinyOAuth.default_expires_in = <seconds>), and use oauth_module_server(reauth_after_seconds = ...) when you need a stricter session-age cap.

  • err_http() now guards against oversized HTTP error bodies before hashing or JSON parsing, so large chunked or misleading error responses now trip the existing body-size limit consistently.

  • at_hash validation now resolves EdDSA from the verified signing key/JWK instead of guessing from alg alone: Ed25519 uses the exact SHA-512 mapping, while signature-skipped or currently unsupported EdDSA curves fail closed.

  • Deprecated error_on_softened(). It remains a narrow guard for a few dev/debug softeners, but the docs now stop presenting it as a comprehensive deployment-hardening check and show explicit option checks instead.

  • Renamed the resource-request helpers to resource_req() and perform_resource_req(). The existing public client_bearer_req() name remains available as a deprecated alias, and perform_client_bearer_req() is also exported as a deprecated compatibility alias.

  • perform_resource_req() is a new function which builds and performs an authenticated resource-request and, for DPoP-bound access tokens, replays one use_dpop_nonce challenge with the server-provided nonce. It can also take pre-existing ‘httr2’ request objects and layer authentication and DPoP on top.

shinyOAuth 0.4.0

CRAN release: 2026-02-14

  • ‘mirai’ & async backend improvements:

    • Warnings and messages emitted in async workers (e.g., missing expires_in from token response) are now captured and re-emitted on the main process so they appear in the R console. This includes conditions from user-supplied trace_hook / audit_hook functions: warnings, messages, and errors (surfaced as warnings) all propagate back to the main thread. Replay can be disabled via options(shinyOAuth.replay_async_conditions = FALSE).
    • Async callback flow no longer serializes the full client object (including potentially non-serializable custom state_store / JWKS cache backends) into the worker context. The state_store (already consumed on the main thread) is replaced with a lightweight serializable dummy before dispatch. If the client still fails serialization, the flow falls back to synchronous execution with an explicit warning instead of an opaque runtime error.
    • Further reduced serialization overhead towards async workers by using certain functions from the package namespace directly.
    • Detect active daemons via mirai::daemons_set() instead of mirai::status(). Falls back to mirai::info() on older ‘mirai’ versions that lack mirai::daemons_set() (< 2.3.0).
    • Configurable per-task timeout via options(shinyOAuth.async_timeout) (milliseconds); timed-out ‘mirai’ tasks are automatically cancelled by the dispatcher. Default is NULL (no timeout).
    • Async audit events now include a mirai_error_type field. This classifies mirai transport-level failures separately from application-level errors.
    • Prevent ‘mirai’ warning spam about ‘stats’ maybe not being available in workers.
  • ID token validation (validate_id_token()):

    • Now enforces RFC 7515 section 4.1.11 critical header parameter (crit) processing rules. Tokens containing unsupported critical extensions are rejected with a shinyOAuth_id_token_error. The current implementation supports no critical extensions, so any crit presence triggers rejection.
    • Now validates the at_hash (Access Token hash) claim when present in the ID token (per OIDC Core section 3.1.3.8 and 3.2.2.9). If the claim exists, the access token binding is verified; a mismatch raises a shinyOAuth_id_token_error. New id_token_at_hash_required property on OAuthProvider (default FALSE) forces login to fail when the ID token does not contain an at_hash claim.
    • Now validates, for refreshed ID tokens, per OIDC Core section 12.2, iss and aud claims against the original ID token’s values (not just the provider configuration) to cover edge cases with multi-tenant providers or rotating issuer URIs. Enforced in both validated and non-validated code paths.
    • Now detects encrypted ID tokens (JWE compact serialization, 5 dot-separated segments) early and raises a clear shinyOAuth_id_token_error instead of letting a confusing alg/typ/parse failure propagate.
    • Now validates the auth_time claim when max_age is present in extra_auth_params (OIDC Core section 3.1.2.1).
    • Now enforces a package-defined maximum ID token lifetime (exp - iat); tokens with unreasonably long lifetimes are rejected with a shinyOAuth_id_token_error. Configure via options(shinyOAuth.max_id_token_lifetime = <seconds>) (default of 86400 which is 24 hours). Set to Inf to disable the check.
  • Stricter state store usage:

    • custom_cache() gains an optional take parameter for atomic get-and-delete.
    • state_store_get_remove() prefers $take() when available; falls back to $get() + $remove() with a mandatory post-removal absence check (instead of trusting $remove() return values).
    • Non-cachem::cache_mem() stores without $take() now error by default to prevent TOCTOU replay attacks in shared/multi-worker deployments. To bypass this error, operators must explicitly acknowledge the risk by setting options(shinyOAuth.allow_non_atomic_state_store = TRUE), which downgrades the error to a warning.
    • OAuthClient validator now validates $take() signature when present.
    • The $remove() return value is no longer relied upon in the fallback path; the post-removal $get() absence check is authoritative.
  • Stricter JWKS cache handling: JWKS cache key now includes host-policy fields (jwks_host_issuer_match, jwks_host_allow_only). Previously, two provider configs for the same issuer with different host policies shared the same cache entry, allowing a relaxed-policy provider to populate the cache and a strict-policy provider to skip host validation on cache hit. Cache entries now also store the JWKS source host and re-validate it against the current provider policy on read (defense-in-depth).

  • Stricter URL validation: OAuthClient now rejects redirect URIs containing fragments (per RFC 6749, section 3.1.2); OAuthProvider now rejects issuer identifiers containing query or fragment components, covering both oauth_provider_oidc_discover() and manual construction of providers.

  • Stricter state payload parsing: callback state now rejects embedded NUL bytes before JSON decoding.

  • Stricter response size validation: enforce max response body size on all outbound HTTP endpoints (token, introspection, userinfo, OIDC discovery, JWKS). Curl aborts the transfer early when Content-Length exceeds the limit; a post-download guard catches chunked responses. Default 1 MiB, configurable via options(shinyOAuth.max_body_bytes).

  • OAuthProvider (S7 class):

    • leeway validator now rejects non-finite values (Inf, -Inf, NaN). Previously these passed validation but were silently coerced to 0 at runtime, effectively disabling clock-skew tolerance.
    • Reserved OAuth parameter blocking in extra_auth_params and extra_token_params is now case-insensitive and trims whitespace.
    • Vector inputs for pkce_method and URL parameters (auth_url, token_url, userinfo_url, introspection_url, revocation_url) now produce clear scalar-input errors instead of cryptic coercion failures.
  • OAuthClient (S7 class):

    • Gains a claims_validation property; when the client sends a structured claims request parameter with essential = TRUE entries, this setting controls whether the returned ID token and/or userinfo response are checked for those essential claims (similar to scope_validation).
    • Gains a required_acr_values property; enables client-side enforcement of the OIDC acr (Authentication Context Class Reference) claim.
    • extra_token_headers are now consistently applied to revoke and introspect requests, matching the existing behavior for token exchange and refresh. Previously, provider integrations requiring custom headers across all token endpoints could partially fail on revocation/introspection.
    • Fixed incorrect warning about client being created in Shiny when this was not the case.
    • Malformed client_assertion_alg and client_assertion_audience values (e.g., character(0), multi-element vectors) now produce clear validation errors instead of crashing with base R subscript-out-of-bounds errors. Empty string "" for client_assertion_audience is now explicitly rejected instead of being silently treated as “not provided”.
  • OAuthToken (S7 class):

    • Gains a read-only id_token_claims property that exposes the decoded ID token JWT payload as a named list, surfacing all OIDC claims (e.g., acr, amr, auth_time) without manual decoding.
    • Gains an id_token_validated property (logical) indicating whether the ID token was cryptographically verified during the OAuth flow.
  • oauth_module_server():

    • Now surfaces error_uri from provider error callbacks (RFC 6749, section 4.1.2.1). The new $error_uri reactive field contains the URI to a human-readable error page when the provider includes one; NULL otherwise. The error_uri callback parameter is also validated against a configurable size limit (e.g., options(shinyOAuth.callback_max_error_uri_bytes = 2048)).
    • OAuth callback query cleanup is now also applied in early return paths of internal function .process_query(), ensuring more consistent cleanup.
    • OAuth callback query size caps are now enforced even when the user is already authenticated. Previously, the “token already present” branch in .process_query() called .query_has_oauth_callback_keys() (which parses the query string) before any size validation, bypassing the intended DoS guardrails. The validate_untrusted_query_string() check now runs unconditionally at the top of .process_query().
    • OAuth callback error responses (?error=...) now require a valid state parameter. Missing/invalid/consumed state is then treated properly as an invalid_state error instead of surfacing the error from ?error=... (which could be set by an attacker).
    • OAuth callback including an iss query parameter now validate this against the provider’s configured/discovered issuer during callback processing (complementing the existing ID token iss claim validation that occurs post-exchange) (per RFC 9207). A mismatch produces an issuer_mismatch error and audit event, defending against authorization-server mix-up attacks in multi-provider scenarios. When iss is absent, current behavior is retained (no enforcement).
  • handle_callback(): no longer accepts decrypted_payload and state_store_values bypass parameters. These parameters were only intended for internal use by oauth_module_server()’s async path. As they can be misused by direct/custom callers to bypass important security checks, they have been moved to an internal-only helper function (handle_callback_internal()).

  • handle_callback()/refresh_token(): when a token response omits expires_in, a warning is now emitted once per phase (exchange_code / refresh_token) so operators know that proactive token refresh will not trigger. Users can now also set a finite default lifetime for such tokens via options(shinyOAuth.default_expires_in = <seconds>); when unset, shinyOAuth now falls back to 3600 seconds.

  • get_userinfo() now supports JWT-encoded userinfo responses per OIDC Core, section 5.3.2. When the endpoint returns Content-Type: application/jwt, the body is decoded as a JWT. Verification is fail-closed: signature verification is always performed against the provider JWKS using the provider’s allowed_algs, alg=none is always rejected, and unparseable headers, non-asymmetric algorithms, or missing issuer/JWKS infrastructure all raise errors. options(shinyOAuth.allow_unsigned_userinfo_jwt = TRUE) permits unsigned JWTs. New userinfo_signed_jwt_required property on OAuthProvider (default FALSE) mandates that the userinfo endpoint returns application/jwt content-type which is then subject to the above verification.

  • client_bearer_req() now validates the target URL against is_ok_host() before attaching the Bearer token. Relative URLs, plain HTTP to non-loopback hosts, and hosts outside options(shinyOAuth.allowed_hosts) are rejected by default. A new check_url argument (default TRUE) allows opting out of the check when the URL has already been validated.

  • err_http() now extracts RFC 6749 section 5.2 structured error fields (error, error_description, error_uri) from JSON error response bodies. These fields are surfaced in the error message bullets, attached to the condition object (as oauth_error, oauth_error_description, oauth_error_uri), and included in trace/audit events. This improves debugging of token endpoint failures (e.g. invalid_grant, invalid_client) without changing existing control flow.

  • OIDC claims parameter support (OIDC Core, section 5.5): OAuthClient and oauth_client() now accept a claims argument to request specific claims from the userinfo Endpoint and/or in the ID token. Pass a list structure (automatically JSON-encoded) or a pre-encoded JSON string.

  • OIDC openid scope enforcement: when a provider has an issuer set (indicating OIDC) and openid is missing from the client’s scopes, build_auth_url() now auto-prepends it and emits a one-time warning.

  • OIDC discovery (oauth_provider_oidc_discover()) now prefers confidential auth methods (client_secret_basic, client_secret_post) over none when both are advertised in token_endpoint_auth_methods_supported. Previously, mixed metadata (e.g. none + client_secret_basic) with PKCE enabled would silently select the public-client posture ("body" without credentials).

  • Scope validation now aligns with the RFC 6749, section 3.3 scope-token grammar (NQSCHAR = %x21 / %x23-5B / %x5D-7E). The previous regex rejected valid ASCII characters such as !, #, $, =, @, ~, and others. All printable ASCII except space, double-quote, and backslash is now accepted.

  • JWT helpers (build_client_assertion(), resolve_client_assertion_audience()) now have defense-in-depth scalar guards so malformed property values cannot cause subscript errors at runtime.

  • Audit events:

    • audit_token_refresh: replaced non-informative had_refresh_token field (always TRUE post-mutation) with refresh_token_rotated (indicates whether the provider returned a new refresh token).

shinyOAuth 0.3.0

CRAN release: 2026-01-30

  • Async backend: the default async backend is now ‘mirai’ (>= 2.0.0) for simpler and more efficient asynchronous execution. Use mirai::daemons() to configure async workers. A ‘future’ backend configured with future::plan() is still supported, but ‘mirai’ takes precedence if both are configured.

  • Test suite: fixed inconsistent results of several tests; tests not suitable for CRAN now skip on CRAN. Silenced test output messages to avoid confusion.

shinyOAuth 0.2.0

CRAN release: 2026-01-13

New/improved

Security

  • Token revocation: tokens can now be revoked when Shiny session ends. Enable via revoke_on_session_end = TRUE in oauth_module_server(). The provider must expose a revocation_url (auto-discovered for OIDC, or set manually via oauth_provider()). New exported function revoke_token().

  • Token introspection on login: validate tokens via the provider’s introspection endpoint during login. Configure via introspect and introspect_elements properties on OAuthClient. The provider must expose an introspection_url (auto-discovered for OIDC, or set manually via oauth_provider()).

  • DoS protection: callback query parameters and state payload/browser token sizes are validated before expensive operations (e.g., hashing for audit logs). Maximum size may be configured via options(); see section ‘Size caps’ in vignette("usage", package = "shinyOAuth").

  • DoS protection: rate-limited JWKS refresh: forced JWKS cache refreshes (triggered by unknown kid) are now rate-limited to prevent abuse.

  • JWKS pinning: pinning is now enforced during signature verification: previously, jwks_pins with jwks_pin_mode = "any" only verified that at least one key in the JWKS matched a pin, but signature verification could still use any matching key (pinned or not). Now, signature verification is restricted to only use keys whose thumbprints appear in the pin list, ensuring true key pinning rather than presence-only checks.

  • use_shinyOAuth() now injects <meta name="referrer" content="no-referrer"> by default to reduce leaking ?code=…&state=… via the Referer header on the callback page. Can be disabled with use_shinyOAuth(inject_referrer_meta = FALSE).

  • Sensitive outbound HTTP requests (token exchange/refresh, introspection, revocation, userinfo, OIDC discovery, JWKS) now by default disable redirect following and reject 3xx responses to prevent bypassing host/HTTPS policies. Configurable via options(shinyOAuth.allow_redirect = TRUE). client_bearer_req() also gains follow_redirect, which defaults to FALSE, to similarly control redirect behavior for requests using bearer tokens.

  • State is now also consumed in login failure paths (when the provider returns an error but also a state).

  • Callback URL parameters are now also cleared in login failure paths.

  • OAuthProvider now requires absolute URLs (scheme + hostname) for all endpoint URLs.

  • Provider fingerprint now includes userinfo_url and introspection_url, reducing risk of misconfiguration when multiple providers share endpoints.

  • state_payload_max_age property on OAuthClient for independent freshness validation of the state payload’s issued_at timestamp.

  • Default client assertion JWT TTL reduced from 5 minutes to 120 seconds, reducing the window for replay attacks while allowing for clock skew.

Auditing

  • New audit events: session_ended (logged on Shiny session close), authenticated_changed (logged when authentication status changes), token_introspection (when introspect_token() is used), token_revocation (when revoke_token() is used), error_state_consumed and error_state_consumption_failed (called when provider returns an error during callback handling and the state is attempted to be consumed).

  • All audit events now include $process_id, $is_async, and $main_process_id (if called from an async worker); these fields help identify which process generated the event and whether it was from an async worker. Async workers now also properly propagate audit hooks from the main process (see ‘Fixed’).

  • Audit event login_success now includes sub_source to indicate whether the subject digest came from userinfo, id_token (verified), or id_token_unverified.

  • Audit digest keying: audit/event digests (e.g., sub_digest, browser_token_digest) now default to HMAC-SHA256 with an auto-generated per-process key to reduce reidentification/correlation risk if logs leak. Configure a key with options(shinyOAuth.audit_digest_key = "..."), or disable keying (legacy deterministic SHA-256) with options(shinyOAuth.audit_digest_key = FALSE).

  • HTTP log sanitization: sensitive data in HTTP contexts (headers, cookies) is now sanitized by default in audit logs. Can be disabled with options(shinyOAuth.audit_redact_http = FALSE). Use options(shinyOAuth.audit_include_http = FALSE) to not include any HTTP data in logs.

UX

  • Configurable scope validation: validate_scopes property on OAuthClient controls whether returned scopes are validated against requested scopes ("strict", "warn", or "none"). Scopes are now normalized (alphabetically sorted) before comparison.

  • OAuthProvider: extra parameters are now blocked from overriding reserved keys essential for the OAuth 2.0/OIDC flow. Reserved keys may be explicitly overridden via options(shinyOAuth.unblock_auth_params = c(...), shinyOAuth.unblock_token_params = c(...), shinyOAuth.unblock_token_headers = c(...)). It is also validated early that all parameters are named, catching configuration errors sooner.

  • Added warning about negative expires_in values in token responses.

  • Added warning when OAuthClient is instantiated inside a Shiny session; may cause sealed state payload decryption to fail when random secret is generated upon client creation.

  • Added hints in error messages when sealed state payload decryption fails.

  • Ensured a clearer error message when token response is in unexpected format.

  • Ensured a clearer error when retrieved state store entry is in unexpected format.

  • Ensured a clearer error message when retrieved userinfo cannot be parsed as JSON.

  • Immediate error when OAuthProvider uses HS* algorithm but options(shinyOAuth.allow_hs = TRUE) is not enabled; also immediate error when OAuthProvider uses HS* algorithm and ID token verification can happen but client_secret is absent or too weak.

  • build_auth_url() now uses package-typed errors (err_invalid_state()) instead of generic stopifnot() assertions, ensuring consistent error handling and audit logging.

Other

  • ID token signature/claims validation now occurs before fetching userinfo. This ensures cryptographic validation passes before making external calls to the userinfo endpoint.

  • When fetching JWKS, if key_ops is present on keys, only keys with key_ops including "verify" are considered.

  • oauth_provider() now defaults allowed_token_types to c("Bearer") for all providers. This prevents accidentally misusing non-Bearer tokens (e.g., DPoP, MAC) as Bearer tokens. Set allowed_token_types = character() to opt out. Token type is also now validated before calling the userinfo endpoint.

  • client_assertion_audience property on OAuthClient allows overriding the JWT audience claim for client assertion authentication.

Fixed

  • Package now correctly requires httr2 >= 1.1.0.

  • authenticated now flips to FALSE promptly when a token expires or reauth_after_seconds elapses, even without other reactive changes. Previously, the value could remain TRUE past expiry until an unrelated reactive update triggered re-evaluation.

  • HTTP error responses (4xx/5xx) are now correctly returned to the caller immediately instead of being misclassified as transport errors and retried.

  • Async worker options propagation: all R options are now automatically propagated to async workers when using async = TRUE. Previously, options set in the main process (including audit_hook, trace_hook, HTTP settings, and any custom options) were not available in future::multisession workers.

  • oauth_provider_microsoft(): fixed incorrect default which blocked multi-tenant configuration.

  • oauth_provider_oidc_discover(): stricter host matching; ? and * wildcards now correctly handled.

  • Fixed potential auto-redirect loop after authentication error has surfaced.

  • Fixed potential race condition between proactive refresh and expiry watcher: the expiry watcher now defers clearing the token and triggering reauthentication while a refresh is in progress.

  • Token expiry handling during token refresh now aligns with how it is handled during login.

  • State payload issued_at validation now applies clock drift leeway (from OAuthProvider@leeway / shinyOAuth.leeway option), consistent with ID token iat check.

shinyOAuth 0.1.4

CRAN release: 2025-11-24

  • Added a console warning about needing to access Shiny apps with oauth_module_server() in a regular browser; also updated examples and vignettes to further clarify this.

  • oauth_module_server(): improved formatting style of warning messages (now consistent with error messages).

shinyOAuth 0.1.3

CRAN release: 2025-11-10

shinyOAuth 0.1.1

CRAN release: 2025-11-09

  • Initial CRAN submission.