Summary
When REDIS_URL is set, a username/password login is stored in Redis with a 1 hour TTL that is never refreshed, so users are signed out roughly an hour after logging in regardless of activity. With Redis disabled the same login mints a JWT with no expiresIn and never expires.
Cause
saveAuth() in src/lib/auth.ts defaults expire = 0, and 0 is the "no expiry" sentinel:
await redis.client.set(authKey, data);
if (expire) {
await redis.client.expire(authKey, expire); // expire is 0 -> never runs
}
The intent is that no TTL is applied. But the Redis client in src/lib/redis.ts substitutes its own fallback whenever set() is called without one:
const ttl = time && time > 0 ? time : DEFAULT_TTL; // DEFAULT_TTL = 3600
so the auth key is actually written with EX 3600.
Nothing renews it either — checkAuth() only does redis.client.get(authKey), never re-expires — so the hour is absolute from login, not a sliding window. Continuous use does not extend the session.
/api/auth/sso passes 86400 explicitly and is unaffected (24h). /api/auth/login and /api/2fa/verify both call saveAuth(data) with no expiry and get 1 hour.
Steps to reproduce
- Set
REDIS_URL and start Umami.
- Log in with username and password.
- Find the session key and check its TTL:
redis-cli --scan --pattern 'auth:*'
redis-cli TTL auth:<key>
It returns 3600 and counts down.
- Keep using the app. The TTL keeps counting down and is never reset.
- When it reaches 0 the next API request returns 401 and the app redirects to
/login.
Expected vs. actual
Expected: the session persists until logout or a password change — matching both the expire = 0 contract in saveAuth() and the behavior when Redis is disabled.
Actual: hard logout roughly one hour after login, no matter how actively the app is being used.
Environment
- Umami v3.4.0 (
dev, 71f488f)
- Self-hosted, PostgreSQL, Redis enabled
- Affects any deployment with
REDIS_URL set; especially noticeable when running several tenants, since each one signs you out on its own hourly clock
Notes
The DEFAULT_TTL fallback in set() was added in 8530329 ("deprecate @umami/redis-client, add redis lib"). The expire = 0 / if (expire) contract in saveAuth() predates it, going back to ede6587 (2023), which is why the two no longer line up.
I have a fix ready and will open a PR against dev.
Summary
When
REDIS_URLis set, a username/password login is stored in Redis with a 1 hour TTL that is never refreshed, so users are signed out roughly an hour after logging in regardless of activity. With Redis disabled the same login mints a JWT with noexpiresInand never expires.Cause
saveAuth()insrc/lib/auth.tsdefaultsexpire = 0, and0is the "no expiry" sentinel:The intent is that no TTL is applied. But the Redis client in
src/lib/redis.tssubstitutes its own fallback wheneverset()is called without one:so the auth key is actually written with
EX 3600.Nothing renews it either —
checkAuth()only doesredis.client.get(authKey), never re-expires — so the hour is absolute from login, not a sliding window. Continuous use does not extend the session./api/auth/ssopasses86400explicitly and is unaffected (24h)./api/auth/loginand/api/2fa/verifyboth callsaveAuth(data)with no expiry and get 1 hour.Steps to reproduce
REDIS_URLand start Umami.3600and counts down./login.Expected vs. actual
Expected: the session persists until logout or a password change — matching both the
expire = 0contract insaveAuth()and the behavior when Redis is disabled.Actual: hard logout roughly one hour after login, no matter how actively the app is being used.
Environment
dev, 71f488f)REDIS_URLset; especially noticeable when running several tenants, since each one signs you out on its own hourly clockNotes
The
DEFAULT_TTLfallback inset()was added in 8530329 ("deprecate @umami/redis-client, add redis lib"). Theexpire = 0/if (expire)contract insaveAuth()predates it, going back to ede6587 (2023), which is why the two no longer line up.I have a fix ready and will open a PR against
dev.