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