feat(jwt): session lifetime caps (inactivity + absolute family cap)#89
Merged
Conversation
JWT sessions in 4.4.x were effectively unlimited — each /jwt/refresh issued a fresh refresh token with expires_at = now + 7d, and the 15-min access TTL meant any page activity rotated the refresh token forward indefinitely. Violates OWASP ASVS V3 absolute-timeout requirements and is the wrong default for admin / regulated-industry deployments. Two new knobs: - KYTE_JWT_REFRESH_TTL default lowered from 7d to 4h (inactivity) - KYTE_JWT_FAMILY_MAX_LIFETIME (new, default 12h) caps absolute session length from the original /jwt/login moment, independent of activity. Enforced in RefreshTokenStore::rotate() — when crossed, whole token family is revoked with revoked_reason='family_max_lifetime'. New column KyteRefreshToken.family_started_at anchors the absolute cap to the original login moment, copied forward unchanged on each rotation so sliding refresh cannot extend it. Backward compatibility: pre-upgrade tokens with family_started_at=0 are treated as "legacy, uncapped" on first post-upgrade rotation and the successor anchors the cap from that moment forward — no mass logout at deploy. Schema migration: migrations/4.5.0_jwt_family_lifetime.sql (ALTER TABLE adding the new column). Same operational pattern as the 4.4.0 JWT migration. Tests: RefreshTokenStoreTest covers (a) family_started_at set on issue, (b) preserved across rotation, (c) cap rejection past window, (d) cap allows refresh inside window, (e) zero-anchor legacy backfill. Defaults align with AWS Console (12h max), Microsoft 365 admin (1h/12h), and OWASP ASVS V3. Customer mobile/consumer apps that need longer sessions can override both constants per-deployment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The initial 4.5.0 cap logic gave tokens with family_started_at=0 a free pass on first rotation, anchoring the cap to "now". Side effect: pre-upgrade sessions kept their original 7-day inactivity TTL AND escaped the 12h absolute cap, so they survived uncapped for up to a week after deploy. Observed on dev — staging Shipyard and getpage.co apps stayed logged in ~7 days while a freshly-logged-in localhost (post-config-change, 4h tokens) expired correctly. Fix: when family_started_at=0, anchor the absolute cap to the token's date_created (best proxy for original login) instead of exempting it. A legacy session older than KYTE_JWT_FAMILY_MAX_LIFETIME is now revoked on its next rotation, exactly like a native session. First deploy of 4.5.0 therefore forces re-login for any JWT session older than 12h — intentional, and the correct security posture for an absolute cap. CHANGELOG updated to document the deploy-time logout, the config.php KYTE_JWT_REFRESH_TTL override gotcha (explicit 604800 shadows the new 4h default), the optional hard-cutover purge SQL, and the kyte-api-js >= 2.0.2 client pairing requirement. Tests: replace the lenient backfill test with two cases — legacy token within cap anchors to date_created; legacy token past cap is revoked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
KYTE_JWT_REFRESH_TTLdefault 7d → 4h (inactivity timeout).KYTE_JWT_FAMILY_MAX_LIFETIME(default 12h) — absolute cap from original login, enforced inRefreshTokenStore::rotate().KyteRefreshToken.family_started_atanchors the cap; migrationmigrations/4.5.0_jwt_family_lifetime.sql.date_created), not exempted — deploy forces re-login for sessions older than 12h.Test plan
RefreshTokenStoreTest: anchor set on issue, preserved across rotation, cap rejection past window, allowed inside window, legacy-within-cap anchors to date_created, legacy-past-cap revokedRollout notes (in CHANGELOG)
KYTE_JWT_REFRESH_TTL=604800shadows the new default — update to 14400 on upgrade🤖 Generated with Claude Code