‘shinyOAuth’ emits structured audit events at key steps in the OAuth 2.0/OIDC flow. These may help detect anomalous activity (e.g., brute force, replay, or configuration errors).
This vignette covers: - How to register audit hooks to export/store events - Which audit events are emitted & what fields are included in each event - Best practices
There are two hook options you can set. Both receive the same event object (a named list). The functions you should register under these options should be fast, non-blocking, and never throw errors.
options(shinyOAuth.audit_hook = function(event) { ... })
- intended for audit-specific sinksoptions(shinyOAuth.trace_hook = function(event) { ... })
- a more general-purpose tracing hook used for both audit events and
error tracesExample of printing audit events to console:
options(shinyOAuth.audit_hook = function(event) {
cat(sprintf("[AUDIT] %s %s\n", event$type, event$trace_id))
str(event)
})To stop receiving events, unset the option:
All audit events share the following base shape:
type: a string starting with
audit_...trace_id: a short correlation id for linking related
recordstimestamp: POSIXct time when the event was created
(from Sys.time())When events are emitted from within a Shiny session, a JSON-friendly
shiny_session list is attached to every event to correlate
audit activity with the HTTP request and session. The structure is
designed to be directly serializable with
jsonlite::toJSON():
shiny_session$token: the Shiny per-session token
(session$token) when available.shiny_session$http: a compact HTTP summary with fields:
method, path, query_string,
host, scheme, remote_addrheaders: a list of request headers derived from
HTTP_* environment variables, with lowercase names (e.g.,
user_agent, x_forwarded_for).Note: the raw session$request from Shiny is not included
to keep the event JSON-serializable and concise.
Note: the shiny_session$http summary intentionally
captures all HTTP_* headers and the raw
QUERY_STRING from the active Shiny request. If you forward
events to a log sink, this may include sensitive material such as the
authorization code, state, and every request header
(including Cookie, Authorization, and other
bearer tokens). Consider stripping or redacting sensitive headers and
query parameters in your hook before exporting.
audit_callback_receivedhandle_callback() begins processing a
callbackprovider, issuer,
client_id_digest, code_digest,
state_digest, browser_token_digestCallback validation spans decryption + freshness + binding of the encrypted payload as well as subsequent checks of values bound to the state (browser token, PKCE code verifier, nonce). Each check emits either a success (only once for the payload) or a failure audit event.
audit_callback_validation_successstate payload has been decrypted
and verified for freshness and client/provider binding (emitted from
state_payload_decrypt_validate())provider, 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_validationcallback_validation_failed event.State retrieval and removal of the single-use state entry are emitted
as separate events by state_store_get_remove().
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)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_tokenaudit_token_exchange_errorprovider, issuer,
client_id_digest, code_digest,
error_classaudit_login_successOAuthToken is
createdprovider, issuer,
client_id_digest, sub_digest,
refresh_token_present, expires_ataudit_login_failedprovider, issuer,
client_id_digest, phase
(sync_token_exchange|async_token_exchange),
error_classaudit_logoutvalues$logout() is called on the moduleprovider, issuer,
client_id_digest, reason (default
manual_logout)audit_session_clearedprovider, issuer,
client_id_digest, reasonrefresh_failed_async,
refresh_failed_sync, reauth_window,
token_expiredaudit_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_classaudit_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, had_refresh_token,
new_expires_ataudit_userinfoget_userinfo() successfully retrieves user
informationprovider, issuer,
client_id_digest, sub_digestState parsing failures occur while decoding and validating the encrypted wrapper prior to extracting the logical state value.
audit_state_parse_failurephase = decrypt, a
reason code (e.g., token_b64_invalid,
iv_missing, tag_len_invalid),
token_digest, and any additional details (such as lengths).
Emitted best-effort from parsing utilities and never interferes with
control flow.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 aboveR/methods__login.RR/oauth_module_server.Raudit_event() defined in
R/errors.R, which delegates to the hook optionstry(..., silent = TRUE) if neededoptions(shinyOAuth.trace_hook=...)Example of a JSON export hook: