Overview
shinyOAuth exports diagnostic logs and traces through OpenTelemetry
(OTel), using the otel R package. Logs record events such
as a failed token exchange. Traces group related operations into spans,
which record their duration and outcome. This allows authentication
diagnostics to be collected alongside other application telemetry.
An exporter sends the records to a console, file, or monitoring
service. The otel package is a dependency of shinyOAuth;
install otelsdk for its exporters. For R callbacks that
receive events directly, see Audit logging
and hooks.
Exporter configuration
Install otelsdk, then set these variables in a fresh R
session before loading shinyOAuth or starting the app:
# install.packages("otelsdk")
Sys.setenv(
OTEL_TRACES_EXPORTER = "console",
OTEL_LOGS_EXPORTER = "console",
OTEL_LOG_LEVEL = "debug"
)
library(shinyOAuth)Authenticate with the example Shiny app. The console exporter prints the emitted records. To send them to a monitoring service instead, follow the otelsdk exporter setup and your service’s endpoint and credential instructions.
If nothing appears, check that an exporter is configured before the package first creates a logger or tracer, and that the logging and tracing options below are enabled. Restart R after changing exporter configuration.
Traces and event correlation
Filter by instrumentation scope
io.github.lukakoning.shinyOAuth in your monitoring system.
Start with shinyOAuth.login.request and
shinyOAuth.callback, then inspect token exchange or
userinfo spans to see where time was spent. The package’s
shinyoauth.trace_id attribute also connects related logs
and spans; it is separate from OTel’s own trace/span IDs.
The logs come from the same events as the audit hook. See the audit event catalog for their meaning. The span catalog below lists names and attributes for detailed lookup.
Logging and tracing options
Both options default to TRUE; an exporter is still
needed to collect the data. Disable either signal without changing your
app’s other telemetry:
options(
shinyOAuth.otel_logging_enabled = FALSE,
shinyOAuth.otel_tracing_enabled = FALSE
)Asynchronous workers
For async work managed by shinyOAuth, exporter environment settings
(OTEL_* and OTEL_R_*), logging and tracing
options, and trace context are propagated to workers. SDK setup
performed through R code is not replayed automatically; run that setup
in each worker or recreate workers after changing it.
Error attributes and redaction
- Successful operations have status
ok; failures have statuserror. Revocation and introspection classify their normalized results on both sync and async spans: HTTP/protocol failures carryerror.type, while missing tokens and unsupported endpoints leave status unset. An inactive-token response is successful introspection (oauth.active = FALSE). Thrown errors also produce anexceptionevent containing the error class. Condition messages are omitted by default and are included only withoptions(shinyOAuth.expose_error_body = TRUE); those messages may contain provider details and should be handled as sensitive data. - Log bodies default to the stable event type. The same exposure
option controls free-form native audit detail, module diagnostics, and
sink failure warnings, including classified SDK diagnostic messages
emitted during package span/log calls. SDK calls made directly by
application code are outside these wrappers. URLs retain scheme and
authority by default; paths require an explicit
shinyOAuth.telemetry_path_scrubberreturning approved route templates. Opt-in detail has URL userinfo, query strings, fragments, and control characters removed and is limited to 512 UTF-8 bytes per field. - Tokens, authorization codes, state payloads, and browser tokens are not included as ordinary span attributes. Digest fields support correlation without the raw value. Keep debugging options off for production exports.
- Top-level package spans often start as roots so that they remain easy to find alongside Shiny’s reactive-update spans.
Authorization metadata privacy
Scope names, claim target names, and ACR values are omitted by
default. The corresponding counts remain available, including
oauth.claims.targets_count and
oauth.scopes.granted_count. To include names in a
controlled environment:
options(shinyOAuth.otel_include_authorization_details = TRUE)This option enables oauth.scopes.requested,
oauth.scopes.granted, oauth.claims.targets,
and oauth.required_acr_values in the catalog below. It is
propagated to asynchronous workers. These names can reveal tenants,
privileges, sensitive data categories, or authentication strength, and
can create high-cardinality telemetry. Restrict exporter and dashboard
access, set a short retention period appropriate to the diagnostic need,
and avoid indexing these fields unless needed. Keep
shinyOAuth.expose_error_body disabled in production; the
authorization-details option does not enable it.
Span catalog
Use this as a reference for a span you see in your tracing system. Attributes are included when relevant and available; their presence can differ between main-process and worker spans.
Span: shinyOAuth.module.init
- When: when
oauth_module_server()initializes for a Shiny session - Represents: module startup and the initial
session_startedaudit emission - Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestshiny.module_idoauth.phase = "module.init"-
oauth.auto_redirect,oauth.refresh_proactively,oauth.revoke_on_session_end,oauth.indefinite_session -
oauth.reauth_after_seconds,oauth.refresh_lead_seconds -
oauth.browser_cookie_samesite,oauth.browser_cookie_path_root - Shiny session/process metadata when available
-
Span: shinyOAuth.login.request
- When: when ‘shinyOAuth’ prepares the authorization redirect in
prepare_call() - Represents: generation of state, PKCE material, nonce, state-store write, and construction of the authorization URL
- Parenting: this span is started as a root span so it remains visible even when login is triggered from within a Shiny reactive update
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.phase = "login.request"oauth.used_pkceoauth.nonce_enabled-
oauth.scopes.requested,oauth.scopes.requested_count oauth.claims.requestedoauth.claims.targets-
oauth.required_acr_values,oauth.required_acr_values_count oauth.max_age.requestedoauth.request_object_usedoauth.extra_auth_params_count- Shiny session/process metadata when available
-
Span: shinyOAuth.login.par
- When: during pushed authorization request (PAR) submission when the
provider exposes
par_url - Represents: PAR request construction, client authentication, PAR
response validation, and extraction of
request_uri - Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.phase = "login.par"oauth.client_auth_styleoauth.extra_auth_params_countoauth.extra_token_headers_count- Shiny session/process metadata when available
-
Span: shinyOAuth.login.par.http
- When: for outbound PAR HTTP calls
- Represents: the actual POST to the configured PAR endpoint
- Main attributes:
http.request.method = "POST"server.addressoauth.phase = "login.par"-
http.response.status_code,http.response.content_typeafter a response is available
- Notes:
- this is used as a client span (
kind = "client") - redirects are rejected before client credentials or PAR parameters can leak
- this is used as a client span (
Span: shinyOAuth.callback
- When: during callback handling
- Represents:
- synchronous callback handling in
handle_callback() - the parent callback span created on the main process before async dispatch
- synchronous callback handling in
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.asyncoauth.phase = "callback"-
oauth.introspect,oauth.introspect_elements_count oauth.userinfo.requiredoauth.userinfo.id_token_match_requiredoauth.id_token.validation_enabled- Shiny session/process metadata when available
-
- Notes:
- synchronous
handle_callback()spans also include the joinedoauth.introspect_elementsattribute - the async parent callback span created on the main process carries
only
oauth.introspect_elements_count
- synchronous
- Parenting:
- when the callback can recover the original login span context from
the encrypted state payload, it becomes a child of that
shinyOAuth.login.request - otherwise it is started as a root span instead of inheriting Shiny’s
reactive_update
- when the callback can recover the original login span context from
the encrypted state payload, it becomes a child of that
Span: shinyOAuth.form_post
- When: while the pre-session
response_mode = "form_post"orresponse_mode = "form_post.jwt"POST callback is validated and bridged into Shiny - Represents: form POST envelope validation, JARM validation when applicable, state payload validation, issuer validation, pending-state lookup, and transient handle storage
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestshiny.module_idoauth.phase = "form_post.post"-
oauth.response_mode = "form_post"for plain form POST callbacks -
oauth.response_mode = "form_post.jwt"for JARM POST callbacks
-
- Notes:
- the POST bridge does not consume the logical login state; the callback handler consumes it later, after validating the browser binding and before exchanging the authorization code
- after state decryption succeeds, the span receives the recovered
shinyoauth.trace_id - invalid envelopes that cannot expose a trusted state value remain root spans
Span: shinyOAuth.form_post.bridge
- When: when the Shiny module consumes the one-time form_post handle from the GET bridge query
- Represents: transient form_post handle lookup and single-use consumption
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestshiny.module_idoauth.phase = "form_post.callback_lookup"oauth.form_post.handle_digest
-
Span: shinyOAuth.callback.validate
- When: during callback validation sub-steps
- Represents multiple validation stages; distinguish them through
oauth.phase - Emitted phases currently include:
-
callback.state_payloadandcallback.state_store_consume- emitted during normal synchronous callback handling, and also on the main process before worker dispatch in async callback mode
callback.browser_token_validationcallback.pkce_verifier_validation-
callback.nonce_validation- emitted during the normal synchronous callback path and, in async mode, inside the worker after parent context restoration
-
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digest-
oauth.phaseset to the specific validation stage - Shiny session/process metadata when available
-
Span: shinyOAuth.callback.worker
- When: when async callback processing restores parent trace context in a worker
- Represents: the worker-side child span for async callback execution
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestshiny.module_idoauth.async = TRUEoauth.phase = "callback.worker"- propagated Shiny session/process metadata
-
Span: shinyOAuth.token.exchange
- When: during the authorization-code exchange
- Represents: construction of the token request, token endpoint call, response parsing, and token-response validation prior to deeper ID token verification
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.phase = "token.exchange"oauth.used_pkceoauth.client_auth_style-
oauth.dpop.configured,oauth.dpop.bound,oauth.dpop.token_type_inferred -
oauth.mtls.client_auth,oauth.mtls.certificate_bound_tokens,oauth.mtls.bound oauth.extra_token_params_countoauth.extra_token_headers_countoauth.token_type-
oauth.received_id_token,oauth.received_refresh_token -
oauth.expires_in_present,oauth.expires_in_synthesized -
oauth.scope.present,oauth.scopes.granted - Shiny session/process metadata when available
-
Span: shinyOAuth.token.exchange.http
- When: for outbound token endpoint HTTP calls
- Represents:
- authorization-code token exchange HTTP request
- refresh-token exchange HTTP request
- Distinguish the two cases with
oauth.phase - Emitted phases currently include:
token.exchangerefresh
- Main attributes:
http.request.method = "POST"server.addressoauth.phase-
oauth.mtls.endpoint_aliaswhen an RFC 8705 alias URL is selected -
oauth.dpop.nonce_challenge,oauth.dpop.nonce_retrywhen a DPoP nonce challenge occurs -
http.response.status_code,http.response.content_typeafter a response is available
- Notes:
- this is used as a client span (
kind = "client") - redirects are rejected before credentials can leak
- this is used as a client span (
Span: shinyOAuth.token.verify
- When: after a token response is available and ‘shinyOAuth’ verifies the token set
- Represents:
- scope reconciliation
- token type allowlist validation
- ID token validation or refresh-time ID token continuity checks
- userinfo/ID token subject matching during refresh when applicable
- Distinguish login and refresh verification through
oauth.phase - Emitted phases currently include:
callback.verifyrefresh.verify
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.phase-
oauth.dpop.bound,oauth.dpop.token_type_inferred oauth.mtls.boundoauth.received_id_tokenoauth.received_refresh_token-
oauth.id_token.required,oauth.id_token.present,oauth.id_token.validated oauth.nonce.requiredoauth.scope.validation_mode-
oauth.scopes.requested,oauth.scopes.requested_count -
oauth.scopes.granted,oauth.scopes.granted_count -
oauth.required_acr_values,oauth.required_acr_values_count oauth.refresh_flow
-
Span: shinyOAuth.userinfo
- When: when
get_userinfo()is called - Represents: userinfo request orchestration, response parsing, JWT-vs-JSON handling, and userinfo-level validation/auditing
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.phase = "userinfo"-
oauth.dpop.bound,oauth.dpop.token_type_inferred -
oauth.mtls.client_certificate,oauth.mtls.certificate_bound_tokens,oauth.mtls.bound oauth.userinfo.jwt_requiredoauth.userinfo.jwt_responseoauth.userinfo.subject_present- Shiny session/process metadata when available
-
Span: shinyOAuth.userinfo.http
- When: for the outbound userinfo HTTP call
- Represents: the actual request to the configured userinfo endpoint
- Main attributes:
http.request.method = "GET"server.addressoauth.phase = "userinfo"-
oauth.mtls.endpoint_aliaswhen an RFC 8705 alias URL is selected -
oauth.dpop.nonce_challenge,oauth.dpop.nonce_retrywhen a DPoP nonce challenge occurs -
http.response.status_code,http.response.content_typeafter a response is available
- Notes:
- this is used as a client span (
kind = "client") - redirects are rejected to avoid bearer-token leakage
- this is used as a client span (
Span: shinyOAuth.refresh
- When: during refresh-token processing
- Represents:
- synchronous refresh execution in
refresh_token() - the parent refresh span created on the main process before async dispatch
- synchronous refresh execution in
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.asyncoauth.phase = "refresh"oauth.client_auth_style-
oauth.dpop.configured,oauth.dpop.bound,oauth.dpop.token_type_inferred -
oauth.mtls.client_auth,oauth.mtls.certificate_bound_tokens,oauth.mtls.bound oauth.extra_token_params_countoauth.extra_token_headers_countoauth.token_type-
oauth.received_id_token,oauth.received_refresh_token -
oauth.expires_in_present,oauth.expires_in_synthesized -
oauth.scope.present,oauth.scopes.granted - current or propagated Shiny session/process metadata
-
Span: shinyOAuth.refresh.worker
- When: when async refresh processing restores parent trace context in a worker
- Represents: the worker-side child span for async refresh execution
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.async = TRUEoauth.phase = "refresh.worker"- propagated Shiny session/process metadata
-
- Notes:
- the actual worker-side refresh logic then runs inside a nested
shinyOAuth.refreshspan beneath this bridge span
- the actual worker-side refresh logic then runs inside a nested
Span: shinyOAuth.logout
- When: when
auth[["logout"]]()is called from the module - Represents: best-effort token revocation kickoff, local token/session clear, browser-token reset, and logout audit emission
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestshiny.module_idoauth.phase = "logout"
-
Span: shinyOAuth.session.end.revoke
- When: when a Shiny session ends with
revoke_on_session_end = TRUEand ‘shinyOAuth’ starts best-effort token revocation - Represents: the session-end revocation orchestration span around the
paired
revoke_token()calls - Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestshiny.module_idoauth.phase = "session.end.revoke"- propagated Shiny session/process metadata from the ended session when available
-
Span: shinyOAuth.token.revoke
- When: during token revocation via
revoke_token() - Represents:
- synchronous revocation execution
- the parent revocation span created on the main process before async dispatch
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.asyncoauth.phase = "token.revoke"-
oauth.token.which("access"or"refresh") oauth.client_auth_styleoauth.extra_token_params_countoauth.extra_token_headers_count-
oauth.supported,oauth.revoked,oauth.statusafter completion - current or propagated Shiny session/process metadata
-
Span: shinyOAuth.token.revoke.http
- When: for the outbound revocation endpoint HTTP call
- Represents: the actual request to the configured revocation endpoint
- Main attributes:
http.request.method = "POST"server.addressoauth.phase = "token.revoke"-
http.response.status_code,http.response.content_typeafter a response is available
- Notes:
- this is used as a client span (
kind = "client") - redirects are rejected to prevent credential leakage
- this is used as a client span (
Span: shinyOAuth.token.revoke.worker
- When: when async revocation processing restores parent trace context in a worker
- Represents: the worker-side child span for async revocation execution
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.async = TRUEoauth.phase = "token.revoke.worker"-
oauth.token.which("access"or"refresh") - propagated Shiny session/process metadata
-
- Notes:
- the actual worker-side revocation logic then runs inside a nested
shinyOAuth.token.revokespan beneath this bridge span
- the actual worker-side revocation logic then runs inside a nested
Span: shinyOAuth.token.introspect
- When: during token introspection via
introspect_token() - Represents:
- synchronous introspection execution
- the parent introspection span created on the main process before async dispatch
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.asyncoauth.phase = "token.introspect"-
oauth.token.which("access"or"refresh") oauth.client_auth_styleoauth.extra_token_params_countoauth.extra_token_headers_count-
oauth.supported,oauth.active,oauth.statusafter completion - current or propagated Shiny session/process metadata
-
Span: shinyOAuth.token.introspect.http
- When: for the outbound introspection endpoint HTTP call
- Represents: the actual request to the configured introspection endpoint
- Main attributes:
http.request.method = "POST"server.addressoauth.phase = "token.introspect"-
http.response.status_code,http.response.content_typeafter a response is available
- Notes:
- this is used as a client span (
kind = "client") - redirects are rejected to prevent credential leakage
- this is used as a client span (
Span: shinyOAuth.token.introspect.worker
- When: when async introspection processing restores parent trace context in a worker
- Represents: the worker-side child span for async introspection execution
- Main attributes:
-
oauth.provider.name,oauth.provider.issuer oauth.client_id_digestoauth.async = TRUEoauth.phase = "token.introspect.worker"-
oauth.token.which("access"or"refresh") - propagated Shiny session/process metadata
-
- Notes:
- the actual worker-side introspection logic then runs inside a nested
shinyOAuth.token.introspectspan beneath this bridge span
- the actual worker-side introspection logic then runs inside a nested