shinyOAuth emits structured audit events for authorization requests,
callbacks, token operations, and changes to the authenticated session.
Configure shinyOAuth.audit_hook with an R function to
receive these events as named lists and send them to your application’s
logging system.
Put this near the top of app.R, before creating the
provider and client:
options(shinyOAuth.audit_hook = function(event) {
cat(sprintf("[shinyOAuth] %s | %s\n", event[["type"]], event[["trace_id"]]))
str(event)
})Run your app and try signing in. You will see event names such as
audit_redirect_issued and audit_login_success.
The trace_id connects related events from the same
operation or login attempt. Several events can describe one failure;
they are not necessarily separate failed logins. For an interactive
login, the same trace_id follows redirect preparation,
callback validation, token exchange, and the login result.
To stop receiving events, set
options(shinyOAuth.audit_hook = NULL). Keep hooks fast and
avoid throwing errors. A hook that waits for a slow log service can
delay the login operation that called it.
Use audit_authenticated_changed to track the module’s
final authentication state and audit_session_cleared to
record why a session ended. Events such as
audit_token_exchange and audit_userinfo report
individual operations; subsequent validation can still fail. Callback
failures include phase and error_class fields
to help identify the failed check. The event catalog below documents the
available names and fields.
Audit events have type, trace_id, and a
timestamp from Sys.time(), plus fields
specific to the operation. The hook also receives error events such as
error, http_error, and
transport_error; their fields are listed at the end of the
catalog. Do not assume every field exists on every event.
When a Shiny session is available, shiny_session
contains:
session_token_digest: a protected identifier for
correlating the session.is_async and process_id: whether work ran
in a background worker and which R process emitted the event. Async
context also includes main_process_id.http: a summary with request method, host, and scheme
by default.The session context is a JSON-friendly list suitable for
jsonlite::toJSON(); the raw Shiny
session[["request"]] object is not included. If
shinyOAuth.audit_include_raw_session_token = TRUE, the raw
session token is available as shiny_session[["token"]] in
native hook events.
Fields ending in _digest allow matching without
recording the original token or identifier. The package’s
trace_id is separate from OpenTelemetry trace/span IDs;
OTel exports it as shinyoauth.trace_id.
By default, the HTTP summary omits paths, query strings, headers, and client addresses. The raw Shiny session token is also omitted. To omit HTTP context entirely:
With this setting, shiny_session[["http"]] is
NULL.
Outbound URLs retain only scheme and authority by default. To include
safe route names in audit and OTel URLs and incoming HTTP context,
configure shinyOAuth.telemetry_path_scrubber as a function
of the path returning a safe absolute path (for example
/users/:id), or NULL to omit it. Use an
allowlist or replace all identifying path segments; the function should
be idempotent. Scrubber errors and invalid results omit the path.
Scrubber input is limited to 2048 bytes; exported paths to 512 bytes.
Method, host, and scheme are limited to 32, 255, and 16 bytes. Control
and Unicode format characters are removed.
For local debugging,
shinyOAuth.audit_redact_http = FALSE includes raw request
details, and
shinyOAuth.audit_include_raw_session_token = TRUE includes
the raw session token. These can expose credentials and personal data;
keep the defaults for production logs. Redaction does not replace your
logging system’s access and retention controls.
With oauth_module_server(async = TRUE), the hook is
passed to background work too. Use a hook that can run in a separate R
process. Existing database connections or open file handles cannot be
safely carried into a worker; create connections there or use a logging
service designed for multiple writers. Appending to a global R list
inside a worker only changes that worker’s copy.
Worker failures may include mirai_error_type:
mirai_error, mirai_timeout,
mirai_connection_reset, or mirai_interrupt.
This helps distinguish an R error from a timeout, crashed worker, or
cancellation. It can be NA for a failure that is not
specific to mirai.
Digests use HMAC-SHA256 with a random process key by default. The module shares that key with its async workers. To match digests across separate app processes or restarts, configure the same secret key everywhere:
audit_digest_key <- Sys.getenv("AUDIT_DIGEST_KEY", unset = NA_character_)
if (is.na(audit_digest_key) || !nzchar(audit_digest_key)) {
stop("AUDIT_DIGEST_KEY must be configured before the app starts")
}
options(shinyOAuth.audit_digest_key = audit_digest_key)Use a string or raw vector containing at least 32 bytes, generated
from at least 32 cryptographically random bytes and stored as a
deployment secret. Invalid or short configured keys cause an error.
FALSE selects unkeyed SHA-256 for compatibility, but makes
low-entropy identifiers easier to guess.
For exporting events and timings through OpenTelemetry, see OpenTelemetry.
This is a lookup reference. Start with the event name in your logs; fields are included when relevant and available.
audit_callback_query_rejectedprovider, issuer,
client_id_digest, error_classphase, reason, and
handle_digest.audit_callback_routing_rejectedphase
(callback_registry_routing), reason,
status (error), and sanitized HTTP metadata in
shiny_session.http.route_unavailable,
route_unregistered, unexpected_transport,
issuer_missing, and issuer_unrecognized.shinyOAuth.callback.route span records the same
phase and reason with error status. No provider identity is assigned
from unvalidated issuer input.audit_callback_iss_missingenforce_callback_issuer = TRUE and the callback
omits the RFC 9207 iss parameterprovider, expected_issuer,
client_id_digest, error_classaudit_callback_iss_mismatchiss query parameter (per
RFC 9207) that does not match the provider’s expected issuerprovider, expected_issuer,
callback_issuer, client_id_digest,
error_classaudit_callback_iss_validation_failedprovider, expected_issuer,
callback_issuer (when present),
client_id_digest, error_classaudit_callback_receivedprovider, issuer,
client_id_digest, code_digest,
state_digest, browser_token_digestCallback validation covers both the sealed-state checks and the later checks of the values tied to that state, such as the browser token, PKCE code verifier, and nonce. Each stage emits either a success event or a failure event.
audit_callback_validation_successprovider, issuer,
client_id_digest, state_digestaudit_callback_validation_failedprovider, issuer,
client_id_digest, state_digest,
phase, error_class (+
browser_token_digest when phase is
browser_token_validation)payload_validation,
browser_token_validation,
pkce_verifier_validation, nonce_validation,
form_post_request_validation,
form_post_callback_lookup,
form_post_callback_validationhandle_digest is included when a form_post callback
handle is missing, expired, or already consumed.callback_validation_failed event.These events identify failures when reading or removing a pending login from the state store.
audit_state_store_lookup_failedstate_store fails (missing, malformed, or underlying cache
error)provider, issuer,
client_id_digest, state_digest,
error_class, phase
(state_store_lookup or
state_store_atomic_take)state_store_atomic_take phase applies when using a store
with an atomic [["take"]]() method.audit_state_store_removal_failedprovider, issuer,
client_id_digest, state_digest,
error_class, phase
(state_store_removal)Digest differences: For audit_callback_validation_failed
during payload decryption (phase = "payload_validation")
the state_digest is computed from the encrypted payload
(plaintext not yet available). For state store events the digest
reflects the plaintext state string.
audit_token_exchangeprovider, issuer,
client_id_digest, code_digest,
used_pkce, received_id_token,
received_refresh_token,
expires_in_synthesizedexpires_in_synthesized (logical): TRUE
when the token response did not include a usable expires_in
value and the package used the configured fallback token lifetimeaudit_token_exchange_errorprovider, issuer,
client_id_digest, code_digest,
error_classDetailed sender-constraint diagnostics such as DPoP token-type inference, DPoP nonce retries, and mTLS endpoint-alias selection are emitted on the OpenTelemetry spans documented in the OpenTelemetry vignette rather than on the high-level audit events.
audit_token_introspectionintrospect_token() reaches a final result (for
example during login or refresh when
introspect = TRUE)provider, issuer,
client_id_digestwhich (“access” or “refresh”)supported (logical), active (logical|NA),
statussub_digest, introspected_client_id_digest,
scope_digest (when available)status values include "ok",
"introspection_unsupported", "missing_token",
"body_too_large", "invalid_json",
"missing_active", "invalid_active", and
"http_<code>"audit_login_successOAuthTokenprovider, issuer,
client_id_digest, sub_digest,
sub_source, refresh_token_present,
expires_atsub_source indicates where sub_digest was
derived from:
userinfo: subject came from the userinfo responseid_token: subject came from an ID token that was
validated (signature + claims)id_token_unverified: subject came from an ID token
payload parse when ID token validation was not performedaudit_login_failedprovider, issuer,
client_id_digest, phase,
error_class, mirai_error_typephase currently includes:
sync_token_exchangeasync_token_exchangeasync_payload_validationasync_state_store_lookupmirai_error_type is only present on async failure
pathsaudit_logoutauth[["logout"]]() is called on the moduleprovider, issuer,
client_id_digest, reason (default
manual_logout)audit_connection_disconnectedEmitted once for each retained connection removed locally, after
bounded remote cleanup. It is emitted even when revocation is disabled
or fails. Context includes owner_digest,
connection_id_digest, retention,
reason, local_outcome,
revoke_requested, remote_refresh_outcome, and
remote_access_outcome. Reasons are disconnect,
disconnect_all, logout, or
session_end. Identifiers use the configured audit digest
policy; credentials and raw identities are excluded. Remote outcomes
describe the revocation attempt, not proof that the authorization server
previously recognized a token.
audit_connections_disconnectedSummarizes a completed local bulk removal, including an empty batch,
with owner_digest, connection_count,
retention, reason, local_outcome,
and revoke_requested. Per-connection events describe remote
outcomes. This event also covers logout and removal of session-only
connections at session end; ending a Shiny session does not remove
browser- or account-retained connections. The batch and per-connection
events share a trace ID and are available to both the native audit hook
and OpenTelemetry. Existing module logout events remain.
audit_session_clearedprovider, issuer,
client_id_digest, reason,
error_class, mirai_error_typerefresh_failed_async,
refresh_failed_sync, reauth_window,
token_expirederror_class is present on refresh failure reasons
(refresh_failed_async, refresh_failed_sync)
but absent for reauth_window and
token_expired; mirai_error_type is only
present for async refresh-failure clearsaudit_token_revocationrevoke_token() reaches a final outcome (including
early unsupported or missing_token returns)
during logout or session endprovider, issuer,
client_id_digestwhich (“access” or “refresh”)supported (logical), revoked (logical|NA),
statusstatus values include "ok",
"revocation_unsupported", "missing_token", and
"http_<code>"audit_refresh_failed_but_kept_sessionindefinite_session = TRUE in
oauth_module_server())provider, issuer,
client_id_digest, reason
(refresh_failed_async|refresh_failed_sync),
kept_token (TRUE), error_class,
mirai_error_typemirai_error_type is only present on async refresh
failuresaudit_invalid_browser_tokenshinyOAuth_sid
value from the browser and requests regenerationprovider, issuer,
client_id_digest, reason,
lengthaudit_token_refreshrefresh_token() successfully refreshes the access
tokenprovider, issuer,
client_id_digest, refresh_token_rotated,
new_expires_at, expires_in_synthesizedexpires_in_synthesized (logical): TRUE
when the refresh response did not include a usable
expires_in value and the package used the configured
fallback token lifetimeaudit_userinfoget_userinfo() is called to retrieve user
information (emitted on success and various failure modes)provider, issuer,
client_id_digest, sub_digest,
statusstatus values:
"ok" – userinfo successfully parsed"parse_error" – response could not be parsed as JSON or
JWT. Additional fields: http_status, url,
content_type, body_digest"userinfo_missing_sub" – OIDC userinfo response was
parsed but omitted the required sub claim"userinfo_not_jwt" – signed JWT required but response
was not application/jwt. Additional fields:
content_type"userinfo_jwt_encrypted" – userinfo response was a JWE,
which ‘shinyOAuth’ does not decrypt"userinfo_jwt_header_parse_failed" – JWT header could
not be parsed"userinfo_jwt_header_invalid" – JWT header parsed but
was malformed or structurally invalid"userinfo_jwt_typ_invalid" – JWT header
typ did not indicate a JWT"userinfo_jwt_unsigned" – JWT uses
alg=none. Additional fields: jwt_alg"userinfo_jwt_alg_rejected" – JWT algorithm not in
provider’s allowed asymmetric algorithms. Additional fields:
jwt_alg"userinfo_jwt_no_issuer" – provider issuer not
configured for JWKS verification"userinfo_jwt_jwks_fetch_failed" – JWKS fetch failed
during signature verification"userinfo_jwt_signature_invalid" – signature
verification failed against candidate JWKS keys"userinfo_jwt_no_matching_key" – provider JWKS had no
compatible key for the JWT"userinfo_jwt_payload_parse_failed" – JWT payload could
not be parsed as JSON"userinfo_jwt_missing_sub",
"userinfo_jwt_missing_iss",
"userinfo_jwt_missing_aud" – signed JWT omitted a required
claim"userinfo_jwt_iss_mismatch",
"userinfo_jwt_aud_mismatch" – signed JWT claims did not
match the configured issuer/client"userinfo_jwt_missing_required_temporal_claims" –
signed JWT omitted required temporal claims such as exp or
iat"userinfo_jwt_invalid_exp",
"userinfo_jwt_invalid_iat",
"userinfo_jwt_invalid_nbf" – temporal claim was present but
not a single usable numeric value"userinfo_jwt_expired",
"userinfo_jwt_iat_future",
"userinfo_jwt_nbf_future" – temporal claim failed time
validationState parsing failures occur while decoding and validating the encrypted wrapper prior to extracting the logical state value, and also when deriving a cache key from a malformed logical state string.
audit_state_parse_failurephase (decrypt or
cache_key), a reason code, and either
token_digest (phase = decrypt) or
state_digest (phase = cache_key), plus any
additional details (such as lengths). Emitted best-effort from parsing
utilities and never interferes with control flow.Provider error callbacks still need valid login state and browser
binding. The events below report the one-time state consumption, not the
outcome of all callback checks. Browser mismatches use
audit_callback_validation_failed with
phase = "browser_token_validation".
audit_error_state_consumedprovider, issuer,
client_id_digest, state_digestaudit_error_state_consumption_failedprovider, issuer,
client_id_digest, state_digest,
error_class, error_messageDigest note: when the callback state can be decrypted,
these events use the logical plaintext state digest so they correlate
with audit_redirect_issued and the normal callback
validation/store events. If decryption fails, the digest falls back to
the encrypted callback payload because the logical state is unknown.
audit_session_startedoauth_module_server())
is initialized for a Shiny sessionmodule_id, ns_prefix,
client_provider, client_issuer,
client_id_digest, plus the standard
shiny_session context described aboveaudit_session_endedonSessionEnded, regardless of configuration)provider, issuer,
client_id_digest, was_authenticatedaudit_session_ended_revokerevoke_on_session_end = TRUE and a token was presentprovider, issuer,
client_id_digest; the actual revocation attempt is logged
separately as audit_token_revocation eventsaudit_authenticated_changed[["authenticated"]] reactive value changes
(TRUE ↔︎ FALSE)provider, issuer,
client_id_digest, authenticated,
previous_authenticated, reasonlogin (when becoming authenticated),
or the error code/state that caused de-authentication (e.g.,
token_expired, logged_out,
token_cleared)In addition to the audit_* events above, the hook also
receives error events emitted just before the package raises an R error
condition. These let you log failures to the same sink as audit
events.
errortype ("error"), trace_id,
message,context fields from the call site (typically
provider, issuer,
client_id_digest, phase,
error_class)http_errortype ("http_error"),
trace_id, messagestatus: HTTP status code (integer, or NA
if unavailable)url: the request scheme and authority, plus an approved
path only when a path scrubber is configuredbody_digest: HMAC-SHA-256 hex digest of the response
body using the configured or per-process audit digest key (for
correlation without leaking content)oauth_error, oauth_error_uri: RFC 6749
§5.2 structured error fields extracted from JSON error responses (e.g.,
from the token endpoint)oauth_error_description: included only when
options(shinyOAuth.expose_error_body = TRUE) is enabled for
debugging, because provider-controlled text can contain request-specific
detailscontext fields from the call sitetransport_errortype ("transport_error"),
trace_id, messagecontext fields from the call site