Repository navigation
Conversation
…its password changes Session JWTs live for seven days and were never re-checked against the account, so deactivating, deleting or demoting a user, or resetting their password, had no effect until the token expired. - Add services/session-revocation: every verified session token is checked against the current account row (exists, active, no role it has lost, password fingerprint unchanged, impersonating admin still an active admin). - A global onRequest guard removes revoked tokens from the Authorization header and auth cookie, so every route treats the request as anonymous. Results are cached per token for 5 seconds. - The Yjs WebSocket verifies its query token through the same check. - Login tokens carry pwh, a keyed fingerprint of the password hash; changing your own password re-signs the current session. - CAS and OpenID logins refuse deactivated accounts. - The yjs-debug ws-url token copies the caller's claims and never outlives the caller's token.
Deploying exelearning with
|
| Latest commit: |
9a0c762
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4e562b97.exelearning.pages.dev |
| Branch Preview URL: | https://hotfix-session-revocation.exelearning.pages.dev |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2594 +/- ##
========================================
Coverage 92.25% 92.26%
========================================
Files 479 480 +1
Lines 89517 89626 +109
========================================
+ Hits 82584 82690 +106
- Misses 6933 6936 +3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Bundle ReportBundle size has no change ✅ |
The deactivated-account test built the callback request by hand, from before main required the oidc_state cookie and a matching nonce, so the handler now rejects it with 400 before it ever looks the account up.
This branch has not been deployed
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.
Problem
Session JWTs are valid for seven days, and every route trusted their claims without looking at the account again. As a result:
ROLE_ADMINleft the admin panel reachable with the old token;is_active);GET /api/yjs/debug/:projectUuid/ws-urlminted a fresh token from any valid token, so a session could be renewed indefinitely.How other projects handle it
refreshUser/hasUserChanged), Laravel session guardget_session_auth_hash), LaravelAuthenticateSessiontoken_version/password_changed_atcolumn compared with the tokenThis PR combines the first two: no migration, and it covers deletion, deactivation, demotion and password changes with one check.
Changes
src/services/session-revocation.ts:getSessionRevocationReason(payload)rejects a token when any of these holds:pwhno longer matches the current password hash;sessionRevocationGuard()is mounted once insrc/index.ts, after the static-file hook and before the maintenance check. It removes a revoked token from theAuthorizationheader or theauthcookie before any route runs, so all ~15 existing verification sites (route plugins, API v1, maintenance bypass) treat the request as anonymous with no per-route changes. Forged or expired tokens are left for the routes to reject as before.verifySessionToken()is now the Yjs WebSocket's default verifier (its token travels in the query string, not in a header).pwh, an HMAC of the stored password hash keyed with the JWT secret. It reveals nothing about the hash.PATCH /api/user/passwordre-signs the caller's own session with the new fingerprint, so changing your own password does not log you out (as Django'supdate_session_auth_hashdoes). Other sessions are revoked.ws-urltoken copies the caller's claims (isGuest,authMethod,pwh) instead of hard-coding them, and its expiry is capped at the caller's ownexp.Compatibility
pwh. They are still checked for account existence, active state and roles, but a password change does not revoke them; they expire within seven days. CLI tokens fromjwt:generatebehave the same way, but--raw-subtokens with an emailsubare now rejected.pwhon purpose, so changing your own password does not break the editor's reconnects.Not covered (follow-up)
Tests
src/services/session-revocation.spec.ts: 32 cases at 100% coverage of the new module. They cover:withJwtAuth/requireAuth/requireAdmin(deactivated → 401, demoted admin → 401 on admin routes);auth.spec.ts: login token carriespwh; CAS and OpenID refuse deactivated accounts.user.spec.ts: a password change re-signs the session with the new fingerprint.admin.spec.ts: the impersonation token carries the target'spwhandimpersonatedBy.yjs-debug.spec.ts: thews-urltoken never outlives the caller and copies its claims.yjs-websocket.spec.ts: the default WebSocket verifier goes through the revocation check.Verification
make fix: exit 0; the 44 warnings were already there before this change, none in changed lines.make test-unit: exit 0, 9235 pass, 0 fail.make test-integration: exit 0, 770 pass, 0 fail.bun src/index.ts, SQLite) with the same cookie: logged-in session returns 200; after deactivating the user,/api/auth/userwith the cookie and/api/v1/projectswith the Bearer token both return 401 while/loginstill renders (200); after re-activating, 200 again; after changing the stored password hash, 401.