Connect shinyOAuth to a shared storage backend, such as Redis or a database,
by wrapping your R functions in a cachem-like interface. Use the result as
state_store in oauth_client() to share pending login state, or as
jwks_cache in oauth_provider() to share provider signing keys (JWKS).
A shared state store is needed when a login can start on one R process and its callback can arrive at another, for example in a multi-worker deployment without sticky routing. With a shared signing-key cache, workers reuse keys fetched by other workers.
Usage
custom_cache(
get,
set,
remove,
take = NULL,
info = NULL,
set_if_absent = NULL,
worker_safe = FALSE
)Arguments
- get
A function(key, missing = NULL) -> value. Required. Should return the stored value, or the
missingargument if the key is not present. Themissingparameter is required because shinyOAuth passes it explicitly.- set
A function(key, value) -> invisible(NULL). Required. Should store the value under the given key.
- remove
A function(key) -> any. Required.
Delete the entry for
key. shinyOAuth does not depend on the return value. State consumption uses atomictakewhere available; a process-local fallback checks that removal succeeded.- take
A function(key, missing = NULL) -> value. Optional.
Read and remove the entry as one indivisible operation, returning its value or the
missingargument when absent. Required for shared state stores: separate reads and deletes can let two requests use the same login record. RedisGETDELand SQLDELETE ... RETURNINGare examples. The process-local memory store has a safe fallback; custom/shared stores withouttakefail by default when login state is consumed.- info
Function() -> list(max_age = seconds, ...). Optional
oauth_module_server()uses the returnedmax_ageto align the browser cookie's maximum age with the store's retention period.- set_if_absent
A function(key, value, ttl = NULL) -> logical. Optional.
An atomic set-if-missing operation for shared JWKS caches and callback bridge slots. It must store
valueand returnTRUEonly whenkeydid not already exist; otherwise it must leave the existing value unchanged and returnFALSE. Whenttlis supplied, the claimed key must expire after that many seconds. Map this to a native backend primitive such as RedisSET ... NX EXor a database uniqueness constraint with expiry. shinyOAuth uses this operation to make forced JWKS-refresh throttling safe across workers. Without it, forced refresh is disabled for shared/custom caches;cachem::cache_mem()keeps its process-local serialized fallback. Callback bridges require it for custom/shared stores to avoid overwriting concurrent candidates; full partitions reject callbacks until slots expire or are consumed. The process-local memory store uses a serialized fallback. The existingshinyOAuth.allow_non_atomic_state_store = TRUEoption also permits non-atomic callback insertion, with no protection against concurrent writers replacing a candidate. Other state-store writes still useset.- worker_safe
Logical, default
FALSE. Explicitly declares that this backend can be serialized and used in another R process. Applies when the cache is used asjwks_cache; see the worker-safety requirements above.
Value
An R6 object exposing cachem-like [["get"]]/[["set"]]/[["remove"]]/[["info"]] methods
and the optional [["take"]] and [["set_if_absent"]] atomic methods.
Details
Backend functions must preserve stored R values and expire entries after their configured lifetime.
Shared login state in multi-worker deployments
The default cachem::cache_mem() state store belongs to one R process.
If a load balancer sends the returning callback to another worker, that
worker cannot find the pending login and validation fails. Shared storage
allows either worker to resume it, provided both use the same state_key
and provider/client settings.
A shared state_store requires atomic take: reading and deleting a pending
login in one operation so two requests cannot use it. Redis GETDEL and SQL
DELETE ... RETURNING provide this operation. Separate reads and deletes,
including those in
cachem::cache_disk(), can let two workers consume the same state.
See the deployment guidance.
For the form_post and form_post.jwt callback bridges, also implement
set_if_absent to reserve callback slots without replacing pending responses.
The backend preserves stored R values and enforces access controls, expiry and atomic consumption. Pending login records are encrypted. An upgrade can invalidate pending records; affected logins then restart.
Hosted Request Objects (request_object_mode = "request_uri") use separate
records. Signed objects can contain readable authorization details; configure
Request Object encryption if these details must be confidential in storage.
For a state store, returning max_age in seconds from info() also lets
oauth_module_server() align the browser cookie lifetime with the store.
Reporting this value does not expire entries; your backend must enforce it.
Shared provider signing keys
A shared jwks_cache can reduce repeated key downloads when several R
workers use the same provider. This is independent of sharing login state:
sharing signing keys alone does not let another worker resume a login.
Key caching uses get and set. Also implement set_if_absent to coordinate
rate-limited forced key refreshes across workers, for example when the provider
rotates its signing keys. Without that atomic operation, forced refresh is
disabled for custom/shared caches. Use separate stores or key namespaces for
login state and signing keys when they require different expiry policies.
A custom signing-key cache defaults to synchronous fallback for asynchronous
login and token operations. worker_safe = TRUE declares that its functions
and captured values remain usable in a new R process. That requires per-operation
connections rather than captured database/file handles; serialization alone
cannot establish worker safety. Standard cachem::cache_mem() and
cachem::cache_disk() backends support worker dispatch automatically.
Other cache implementations can expose a scalar logical worker_safe member
with the same contract. State stores are not used in callback workers.
Examples
# This in-memory example illustrates the cache interface in one R process.
# It does not share entries across workers or implement timed expiry.
# A production shared store must implement both itself.
mem <- new.env(parent = emptyenv())
my_cache <- custom_cache(
get = function(key, missing = NULL) {
base::get0(key, envir = mem, ifnotfound = missing, inherits = FALSE)
},
set = function(key, value) {
assign(key, value, envir = mem)
invisible(NULL)
},
remove = function(key) {
if (exists(key, envir = mem, inherits = FALSE)) {
rm(list = key, envir = mem)
}
invisible(NULL)
},
# In a shared store, replace this with the backend's atomic get-and-delete
# operation, such as Redis GETDEL. This R environment is process-local.
take = function(key, missing = NULL) {
val <- base::get0(key, envir = mem, ifnotfound = missing, inherits = FALSE)
if (exists(key, envir = mem, inherits = FALSE)) {
rm(list = key, envir = mem)
}
val
},
info = function() list(max_age = Inf)
)