# shinyOAuth 0.5.0

* 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:
  - 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_temporal_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

* '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 maximum ID token lifetime (`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).
  - 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

* 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

## 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

* 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

* Rewrote `vignette("authentication-flow")` to improve clarity.

* Skip timing-sensitive tests on CRAN.

# shinyOAuth 0.1.1

* Initial CRAN submission.
