From ade587968f6aa7f3e43304608c79346190b17a06 Mon Sep 17 00:00:00 2001 From: Dexterity <173429049+Dexterity104@users.noreply.github.com> Date: Tue, 19 May 2026 11:38:08 -0400 Subject: [PATCH] :zap: Cache OIDC provider records to skip per-login discovery (#9295) Co-authored-by: Andrey Antukh --- backend/src/app/auth/oidc.clj | 20 +++++++++++++++----- backend/src/app/util/cache.clj | 12 +++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index cfe0547428..26986fd781 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -27,6 +27,7 @@ [app.rpc.commands.profile :as profile] [app.setup :as-alias setup] [app.tokens :as tokens] + [app.util.cache :as cache] [app.util.inet :as inet] [app.util.json :as json] [buddy.sign.jwk :as jwk] @@ -694,15 +695,24 @@ (db/pgarray? roles) (assoc :roles (db/decode-pgarray roles #{})))) -;; TODO: add cache layer for avoid build an discover each time +;; A short TTL avoids paying the OIDC discovery + JWKS fetch on every +;; login; Caffeine will not store the entry when the load fn throws, +;; so a transient failure at the provider's discovery endpoint does +;; not poison the cache. +(defonce ^:private provider-cache + (cache/create :expire "10m" :max-size 64)) + +(defn- load-provider + [cfg id] + (when-let [params (some->> (db/get* cfg :sso-provider {:id id :is-enabled true}) + (decode-row))] + (case (:type params) + "oidc" (prepare-oidc-provider cfg params)))) (defn get-provider [cfg id] (try - (when-let [params (some->> (db/get* cfg :sso-provider {:id id :is-enabled true}) - (decode-row))] - (case (:type params) - "oidc" (prepare-oidc-provider cfg params))) + (cache/get provider-cache id (partial load-provider cfg)) (catch Throwable cause (l/err :hint "unable to configure custom SSO provider" :provider (str id) diff --git a/backend/src/app/util/cache.clj b/backend/src/app/util/cache.clj index 3506a2fb3c..747e7ece3a 100644 --- a/backend/src/app/util/cache.clj +++ b/backend/src/app/util/cache.clj @@ -12,7 +12,6 @@ [app.common.time :as ct] [promesa.exec :as px]) (:import - com.github.benmanes.caffeine.cache.AsyncCache com.github.benmanes.caffeine.cache.Cache com.github.benmanes.caffeine.cache.Caffeine com.github.benmanes.caffeine.cache.RemovalListener @@ -47,15 +46,18 @@ :miss-rate (.missRate stats)})) (defn create - [& {:keys [executor on-remove max-size keepalive]}] + "Build an in-memory cache. Loads run synchronously on the calling + thread, so when a load fn throws or returns nil the entry is not + stored — concurrent loads for the same key still deduplicate." + [& {:keys [executor on-remove max-size keepalive expire]}] (let [cache (as-> (Caffeine/newBuilder) builder (if (fn? on-remove) (.removalListener builder (create-listener on-remove)) builder) (if executor (.executor builder ^Executor (px/resolve-executor executor)) builder) (if keepalive (.expireAfterAccess builder ^Duration (ct/duration keepalive)) builder) + (if expire (.expireAfterWrite builder ^Duration (ct/duration expire)) builder) (if (int? max-size) (.maximumSize builder (long max-size)) builder) (.recordStats builder) - (.buildAsync builder)) - cache (.synchronous ^AsyncCache cache)] + (.build builder))] (reify ICache (get [_ k] @@ -69,7 +71,7 @@ (invalidate! [_] (.invalidateAll ^Cache cache)) (invalidate! [_ k] - (.invalidateAll ^Cache cache ^Object k)) + (.invalidate ^Cache cache ^Object k)) ICacheStats (stats [_]