From f9294f263443227f3e97467eab46e210d215eb2b Mon Sep 17 00:00:00 2001
From: Hristo Kapanakov
Date: Fri, 18 Jul 2025 21:02:21 +0300
Subject: [PATCH] feat: Allow using OICD auth cache instead of session
---
.../installation/backend-config.md | 1 +
mealie/core/settings/settings.py | 1 +
mealie/routes/auth/auth.py | 7 ++-
mealie/routes/auth/auth_cache.py | 55 +++++++++++++++++++
4 files changed, 63 insertions(+), 1 deletion(-)
create mode 100644 mealie/routes/auth/auth_cache.py
diff --git a/docs/docs/documentation/getting-started/installation/backend-config.md b/docs/docs/documentation/getting-started/installation/backend-config.md
index 2fd34f973..638be4cd4 100644
--- a/docs/docs/documentation/getting-started/installation/backend-config.md
+++ b/docs/docs/documentation/getting-started/installation/backend-config.md
@@ -113,6 +113,7 @@ For usage, see [Usage - OpenID Connect](../authentication/oidc-v2.md)
| OIDC_GROUPS_CLAIM | groups | Optional if not using `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP`. This is the claim Mealie will request from your IdP and will use to compare to `OIDC_USER_GROUP` or `OIDC_ADMIN_GROUP` to allow the user to log in to Mealie or is set as an admin. **Your IdP must be configured to grant this claim** |
| OIDC_SCOPES_OVERRIDE | None | Advanced configuration used to override the scopes requested from the IdP. **Most users won't need to change this**. At a minimum, 'openid profile email' are required. |
| OIDC_TLS_CACERTFILE | None | File path to Certificate Authority used to verify server certificate (e.g. `/path/to/ca.crt`) |
+| OIDC_USE_AUTH_CACHE | False | If `True`, OIDC authentication will use server cache instead of session to store its temporary data. |
### OpenAI
diff --git a/mealie/core/settings/settings.py b/mealie/core/settings/settings.py
index 7af9d481a..05822c7b6 100644
--- a/mealie/core/settings/settings.py
+++ b/mealie/core/settings/settings.py
@@ -338,6 +338,7 @@ class AppSettings(AppLoggingSettings):
OIDC_GROUPS_CLAIM: str | None = "groups"
OIDC_SCOPES_OVERRIDE: str | None = None
OIDC_TLS_CACERTFILE: str | None = None
+ OIDC_USE_AUTH_CACHE: bool = False
@property
def OIDC_REQUIRES_GROUP_CLAIM(self) -> bool:
diff --git a/mealie/routes/auth/auth.py b/mealie/routes/auth/auth.py
index 2e5b66174..6e523c4a0 100644
--- a/mealie/routes/auth/auth.py
+++ b/mealie/routes/auth/auth.py
@@ -19,6 +19,8 @@ from mealie.routes._base.routers import UserAPIRouter
from mealie.schema.user import PrivateUser
from mealie.schema.user.auth import CredentialsRequestForm
+from .auth_cache import AuthCache
+
public_router = APIRouter(tags=["Users: Authentication"])
user_router = UserAPIRouter(tags=["Users: Authentication"])
logger = root_logger.get_logger("auth")
@@ -27,7 +29,10 @@ remember_me_duration = timedelta(days=14)
settings = get_app_settings()
if settings.OIDC_READY:
- oauth = OAuth()
+ cache = None
+ if settings.OIDC_USE_AUTH_CACHE:
+ cache = AuthCache()
+ oauth = OAuth(cache=cache)
scope = None
if settings.OIDC_SCOPES_OVERRIDE:
scope = settings.OIDC_SCOPES_OVERRIDE
diff --git a/mealie/routes/auth/auth_cache.py b/mealie/routes/auth/auth_cache.py
new file mode 100644
index 000000000..dc4040dcf
--- /dev/null
+++ b/mealie/routes/auth/auth_cache.py
@@ -0,0 +1,55 @@
+import time
+
+try:
+ import cPickle as pickle
+except ImportError:
+ import pickle
+
+
+class AuthCache:
+ def __init__(self, threshold=500, default_timeout=300):
+ self.default_timeout = default_timeout
+ self._cache = {}
+ self.clear = self._cache.clear
+ self._threshold = threshold
+
+ def _prune(self):
+ if len(self._cache) > self._threshold:
+ now = time.time()
+ toremove = []
+ for idx, (key, (expires, _)) in enumerate(self._cache.items()):
+ if (expires != 0 and expires <= now) or idx % 3 == 0:
+ toremove.append(key)
+ for key in toremove:
+ self._cache.pop(key, None)
+
+ def _normalize_timeout(self, timeout):
+ if timeout is None:
+ timeout = self.default_timeout
+ if timeout > 0:
+ timeout = time.time() + timeout
+ return timeout
+
+ async def get(self, key):
+ try:
+ expires, value = self._cache[key]
+ if expires == 0 or expires > time.time():
+ return pickle.loads(value)
+ except (KeyError, pickle.PickleError):
+ return None
+
+ async def set(self, key, value, timeout=None):
+ expires = self._normalize_timeout(timeout)
+ self._prune()
+ self._cache[key] = (expires, pickle.dumps(value, pickle.HIGHEST_PROTOCOL))
+ return True
+
+ async def delete(self, key):
+ return self._cache.pop(key, None) is not None
+
+ async def has(self, key):
+ try:
+ expires, value = self._cache[key]
+ return expires == 0 or expires > time.time()
+ except KeyError:
+ return False
\ No newline at end of file