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 authorization_request_mode = "request"
(sent as parameter) or via
authorization_request_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.
Observability and audit logging improvements:
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.remote_addr as
well as proxy headers, so default audit events no longer export raw
client IP addresses.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.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:
revoke_on_session_end = TRUE but the provider does not
expose a revocation_url, instead of crashing while
formatting that configuration error.?error=... handling until the
browser token is available and treating browser-token mismatches as
invalid_state instead of surfacing provider-controlled
error text.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.invalid_state in its callback error state for
CSRF/state/browser-token validation failures instead of flattening those
paths into token_exchange_error.error_uri values unless they are
absolute HTTPS URLs, so unsafe schemes like javascript: are
no longer surfaced through values$error_uri.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:
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.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.enforce_callback_issuer unset and the provider
advertises
authorization_response_iss_parameter_supported = TRUE.resource support, so authorization,
token exchange, and refresh requests can request audience-restricted
tokens without dropping down to manual extra params.scope_validation to "warn", so
RFC-compliant reduced grants surface as warnings unless you opt into
scope_validation = "strict".values entry without I(...), because
jsonlite::toJSON(auto_unbox = TRUE) would otherwise
serialize that OIDC array constraint as a scalar.value and
values constraints when claims_validation is
enabled, not just presence of essential = TRUE claims.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".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.sub for
introspect_elements = "sub" before falling back to
userinfo, so unvalidated ID token payloads no longer anchor the
introspection subject check.invalid_state instead of
resuming under a different worker policy.oauth_provider() (OAuthProvider)
now:
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.userinfo_id_token_match, and shinyOAuth now always binds
userinfo to a validated ID token subject when that baseline exists.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.jwks_cache$get() signatures without
calling the cache during construction, avoiding side effects in
duck-typed cache backends.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.refresh_token parameter name in
extra_token_params to prevent duplicate refresh-token
parameters during token refresh requests.oauth_provider_oidc_discover() now:
S256. shinyOAuth keeps S256 as the default and
only allows a downgrade to plain when you pass
pkce_method = "plain" explicitly.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.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.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.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:
id_token from
refresh responses.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:
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.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:
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_temporal_claims.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.
‘mirai’ & async backend improvements:
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).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.mirai::daemons_set() instead
of mirai::status(). Falls back to
mirai::info() on older ‘mirai’ versions that lack
mirai::daemons_set() (< 2.3.0).options(shinyOAuth.async_timeout) (milliseconds); timed-out
‘mirai’ tasks are automatically cancelled by the dispatcher. Default is
NULL (no timeout).mirai_error_type
field. This classifies mirai transport-level failures separately from
application-level errors.ID token validation (validate_id_token()):
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.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.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.shinyOAuth_id_token_error instead of letting a confusing
alg/typ/parse failure propagate.auth_time claim when
max_age is present in extra_auth_params (OIDC
Core section 3.1.2.1).exp - iat)
per OIDC Core section 3.1.3.7; 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).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.$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.extra_auth_params
and extra_token_params is now case-insensitive and trims
whitespace.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):
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).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.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):
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.id_token_validated property (logical)
indicating whether the ID token was cryptographically verified during
the OAuth flow.oauth_module_server():
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))..process_query(), ensuring more
consistent cleanup..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().?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).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).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.
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.
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.
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.
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.
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.
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).
Rewrote vignette("authentication-flow") to improve
clarity.
Skip timing-sensitive tests on CRAN.