Changelog
Source:NEWS.md
shinyOAuth (development version)
Added JWT Secured Authorization Response Mode (JARM) support with
response_mode = "jwt","query.jwt", and"form_post.jwt". JARM callbacks currently resume throughoauth_module_server()only;handle_callback()still accepts only the classic directcode+ sealedstatecallback shape. JARM clients can tune the maximum accepted authorization-response JWT lifetime withjarm_max_lifetime(default 600 seconds).oauth_module_server()now warns once when a client resolves toresponse_mode = "form_post"or"form_post.jwt"but no prioroauth_form_post_ui()call was detected for that module/client callback setup, helping catch missing form_post UI wrappers earlier.-
oauth_client()/OAuthClientandoauth_provider()/OAuthProviderhave had their arguments reorganized and renamed for better clarity. Both helper constructors (oauth_client()andoauth_provider()) still resolve previous argument names through compatibility aliases, but the underlying S7 classes only use the new names. Renamed arguments include:-
oauth_client():-
client_private_key->client_assertion_private_key -
client_private_key_kid->client_assertion_private_key_kid -
userinfo_jwt_required_temporal_claims->userinfo_jwt_required_time_claims -
mtls_request_certificate_bound_access_tokens->mtls_certificate_bound_access_tokens -
tls_client_cert_file->mtls_client_cert_file -
tls_client_key_file->mtls_client_key_file -
tls_client_key_password->mtls_client_key_password -
tls_client_ca_file->mtls_client_ca_file -
authorization_request_mode->request_object_mode -
authorization_request_signing_alg->request_object_signing_alg -
authorization_request_audience->request_object_audience -
authorization_request_encryption_alg->request_object_encryption_alg -
authorization_request_encryption_enc->request_object_encryption_enc -
authorization_request_encryption_kid->request_object_encryption_kid -
authorization_request_ttl->request_object_ttl -
authorization_request_nbf_skew->request_object_nbf_skew -
authorization_signed_response_alg->jarm_signed_response_alg -
authorization_encrypted_response_alg->jarm_encrypted_response_alg -
authorization_encrypted_response_enc->jarm_encrypted_response_enc -
authorization_response_decryption_private_key->jarm_decryption_private_key -
authorization_response_decryption_private_key_kid->jarm_decryption_private_key_kid
-
-
oauth_provider()-
require_pushed_authorization_requests->par_required -
require_signed_request_object->signed_request_object_required -
require_request_uri_registration->request_uri_registration_required -
authorization_signing_alg_values_supported->jarm_signing_alg_values_supported -
authorization_encryption_alg_values_supported->jarm_encryption_alg_values_supported -
authorization_encryption_enc_values_supported->jarm_encryption_enc_values_supported -
tolerate_duplicate_top_level_jarm_iss->jarm_tolerate_duplicate_top_level_iss -
tls_client_certificate_bound_access_tokens->mtls_client_certificate_bound_access_tokens
-
-
-
oauth_client()(OAuthClient) now:- Supports
dpop_require_observed_cnf = TRUEfor high-assurance DPoP deployments. When enabled, shinyOAuth rejectstoken_type = "DPoP"access tokens unless it can observecnf.jktlocally in the token or via introspection, so opaque tokens no longer rely ontoken_typealone. - No longer defaults
client_id/client_secretfromSys.getenv('OAUTH_CLIENT_ID')/Sys.getenv('OAUTH_CLIENT_SECRET'), to make it more explicit that these values must be set for the client to work. - Has printing which now handles
client_secret = ""cleanly for public-client setups that do not send a secret, instead of failing while formatting the redacted console preview. - Treats an omitted
client_secretas an absent value (character(0)), soprivate_key_jwtand other secretless client-auth setups can omit the argument and still flow through the normal auth-style validation instead of failing at argument matching.
- Supports
oauth_provider_oidc_discover()now preserves JARM discovery metadata from the canonicaljarm_*_values_supportedfields, while still accepting the olderauthorization_*compatibility aliases.-
oauth_provider_oidc_discover()now:- Accepts either an issuer base URL or the standard
/.well-known/openid-configurationURL. Full discovery URLs are normalized back to the issuer base before request construction, so strict issuer matching still applies without requiringissuer_match = "host". - Has transport failures surface the attempted
/.well-known/openid-configurationURL plus the underlying network error, making discovery misconfiguration and connectivity problems easier to diagnose. - Accepts discovery metadata that differs from the configured issuer only by one trailing slash in the published
issuer, while still storing the provider’s advertised issuer verbatim for downstreamisschecks.
- Accepts either an issuer base URL or the standard
-
oauth_provider()/OAuthProviderand runtime JWKS resolution now:- Accept an explicit
jwks_urioverride for providers that publish signing keys outside OIDC discovery or that need a pinned runtime JWKS location. - Preserve discovered
jwks_urivalues on discovery-backed providers so ID-token, JARM, Request Object, and signed UserInfo verification all reuse the same resolved JWKS source. - Fall back to RFC 8414 authorization-server metadata when runtime JWKS fetches need to resolve signing keys for generic OAuth 2.0 / JARM issuers, instead of relying only on OpenID Connect discovery.
- Fail earlier when discovery enables signature-validation modes but omits
jwks_uri, or when the discovered JWKS URL is malformed or violates the configured host policy.
- Accept an explicit
oauth_provider_apple()has been added which configures Apple’s OIDC endpoints and ID-token defaults, and addedoauth_client_secret_apple()which generates the ES256 JWT that Apple expects in theclient_secretfield of anOAuthClientconfigured withoauth_provider_apple().oauth_provider_okta()can now target Okta’s org authorization server withauth_server = NULL, instead of always forcing/oauth2/{auth_server}and the custom-server path.oauth_provider_keycloak()now defaultsjarm_tolerate_duplicate_top_level_iss = TRUEfor interoperability with current Keycloak JARM responses, while still letting callers opt out and fail closed on duplicate top-levelissmembers.Provider callback
error_urivalues now have to stay on a provider host or another host you already allowlist viaoptions(shinyOAuth.allowed_hosts = ...). Unrelated HTTPS hosts are now dropped instead of being surfaced throughvalues$error_uri. Trustederror_urivalues are also preserved across deferred OAuth error callbacks that wait for the browser token before resuming.Native audit hooks now receive
shiny_session$session_token_digestby default instead of the raw Shinysession$token. Setoptions(shinyOAuth.audit_include_raw_session_token = TRUE)only when you explicitly need the raw token in a controlled sink.Added
vignette("advanced-security", package = "shinyOAuth"), which collects higher-assurance configuration guidance for mTLS, JAR, PAR,form_post, JARM, and DPoP setups.Internal list-like access now consistently uses exact
[[...]]indexing instead of$, reducing potential for accidental partial matches across runtime code, tests, and integration fixtures.
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_keynow send DPoP proofs on token and protected-resource requests, includedpop_jktin authorization requests, can requiretoken_type = "DPoP"pluscnf.jktbinding, and replay oneDPoP-Noncechallenge 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 viarequest_object_mode = "request"(sent as parameter) or viarequest_object_mode = "request_uri"(served from your Shiny app).Added Pushed Authorization Request (‘PAR’, RFC 9126) support. Providers can now configure
par_urldirectly or pick it up from OIDC discovery, and login flows will push the authorization request and redirect with the returnedrequest_uri.Added
response_mode = "form_post"support for authorization-code callbacks. Apps can wrap their UI withoauth_form_post_ui()so Shiny accepts the provider POST, stores the callback server-side under a one-time handle, and letsoauth_module_server()finish the existing state, issuer, and token exchange flow. Stored callback handles are bounded by the effective state/store TTL and theshinyOAuth.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.-
Observability and audit logging improvements:
- Improved observability correlation for existing audit flows. Interactive login now reuses a single flow
trace_idacross 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 theOAuthToken@id_token_validatedresult 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_addras well as proxy headers, so default audit events no longer export raw client IP addresses. - Improved existing audit event types.
audit_token_exchangeandaudit_token_refreshnow includeexpires_in_synthesized, indicating that the provider did not return a usableexpires_inand shinyOAuth had to synthesize one;audit_login_failednow distinguishes async payload-validation and state-store-lookup failures from async token-exchange failures;audit_userinfodistinguishes missingsuband JWT/JWKS validation failures; and error-state consumption events use the logical state digest when available for better correlation. Seevignette("audit-logging", package = "shinyOAuth")for more information. -
options(shinyOAuth.trace_hook = ...)is no longer treated as a separate documented event sink. Preferoptions(shinyOAuth.audit_hook = ...); the oldtrace_hookoption now remains only as a backward-compatible alias whenaudit_hookis unset. - Removed the documented global
shinyOAuth.print_errors/shinyOAuth.print_tracebackoptions. Internal console error logging now uses explicit internal flags instead of package-wide option fallbacks. -
http_erroraudit events now omit raw provideroauth_error_descriptiontext by default and keep onlyoauth_error,oauth_error_uri, andbody_digest. The raw description is emitted only whenoptions(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.
- Improved observability correlation for existing audit flows. Interactive login now reuses a single flow
-
- Explicitly ignores new login requests while a session is already authenticated.
- Fails cleanly at startup when
revoke_on_session_end = TRUEbut the provider does not expose arevocation_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 asinvalid_stateinstead 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 directrefresh_token(..., introspect = TRUE)calls. - Preserves
invalid_statein its callback error state for CSRF/state/browser-token validation failures instead of flattening those paths intotoken_exchange_error. - Drops provider
error_urivalues unless they are absolute HTTPS URLs, so unsafe schemes likejavascript:are no longer surfaced throughvalues$error_uri. - Validates
browser_cookie_pathmore strictly, requiring a leading/and rejecting semicolons or control characters so unsafe cookie attributes cannot be injected through the configured cookie Path.
OAuthTokenandOAuthClientnow print with redacted token/secret/key previews instead of exposing full credential material in default console output.OAuthTokennow tracks normalizedgranted_scopesplusgranted_scopes_verified, so apps can distinguish between scope sets that were explicitly returned and ones that were carried forward when the provider omittedscope. Refresh now preserves prior granted scopes instead of widening back to the client’s configured scopes when a refresh response omitsscope.-
oauth_client()(OAuthClient) now:- Accepts custom state-store entries that omit unused
pkce_code_verifierandnoncefields when the provider does not require them, while still rejecting missing PKCE or nonce values when those checks are enabled. - Supports
enforce_callback_issuer = TRUEto require the RFC 9207isscallback parameter for shared-redirect multi-issuer deployments. Relatedly,handle_callback()now acceptsiss, so advanced callers building aroundprepare_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_issuerunset and the provider advertisesauthorization_response_iss_parameter_supported = TRUE. - Has native RFC 8707
resourcesupport, so authorization, token exchange, and refresh requests can request audience-restricted tokens without dropping down to manual extra params. - Defaults
scope_validationto"warn", so RFC-compliant reduced grants surface as warnings unless you opt intoscope_validation = "strict". - Warns when list-based OIDC claims requests use a single-element
valuesentry withoutI(...), becausejsonlite::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
valueandvaluesconstraints whenclaims_validationis enabled, not just presence ofessential = TRUEclaims. - Defaults enforceable OIDC claim requests to
claims_validation = "warn"when callers requestessentialor value-constrained claims and do not setclaims_validationexplicitly, so claim mismatches are surfaced by default unless callers opt out withclaims_validation = "none". - Carries the same effective requested scopes through the whole login flow. If
openidis 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
subforintrospect_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_stateinstead of resuming under a different worker policy.
- Accepts custom state-store entries that omit unused
-
oauth_provider()(OAuthProvider) now:- Allows
response_mode = "query"andresponse_mode = "form_post"for authorization-code callbacks, while still rejecting unsupported modes such as"fragment". When provider metadata advertisesresponse_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_errorconditions 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_algsagainst 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_tokenparameter name inextra_token_paramsto prevent duplicate refresh-token parameters during token refresh requests.
- Allows
-
oauth_provider_oidc_discover()now:- Fails fast when metadata advertises PKCE methods but omits
S256. shinyOAuth keepsS256as the default and only allows a downgrade toplainwhen you passpkce_method = "plain"explicitly. - Fails fast when the discovery document omits
jwks_uribut the selected policy still needs signing keys, includingid_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_urivalues 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_onlyduring its earlyjwks_urihost 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 sendsclient_secret, even whenoauth_client()picks one up fromOAUTH_CLIENT_SECRET.
- Fails fast when metadata advertises PKCE methods but omits
oauth_provider_oidc()now trims trailing slashes frombase_urlbefore deriving endpoint URLs and the configuredissuer, avoiding valid ID tokens being rejected on a strict OIDCisscomparison when the helper was configured with a URL likehttps://issuer.example/.oauth_provider_microsoft()no longer drops the Microsoft alias tenants to OAuth 2.0 plus userinfo identity by default.commonandorganizationsnow validate ID tokens using Microsoft’s tenant-independent issuer and signing-key issuer rules, andconsumersnow validates against the stable consumer tenant issuer.validate_id_token()now properly rejectsauth_timeclaims set in the future (beyond leeway). Previously, a futureauth_timeproduced a negative elapsed value that always passed themax_agefreshness check.introspect_token()now usesprovider@userinfo_id_selectorconsistently 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_tokenfrom 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 requiredintrospect_elementssuch assub,client_id, orscope.
- 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
-
get_userinfo()now:- Applies the same hard JWK
algcompatibility 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
subclaim in userinfo responses from OIDC providers (those with anissuerconfigured), per OIDC Core section 5.3. Previously, a non-compliant response withoutsubcould be accepted ifuserinfo_id_token_matchwas not enabled. The signed-JWT path (validate_signed_userinfo_claims()) also now checkssubalongside the existingiss/audvalidation.
- Applies the same hard JWK
-
Signed UserInfo JWT validation now:
- Enforces
exp,iat, andnbfwhen 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 viauserinfo_jwt_required_time_claims. - Uses internal JWS verifier instead of
jose::jwt_decode_sig(), so EdDSA UserInfo JWTs can verify correctly, providerleewayis honored consistently, and invalidtypheaders are rejected.
- Enforces
Successful token and refresh responses now always require
token_type, even whenallowed_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-timenonce, andazpin addition to the existingiss/sub/audchecks.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_granterrors or triggering refresh-token replay detection.Hardened runtime JWKS discovery by validating the discovery issuer before trusting
jwks_uri. This policy is now stored onOAuthProviderviaissuer_match, so both provider discovery and runtime JWKS fetches apply the same rule:urlfor exact issuer URL matching,hostfor scheme-and-host matching, ornoneto 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_hostssettings, and cache hits re-check the storedjwks_uribefore a JWKS is trusted.Scope validation now treats an omitted
scopein 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 separatereadandwritescopes.Missing
expires_invalues now default to a finite 3600-second fallback rather than an effectively indefinite session. Override this withoptions(shinyOAuth.default_expires_in = <seconds>), and useoauth_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_hashvalidation now resolvesEdDSAfrom the verified signing key/JWK instead of guessing fromalgalone:Ed25519uses the exact SHA-512 mapping, while signature-skipped or currently unsupportedEdDSAcurves 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()andperform_resource_req(). The existing publicclient_bearer_req()name remains available as a deprecated alias, andperform_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 oneuse_dpop_noncechallenge 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_infrom token response) are now captured and re-emitted on the main process so they appear in the R console. This includes conditions from user-suppliedtrace_hook/audit_hookfunctions: warnings, messages, and errors (surfaced as warnings) all propagate back to the main thread. Replay can be disabled viaoptions(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. Thestate_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 ofmirai::status(). Falls back tomirai::info()on older ‘mirai’ versions that lackmirai::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 isNULL(no timeout). - Async audit events now include a
mirai_error_typefield. This classifies mirai transport-level failures separately from application-level errors. - Prevent ‘mirai’ warning spam about ‘stats’ maybe not being available in workers.
- Warnings and messages emitted in async workers (e.g., missing
-
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 ashinyOAuth_id_token_error. The current implementation supports no critical extensions, so anycritpresence 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 ashinyOAuth_id_token_error. Newid_token_at_hash_requiredproperty onOAuthProvider(defaultFALSE) forces login to fail when the ID token does not contain anat_hashclaim. - Now validates, for refreshed ID tokens, per OIDC Core section 12.2,
issandaudclaims 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_errorinstead of letting a confusing alg/typ/parse failure propagate. - Now validates the
auth_timeclaim whenmax_ageis present inextra_auth_params(OIDC Core section 3.1.2.1). - Now enforces a maximum ID token lifetime (
exp - iat) per OIDC Core section 3.1.3.7; tokens with unreasonably long lifetimes are rejected with ashinyOAuth_id_token_error. Configure viaoptions(shinyOAuth.max_id_token_lifetime = <seconds>)(default of86400which is 24 hours). Set toInfto disable the check.
- Now enforces RFC 7515 section 4.1.11 critical header parameter (
-
Stricter state store usage:
-
custom_cache()gains an optionaltakeparameter 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 settingoptions(shinyOAuth.allow_non_atomic_state_store = TRUE), which downgrades the error to a warning. -
OAuthClientvalidator 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:
OAuthClientnow rejects redirect URIs containing fragments (per RFC 6749, section 3.1.2);OAuthProvidernow rejects issuer identifiers containing query or fragment components, covering bothoauth_provider_oidc_discover()and manual construction of providers.Stricter state payload parsing: callback
statenow 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-Lengthexceeds the limit; a post-download guard catches chunked responses. Default 1 MiB, configurable viaoptions(shinyOAuth.max_body_bytes).-
OAuthProvider(S7 class):-
leewayvalidator 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_paramsandextra_token_paramsis now case-insensitive and trims whitespace. - Vector inputs for
pkce_methodand 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_validationproperty; when the client sends a structuredclaimsrequest parameter withessential = TRUEentries, this setting controls whether the returned ID token and/or userinfo response are checked for those essential claims (similar toscope_validation). - Gains a
required_acr_valuesproperty; enables client-side enforcement of the OIDCacr(Authentication Context Class Reference) claim. -
extra_token_headersare 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_algandclient_assertion_audiencevalues (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""forclient_assertion_audienceis now explicitly rejected instead of being silently treated as “not provided”.
- Gains a
-
OAuthToken(S7 class):- Gains a read-only
id_token_claimsproperty 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_validatedproperty (logical) indicating whether the ID token was cryptographically verified during the OAuth flow.
- Gains a read-only
-
- Now surfaces
error_urifrom provider error callbacks (RFC 6749, section 4.1.2.1). The new$error_urireactive field contains the URI to a human-readable error page when the provider includes one;NULLotherwise. Theerror_uricallback 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. Thevalidate_untrusted_query_string()check now runs unconditionally at the top of.process_query(). - OAuth callback error responses (
?error=...) now require a validstateparameter. Missing/invalid/consumed state is then treated properly as aninvalid_stateerror instead of surfacing the error from?error=...(which could be set by an attacker). - OAuth callback including an
issquery parameter now validate this against the provider’s configured/discovered issuer during callback processing (complementing the existing ID tokenissclaim validation that occurs post-exchange) (per RFC 9207). A mismatch produces anissuer_mismatcherror and audit event, defending against authorization-server mix-up attacks in multi-provider scenarios. Whenissis absent, current behavior is retained (no enforcement).
- Now surfaces
handle_callback(): no longer acceptsdecrypted_payloadandstate_store_valuesbypass parameters. These parameters were only intended for internal use byoauth_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 omitsexpires_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 viaoptions(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 returnsContent-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’sallowed_algs,alg=noneis 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. Newuserinfo_signed_jwt_requiredproperty onOAuthProvider(defaultFALSE) mandates that the userinfo endpoint returnsapplication/jwtcontent-type which is then subject to the above verification.client_bearer_req()now validates the target URL againstis_ok_host()before attaching the Bearer token. Relative URLs, plain HTTP to non-loopback hosts, and hosts outsideoptions(shinyOAuth.allowed_hosts)are rejected by default. A newcheck_urlargument (defaultTRUE) 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 (asoauth_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
claimsparameter support (OIDC Core, section 5.5):OAuthClientandoauth_client()now accept aclaimsargument 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
openidscope enforcement: when a provider has anissuerset (indicating OIDC) andopenidis 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) overnonewhen both are advertised intoken_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-tokengrammar (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-informativehad_refresh_tokenfield (alwaysTRUEpost-mutation) withrefresh_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 withfuture::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 = TRUEinoauth_module_server(). The provider must expose arevocation_url(auto-discovered for OIDC, or set manually viaoauth_provider()). New exported functionrevoke_token().Token introspection on login: validate tokens via the provider’s introspection endpoint during login. Configure via
introspectandintrospect_elementsproperties onOAuthClient. The provider must expose anintrospection_url(auto-discovered for OIDC, or set manually viaoauth_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’ invignette("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_pinswithjwks_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 withuse_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 gainsfollow_redirect, which defaults toFALSE, 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.
OAuthProvidernow requires absolute URLs (scheme + hostname) for all endpoint URLs.Provider fingerprint now includes
userinfo_urlandintrospection_url, reducing risk of misconfiguration when multiple providers share endpoints.state_payload_max_ageproperty onOAuthClientfor independent freshness validation of the state payload’sissued_attimestamp.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(whenintrospect_token()is used),token_revocation(whenrevoke_token()is used),error_state_consumedanderror_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_successnow includessub_sourceto indicate whether the subject digest came fromuserinfo,id_token(verified), orid_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 withoptions(shinyOAuth.audit_digest_key = "..."), or disable keying (legacy deterministic SHA-256) withoptions(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). Useoptions(shinyOAuth.audit_include_http = FALSE)to not include any HTTP data in logs.
UX
Configurable scope validation:
validate_scopesproperty onOAuthClientcontrols 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 viaoptions(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_invalues in token responses.Added warning when
OAuthClientis 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
OAuthProviderusesHS*algorithm butoptions(shinyOAuth.allow_hs = TRUE)is not enabled; also immediate error whenOAuthProviderusesHS*algorithm and ID token verification can happen butclient_secretis absent or too weak.build_auth_url()now uses package-typed errors (err_invalid_state()) instead of genericstopifnot()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_opsis present on keys, only keys withkey_opsincluding"verify"are considered.oauth_provider()now defaultsallowed_token_typestoc("Bearer")for all providers. This prevents accidentally misusing non-Bearer tokens (e.g., DPoP, MAC) as Bearer tokens. Setallowed_token_types = character()to opt out. Token type is also now validated before calling the userinfo endpoint.client_assertion_audienceproperty onOAuthClientallows overriding the JWT audience claim for client assertion authentication.
Fixed
Package now correctly requires
httr2>= 1.1.0.authenticatednow flips toFALSEpromptly when a token expires orreauth_after_secondselapses, even without other reactive changes. Previously, the value could remainTRUEpast 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 (includingaudit_hook,trace_hook, HTTP settings, and any custom options) were not available infuture::multisessionworkers.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_atvalidation now applies clock drift leeway (fromOAuthProvider@leeway/shinyOAuth.leewayoption), consistent with ID tokeniatcheck.
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
Rewrote
vignette("authentication-flow")to improve clarity.Skip timing-sensitive tests on CRAN.