Overview
This vignette documents the configuration of OAuth 2.0 and OpenID Connect extensions supported by shinyOAuth: JWT client authentication, mutual TLS (mTLS), signed authorization requests (JAR), pushed authorization requests (PAR), Form Post responses, signed authorization responses (JARM), and tokens bound to a private key (DPoP).
These features require corresponding support and configuration at the provider. The examples extend the provider and client setup in the usage vignette. Replace placeholder domains, credentials, and key paths with your registered values. Each example shows the settings for the feature being discussed.
Browser binding and deployment boundary
Use HTTPS and treat every service on the same hostname as trusted,
including services on other ports. Cookie scope does not include the
port, as specified in RFC 6265
section 8.5. The __Host- prefix prevents sibling-domain
cookie injection; it does not isolate ports. Signed or
HttpOnly cookies still reach same-host services in HTTP
requests. Use a dedicated hostname when those services are
untrusted.
shinyOAuth keeps the actual browser-binding token in origin- and tab-scoped session storage, accompanied by an independent random cookie marker. It restores the binding only when the cookie matches an unexpired local record. Reading or planting a cookie on another port cannot establish that record. Cookie disruption can still abort login; pages and scripts on the same origin can access session storage, so XSS prevention remains necessary. Cookies, session storage, and Web Crypto must be available, and pending logins from older cookie-only or local-storage versions must be restarted after upgrading.
Each new authorization request gets a fresh server-selected binding
and a distinct marker cookie. Idle predecessor markers are removed when
the new binding is ready. Predecessors associated with pending
transactions (or older records whose transaction status is unknown)
remain until their original TTL expires, so a cloned tab cannot
invalidate the original tab’s pending login. Application callback routes
and tabs have independent records; complete login in the tab that
started it. Private module inputs are excluded from both URL and disk
bookmarks. Avoid copying browser tokens into custom bookmark values or
logs. The acknowledgment is a delivery check, not independent server
verification of HTTP cookie possession. The standalone
prepare_call() and handle_callback() APIs
still require the caller to establish and protect their own browser
binding.
Provider metadata and OIDC discovery
oauth_provider_oidc_discover() reads provider metadata
used by these features, including PAR support, JARM and DPoP algorithms,
and mTLS endpoint aliases:
provider <- oauth_provider_oidc_discover(
issuer = "https://id.example.com"
)The sections below show the extra settings you usually add on top of
your normal oauth_client() setup.
JWT client authentication
Some providers require your app to sign a short statement proving its identity when requesting tokens. This client assertion is a JWT (JSON Web Token). It identifies the app, rather than the user signing in.
For a registered private key, select the method during provider setup and supply the key when creating your client:
provider <- oauth_provider_oidc_discover(
"https://id.example.com", token_auth_style = "private_key_jwt"
)
client <- oauth_client(
provider = provider,
client_id = "client-id",
redirect_uri = "https://app.example.com",
scopes = c("openid", "profile"),
client_assertion_private_key = openssl::read_key("keys/client-key.pem"),
client_assertion_private_key_kid = "registered-key-id"
)Register the corresponding public key with your provider. For
client_secret_jwt, select that method and supply a
sufficiently strong client_secret instead.
client_assertion_alg has a key-compatible default;
client_assertion_audience overrides the expected recipient
if your provider requires a value other than the token request URL.
For the client assertion profile referenced by OAuth 2.1 draft 16, select the trusted authorization-server issuer as the sole audience and optionally use the recommended explicit type:
client@client_assertion_audience <- provider@issuer
client@client_assertion_typ <- "client-authentication+jwt"RFC7523bis
draft 11 section 4 requires the issuer audience and recommends this
type. The package preserves the existing JWT type and
request-endpoint audience defaults, including PAR’s existing issuer
handling. The checker treats audience mismatches as mandatory findings
and legacy typing as an advisory. These settings affect client
authentication assertions, including retries, and do not change JAR,
JARM or ID token headers. The same settings can be selected in
endpoint_auth overrides.
Authentication can differ at PAR, introspection, and revocation endpoints. Discovery preserves their independent method and algorithm metadata; configure credentials and audiences to match each endpoint’s registration agreement:
client@endpoint_auth <- list(
introspection = list(
token_auth_style = "header",
client_id = "registered-inspector",
client_secret = Sys.getenv("INTROSPECTION_SECRET"),
extra_headers = c("X-App" = "registered-app")
),
revocation = list(
token_auth_style = "private_key_jwt",
client_assertion_private_key = openssl::read_key("keys/revocation-key.pem"),
client_assertion_alg = "RS256",
client_assertion_audience = "https://id.example.com/revocation"
)
)Unspecified credentials inherit the client’s settings. Advertised
methods and JWT algorithms are checked per endpoint. Discovery defaults
omitted revocation methods to Basic authentication; omitted
introspection methods have no standard default, so confirm the
configured method with your provider. PAR inherits the token
authentication settings unless explicitly overridden.
extra_token_headers now applies only to exchange and
refresh: opt in through extra_headers at each other
endpoint that should receive those headers, even on the same origin.
Issuer comparison and transport policy
compare_callback_issuer = TRUE compares a supplied
callback iss exactly against provider@issuer.
Pair it with enforce_callback_issuer = FALSE when absence
is permitted for your older provider. Required presence always implies
comparison. An explicitly supplied legacy
enforce_callback_issuer = FALSE retains complete opt-out
unless comparison is explicitly enabled. Otherwise, issuer-configured
clients enable comparison automatically.
Participating clients require iss when the provider
advertises RFC 9207 support. Validated JARM supplies its own issuer and
does not require a redundant outer value. Multi-server applications must
retain their selected issuer or distinct-route defense. The expected
issuer comes from trusted configuration; it is not inferred from a token
URL or normalized before comparison. See RFC 9207
section 2.4.
options(shinyOAuth.tls_min_version = "1.2") requires TLS
1.2 or later on package HTTPS requests; "1.3" selects a
higher minimum and NULL preserves runtime defaults.
Configure it before discovery or login. The policy preserves stronger
supplied minima, compatible maxima and custom CA roots. It also applies
in async workers and participates in pending-login policy checks. An
absent option is insufficient evidence about an actual handshake; the
checker reports older unresolved backend defaults as unknown.
Browser/proxy hops and future resource requests need separate
validation. Details and constraints are in package
options.
Callback pages and application scripts
Use oauth_ui(ui, id = "auth", client = client) for query
and query-JARM responses. The ID and client must match
oauth_module_server(). The wrapper validates callbacks and
stores sealed, short-lived responses, then redirects to a one-time
bridge URL before invoking the application UI. State remains single-use
and browser-bound. Missing bridge configuration rejects raw callbacks;
existing oauth_ui(ui) setups must add id and
client.
Callback and HTML responses send
Cache-Control: no-store, Pragma: no-cache, and
Referrer-Policy: no-referrer. These do not remove upstream
access logs: configure proxies and hosting logs to omit callback
queries. Keep third-party scripts off any unsanitized callback page,
including when using use_shinyOAuth() with a custom HTTP
integration. Register fixed tenant or routing query parameters in the
redirect URI; unregistered inbound parameters are discarded on
continuation. For trusted HTTPS-terminating proxies, use
request_uri_resolver with the same trust checks as
oauth_form_post_ui().
Provider key-set validation
shinyOAuth validates the whole fetched JWKS before selecting a key by
kid, usage, operations, or algorithm. A malformed RSA, EC,
or OKP public entry can therefore reject the set even when another entry
would validate the token. This is a deliberately stricter availability
policy than the recommendation to ignore individual unusable keys in RFC 7517
section 5. Providers must publish structurally valid public entries
throughout key rotation; an unrelated broken entry can interrupt login,
signed UserInfo, JARM, or Request Object encryption until the provider
repairs its JWKS.
Unknown key types are ignored for selection after common structural
checks. Malformed set structure, duplicate JSON members, and secret key
material are rejected. With pinning mode "any", at least
one supported public key must match a configured thumbprint, and
selected keys must be pinned. Mode "all" requires every
RSA, EC, and OKP entry in the set to have a computable, configured
thumbprint, including entries that would later be filtered out. Neither
mode skips the whole-set structural checks or the selected key’s
strength checks.
Trusted ID-token audiences
ID tokens with multiple audiences remain rejected by default. If an
issuer legitimately includes another trusted audience, configure
trusted_id_token_audiences = c("trusted-service") on
oauth_client(). The token must still include this client’s
ID in aud, and azp must equal this client’s ID
when present. Trusted multiple audiences do not require an
azp claim under OIDC
Core section 3.1.3.7. Other audiences, incorrect authorized parties,
and invalid signatures are rejected.
Mutual TLS (mTLS)
With mutual TLS (mTLS), the client presents a certificate during the TLS connection. OAuth 2.0 uses this for certificate-based client authentication and for certificate-bound access tokens (RFC 8705). With certificate-bound tokens, the API requires the matching certificate when accepting a token. The provider must support the selected use of mTLS.
provider <- oauth_provider(
name = "example-mtls",
# Exact OIDC issuer; enables nonce and ID-token validation
issuer = "https://id.example.com",
auth_url = "https://id.example.com/authorize",
token_url = "https://id.example.com/token",
jwks_uri = "https://id.example.com/jwks",
userinfo_url = "https://id.example.com/userinfo",
# Use RFC 8705 client-certificate auth at the token endpoint
token_auth_style = "tls_client_auth",
# Use mTLS-specific endpoints when the provider publishes them
mtls_endpoint_aliases = list(
token_endpoint = "https://mtls.id.example.com/token",
userinfo_endpoint = "https://mtls.id.example.com/userinfo"
),
# Expect certificate-bound access tokens from the provider
mtls_client_certificate_bound_access_tokens = TRUE
)
client <- oauth_client(
provider = provider,
client_id = "client-id",
redirect_uri = "https://app.example.com/auth/callback",
scopes = c("openid", "profile"),
# Certificate and key sent on mTLS requests
mtls_client_cert_file = "certs/client.pem",
mtls_client_key_file = "certs/client-key.pem",
mtls_client_ca_file = "certs/ca.pem",
# Require the matching certificate when access tokens are used
mtls_certificate_bound_access_tokens = TRUE
)mtls_certificate_bound_access_tokens = TRUE enables
certificate presentation and mTLS endpoint-alias selection independently
of the OAuth client authentication method. Configuring certificate files
alone does not enable certificate-bound token requests.
The separate mtls_require_observed_cnf policy defaults
to TRUE, preserving strict local assurance: shinyOAuth must
observe cnf[["x5t#S256"]] in the token response, a JWT
access token, or introspection and match it to the configured
certificate. An opaque bound token without observable binding fails this
policy.
RFC 8705 also permits opaque tokens whose binding is known only to the authorization and resource servers. For that deployment, configure the certificate/key and provider capability as above, then use both settings:
client <- oauth_client(
provider = provider,
client_id = "client-id",
redirect_uri = "https://app.example.com/auth/callback",
mtls_client_cert_file = "certs/client.pem",
mtls_client_key_file = "certs/client-key.pem",
mtls_certificate_bound_access_tokens = TRUE,
mtls_require_observed_cnf = FALSE
)This mode still presents the certificate on token, refresh, UserInfo,
and protected-resource requests, including with body authentication,
public clients, or private_key_jwt. It prefers configured
mTLS aliases and falls back to the configured endpoint URL when no alias
is available. The servers must enforce binding; shinyOAuth allows
missing confirmation but still rejects any observed mismatch or
conflicting confirmation claims. See RFC 8705,
Section 3.
On Windows, separate PEM certificate/key files require curl’s OpenSSL
backend. Set CURL_SSL_BACKEND=openssl in
.Renviron and restart R, or run
Sys.setenv(CURL_SSL_BACKEND = "openssl") before loading
curl, httr2, or shinyOAuth in a fresh session. Check
curl::curl_version()[["ssl_version"]]: parenthesized
backends are inactive alternatives. If OpenSSL is unavailable, install a
curl build that provides it. shinyOAuth rejects an active Schannel
backend for this PEM configuration before sending the request. See the
libcurl
certificate documentation.
If your provider uses dynamic client registration,
oauth_client_mtls_registration() can build the RFC 8705
registration metadata from the configured client.
JWT-secured authorization request (JAR)
JAR protects authorization request parameters with a signature. The client sends them in a JWT called a Request Object, which the provider verifies before processing the request. Optional Request Object encryption also protects the request contents. Both settings must match the provider registration.
For enforced request integrity, configure the authorization server to
require signed Request Objects for this client, for example with the RFC
9101 client registration field
require_signed_request_object = true, and register its
signing key and allowed algorithm. Confirm the server actually enforces
this policy. Setting signed_request_object_required = TRUE
below describes that server policy and enforces local construction; it
does not register or change the client at the server. If unsigned
requests remain accepted, signing is optional and cannot prevent a
downgrade to unsigned authorization requests (RFC 9101
section 10.5).
provider <- oauth_provider(
name = "example-jar",
issuer = "https://id.example.com",
auth_url = "https://id.example.com/authorize",
token_url = "https://id.example.com/token",
# The server registration must already require signed Request Objects
signed_request_object_required = TRUE,
request_parameter_supported = TRUE,
request_object_signing_alg_values_supported = c("RS256")
)
client <- oauth_client(
provider = provider,
client_id = "client-id",
client_secret = "client-secret",
redirect_uri = "https://app.example.com/auth/callback",
scopes = c("openid", "profile"),
# Signing key for the Request Object
client_assertion_private_key = openssl::read_key("keys/client-key.pem"),
# Send the authorization request as a JWT in the request parameter
request_object_mode = "request",
request_object_signing_alg = "RS256"
)Test the server policy before deployment: send an otherwise valid
authorization request for this same client without request
or request_uri. For example:
unsigned <- httr2::request(provider@auth_url) |>
httr2::req_url_query(
client_id = client@client_id,
redirect_uri = client@redirect_uri,
response_type = "code", scope = "openid profile",
state = "unsigned-policy-probe",
code_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
code_challenge_method = "S256"
) |>
httr2::req_options(followlocation = FALSE) |>
httr2::req_error(is_error = function(resp) FALSE) |>
httr2::req_perform()
httr2::resp_status(unsigned)
httr2::resp_headers(unsigned)
httr2::resp_body_string(unsigned)The negative test passes only when the server explicitly rejects the
request because the required signed Request Object is missing. Check its
documented error response or server audit event for that reason; a
generic HTTP error is insufficient. A login/consent page, authorization
code, or login_required response does not establish
enforcement. Repeat this probe in deployment tests alongside a
successful signed request.
Register the signing key with your provider. To encrypt the signed
request too, configure
request_object_encryption_alg = "RSA-OAEP" and
request_object_encryption_enc to a supported AES-CBC-HMAC
value such as "A128CBC-HS256". The provider must publish a
suitable encryption key or you must supply
request_object_encryption_jwk to oauth_provider().
See oauth_client()
for supported algorithms and the provider reference for encryption key
selection.
PAR, described below, keeps most request details out of the browser URL. It can also carry a signed Request Object, combining PAR with JAR.
Request Objects published by the Shiny app
If you set request_object_mode = "request_uri",
shinyOAuth still builds a signed Request Object, but instead of putting
that JWT directly on the browser redirect as request=...,
it publishes the Request Object at a URL and sends the provider
request_uri=<that URL>. The provider then fetches
that published Request Object itself.
oauth_module_server() serves the Request Object at a
short-lived URL under the Shiny app. The provider must be able to
request that URL directly.
This mode is separate from PAR and cannot be used when the provider requires PAR. With PAR, the provider issues the reference; with this mode, the provider fetches a URL published by your app.
Deployment requirements:
- the published URL must use HTTPS and be reachable from the provider, not just from the user’s browser
- if the provider requires pre-registered
request_urivalues, the public URL or wildcard prefix must already be registered there
request_uri_provider <- oauth_provider(
name = "example-request-uri",
issuer = "https://id.example.com",
auth_url = "https://id.example.com/authorize",
token_url = "https://id.example.com/token",
request_uri_parameter_supported = TRUE,
request_object_signing_alg_values_supported = "RS256"
)
client <- oauth_client(
provider = request_uri_provider,
client_id = "client-id",
client_secret = "client-secret",
redirect_uri = "https://app.example.com/auth/callback",
scopes = c("openid", "profile"),
client_assertion_private_key = openssl::read_key("keys/client-key.pem"),
# Publish the Request Object by reference instead of sending it inline
request_object_mode = "request_uri",
request_object_signing_alg = "RS256"
)
# Inside server()
auth <- oauth_module_server(
"auth",
client,
auto_redirect = TRUE,
# Public HTTPS base URL of this Shiny app as seen by the provider
request_uri_base_url = "https://shiny.yourdomain.com/myapp"
)Wrap the app UI in
oauth_ui(ui, id = "auth", client = client) (or
oauth_form_post_ui() for POST callbacks). The app root
serves Request Objects using a random, single-purpose
shinyOAuth_request_object query handle. Only its digest is
stored alongside the object in client@state_store. GET
consumes it atomically; HEAD does not consume it. Retrieval expires at
the earlier of the object expiry and 120 seconds after publication.
Cache eviction can shorten availability; the store’s retention policy
controls physical cleanup. If the provider requires
request_uri registration, update its permitted URLs to
cover this app-root endpoint when migrating from the old
/session/ paths. For multiple workers, use a shared state
store with atomic take(); a memory store works only when
publication and retrieval reach the same R process.
The hosted JWT and its expiry occupy a separate store record, outside
the pending-login record’s state_key sealing. Signing
leaves JWT claims readable; enable Request Object JWE encryption for
claim confidentiality. See the custom_cache()
shared-store contract for record protection and expiry
requirements.
Earlier versions used Shiny’s raw live session token in
registerDataObj() URLs. Those URLs could disclose a
capability also used by other session resources through browser,
provider, or proxy logs. The new handles contain no session token. Still
redact request URLs in logs and limit log retention: disclosing a live
handle allows its holder to consume the pending object. Prefer PAR when
supported to use a provider-issued reference and avoid a public app
retrieval endpoint. OIDC signed requests retain outer
client_id, response_type, and
scope parameters.
authorization_request_front_channel_mode = "minimal" is
available for compatible PAR providers, but is rejected for OIDC inline
or client-published signed requests.
Pushed authorization requests (PAR)
PAR sends the authorization request from your server to the provider
first. The browser then gets redirected with a short
request_uri handle instead of the full request details.
PAR allows the provider to validate the request before the browser
redirect and avoids placing large requests in a URL. It also keeps most
request details out of browser history and logs of browser requests. Set
par_required = TRUE when the provider requires PAR.
provider <- oauth_provider(
name = "example-par",
issuer = "https://id.example.com",
auth_url = "https://id.example.com/authorize",
token_url = "https://id.example.com/token",
# Enable pushed authorization requests
par_url = "https://id.example.com/par",
par_required = TRUE,
# Keep the browser redirect down to client_id + PAR request_uri
authorization_request_front_channel_mode = "minimal"
)
client <- oauth_client(
provider = provider,
client_id = "client-id",
client_secret = "client-secret",
redirect_uri = "https://app.example.com/auth/callback",
scopes = c("openid", "profile")
)Form Post response mode
response_mode = "form_post" tells the provider to send
the authorization response back as an HTTP POST body instead of query
parameters on the URL. The body still contains normal OAuth fields such
as code, state, error, and
iss.
Use form_post when required by the provider or to keep
callback values out of the browser URL, history, and logs of browser
requests. It changes the callback transport; it does not sign or encrypt
the response.
Keep your existing provider and credentials, set
response_mode = "form_post" on the client, then use this UI
setup. Here client has a registered
redirect_uri such as
https://app.example.com/callback.
base_ui <- shiny::fluidPage(shiny::textOutput("status"))
ui <- oauth_form_post_ui(base_ui, id = "auth", client = client)
server <- function(input, output, session) {
auth <- oauth_module_server("auth", client)
output[["status"]] <- shiny::renderText({
if (isTRUE(auth[["authenticated"]])) "Signed in" else "Waiting for login"
})
}
app <- shiny::shinyApp(ui, server, uiPattern = ".*")The wrapper includes oauth_ui() setup. Its module ID and
client must match the server’s. For a callback path such as
/callback, uiPattern = ".*" lets Shiny send
the POST to the wrapper. A callback at the app root also works.
callback_path defaults to the path in
redirect_uri; keep both aligned.
Deployment behind an HTTPS proxy
If your web server accepts HTTPS but forwards HTTP to Shiny, the
wrapper needs a trusted way to recover the public request address.
Configure request_uri_resolver for your own proxy. This
example accepts one proxy IP and a fixed public origin, including a
mounted app path:
trusted_proxy_uri <- function(req) {
if (!identical(req[["REMOTE_ADDR"]], "10.0.0.10") ||
!identical(req[["HTTP_X_FORWARDED_PROTO"]], "https")) {
return(NULL)
}
paste0("https://app.example.com", req[["SCRIPT_NAME"]], req[["PATH_INFO"]])
}
ui <- oauth_form_post_ui(
base_ui, id = "auth", client = client,
request_uri_resolver = trusted_proxy_uri
)Use your deployment’s verified proxy address and public origin. The result must still match the configured redirect origin and callback path. Do not trust forwarded headers from arbitrary clients.
JWT-secured authorization response mode (JARM)
JARM protects the authorization response with a signature. The provider returns a JWT, and shinyOAuth verifies its signature, issuer, audience, and expiry before processing the callback fields. If encryption is configured, shinyOAuth decrypts the response before validating the signed contents.
provider <- oauth_provider(
name = "example-jarm",
issuer = "https://id.example.com",
auth_url = "https://id.example.com/authorize",
token_url = "https://id.example.com/token",
# Advertise the JARM response modes and algorithms this provider supports
response_modes_supported = c("query", "query.jwt", "form_post.jwt"),
jarm_signing_alg_values_supported = c("RS256"),
jarm_encryption_alg_values_supported = c("RSA-OAEP"),
jarm_encryption_enc_values_supported = c("A128CBC-HS256")
)
client <- oauth_client(
provider = provider,
client_id = "client-id",
client_secret = "client-secret",
redirect_uri = "https://app.example.com/auth/callback",
scopes = c("openid", "profile"),
# Ask for a JWT-wrapped authorization response
response_mode = "query.jwt",
jarm_signed_response_alg = "RS256"
)For encrypted JARM, add the decryption settings:
client <- oauth_client(
provider = provider,
client_id = "client-id",
client_secret = "client-secret",
redirect_uri = "https://app.example.com/auth/callback",
scopes = c("openid", "profile"),
response_mode = "query.jwt",
jarm_signed_response_alg = "RS256",
# Optional: decrypt JARM before validating the signed payload
jarm_encrypted_response_alg = "RSA-OAEP",
jarm_encrypted_response_enc = "A128CBC-HS256",
jarm_decryption_private_key = openssl::read_key("keys/jarm-decrypt.pem")
)JARM is currently intended for oauth_module_server(). If
you use response_mode = "form_post.jwt", wrap your UI with
oauth_form_post_ui().
Demonstrating proof-of-possession (DPoP)
DPoP binds tokens to a client key. The client signs a proof for each token or API request, and the receiving server verifies it against the token binding. An API enforcing DPoP requires both the access token and a proof from the matching private key. Configure it when supported by the authorization server and the API.
provider <- oauth_provider(
name = "example-dpop",
issuer = "https://id.example.com",
auth_url = "https://id.example.com/authorize",
token_url = "https://id.example.com/token",
# Optional metadata check for acceptable DPoP signing algorithms
dpop_signing_alg_values_supported = c("ES256")
)
client <- oauth_client(
provider = provider,
client_id = "client-id",
client_secret = "client-secret",
redirect_uri = "https://app.example.com/auth/callback",
scopes = c("openid", "profile", "api.read"),
# Private key used to sign DPoP proofs
dpop_private_key = openssl::read_key("keys/dpop-key.pem"),
dpop_signing_alg = "ES256"
)After login, keep using the request helpers instead of adding
Authorization or DPoP headers manually:
resp <- perform_resource_req(
auth[["token"]],
"https://api.example.com/me",
# Lets shinyOAuth attach the DPoP proof and handle nonce challenges
client = client
)Token binding requirements and validation
Supplying a DPoP key makes dpop_require_access_token
default to TRUE: the provider must return a DPoP access
token. If binding data is visible, its cnf[["jkt"]] key
thumbprint must match. For opaque tokens with no visible binding, enable
dpop_require_observed_cnf = TRUE and arrange introspection
if your deployment needs to confirm that binding locally.
For certificate-bound tokens, the corresponding field is
cnf[["x5t#S256"]]. The package checks it against the
configured certificate before protected API and userinfo calls. A
refreshed token needs fresh binding data from the new token or its
introspection response; the old certificate thumbprint is not carried
forward when the response omits it.
Binding data read from a JWT access token is observed payload data; shinyOAuth does not independently verify that access token’s signature. Introspection can provide confirmation from the provider. The API must enforce the binding too for a stolen token to be unusable without its key or certificate.
DPoP in API requests
perform_resource_req() and get_userinfo()
handle a DPoP nonce challenge with one fresh-proof retry. Later requests
to the same resource server can reuse its nonce; token-server and
resource-server nonces are kept separate. Retries of eligible API
requests generate fresh proofs.
DPoP nonces must follow RFC 9449’s visible ASCII syntax. The package
also applies a local 4096-byte limit to bound proof and cache sizes; the
RFC itself sets no maximum length. Configure
options(shinyOAuth.dpop_nonce_max_bytes = 8192L) for a
provider issuing larger nonces (supported range: 1–65536 bytes). A
response above the configured limit raises an explicit error without
logging its nonce.
resource_req() only builds the request. A DPoP proof is
tied to its HTTP method and base URL, so do not change those after
construction. Supply query parameters through the helper’s
query argument; external URL modifiers can decode reserved
path characters and invalidate the proof. Use
perform_resource_req() to manage nonce retries.
Signature and encryption support
For outgoing private-key client assertions, JAR, and DPoP, signing
supports RS256, RS384, ES256,
ES384, ES512, and both EdDSA and
Ed25519 with Ed25519 keys. Ed25519 accepts an OpenSSL
private key or PEM; DPoP embeds only its public OKP JWK. Algorithm
inference and explicit choices remain constrained by provider metadata.
Secret-based assertions and JAR support HS256,
HS384, and HS512. RSA continues to default to
RS256; choose client_assertion_alg = "RS384" explicitly for
a registration requiring it. RS384 uses SHA-384 with RSASSA-PKCS1-v1_5
and requires an RSA key of at least 2048 bits, as specified in RFC 7518
section 3.3. RSA-PSS and Ed448 are not supported for outgoing
signatures. Incoming signature policies are separate; see oauth_provider()
and the jarm_* arguments in oauth_client().
Request Object encryption and JARM decryption support
RSA-OAEP with A128CBC-HS256,
A192CBC-HS384, or A256CBC-HS512. This does not
imply support for encrypted ID tokens or encrypted userinfo, which are
rejected.
The R openssl API currently exposes neither OAEP digest
selection nor RSA-PSS padding options. Its GCM functions do not accept
the additional authenticated data needed to authenticate JWE protected
headers. Consequently, RSA-OAEP-256,
PS256/384/512, and A128/192/256GCM JWE modes
remain unsupported and fail closed; provider metadata never causes a
fallback to another algorithm. These are backend interoperability
limits, not interchangeable names for the supported modes. See the OpenSSL R
API.