Sessions not invalidated on password/role change or deactivation #4

Open
opened 2026-04-02 11:20:25 +02:00 by marcel · 5 comments
Owner

Problem

When a user changes their password, or an admin changes a user's systemRole or deactivates their account, existing sessions remain valid. A compromised session stays alive indefinitely. An admin demoting someone from admin to user doesn't revoke their existing admin-level session.

Affected files

  • AuthService.java:64-83 — password change does not invalidate sessions
  • AdminService.java:96-100 — role change does not invalidate sessions
  • AdminService.java:102-105 — deactivation does not invalidate sessions

Attack scenario

  1. Admin discovers a compromised account and deactivates it → attacker's existing session still works.
  2. Admin demotes a rogue admin to user → their session still has ROLE_ADMIN until it naturally expires.

Invalidate all sessions for the affected user on:

  • Password change (self-service or admin reset)
  • systemRole change
  • Account deactivation

Use Spring Session's FindByIndexNameSessionRepository or maintain a session registry to look up sessions by principal.

Severity

High — privilege changes and account deactivation have no immediate effect on active sessions.

## Problem When a user changes their password, or an admin changes a user's `systemRole` or deactivates their account, existing sessions remain valid. A compromised session stays alive indefinitely. An admin demoting someone from `admin` to `user` doesn't revoke their existing admin-level session. ## Affected files - `AuthService.java:64-83` — password change does not invalidate sessions - `AdminService.java:96-100` — role change does not invalidate sessions - `AdminService.java:102-105` — deactivation does not invalidate sessions ## Attack scenario 1. Admin discovers a compromised account and deactivates it → attacker's existing session still works. 2. Admin demotes a rogue admin to `user` → their session still has `ROLE_ADMIN` until it naturally expires. ## Recommended fix Invalidate all sessions for the affected user on: - Password change (self-service or admin reset) - `systemRole` change - Account deactivation Use Spring Session's `FindByIndexNameSessionRepository` or maintain a session registry to look up sessions by principal. ## Severity High — privilege changes and account deactivation have no immediate effect on active sessions.
marcel added the kind/securitypriority/high labels 2026-04-02 11:20:58 +02:00
Author
Owner

👨‍💻 Kai — Frontend Engineer

This is a backend concern, but it surfaces meaningfully in the frontend experience — here's what I need to handle.

Frontend implications of session invalidation:

  • When a user's session is invalidated server-side (password change, role change, deactivation), their next request to a +page.server.ts load function will get a 401 (or be redirected to login). SvelteKit's hooks.server.ts should be the place that catches this and redirects cleanly — I need to confirm that path is already handled, or add it.
  • The affected user's browser currently has a stale session cookie. After invalidation, they should see the login page with a clear message — not a silent error or broken state. Do we have a standard "your session has expired" message pattern already?

Questions:

  • When the backend invalidates a session, does the response include a Set-Cookie that clears the JSESSIONID, or does it just invalidate server-side? The client cookie will persist until it expires or is cleared — is there a signal we can rely on to detect this on the frontend?
  • For the password change flow specifically: after a successful password change, should the current user stay logged in (current session preserved, others invalidated) or be redirected to login? This affects the UX flow I design for the password change confirmation screen.
  • For the deactivation case: the admin who deactivates a user should see immediate feedback that the action is effective now. Is there any UI element that shows active session status?

No blockers on my end — just want to design the right post-invalidation UX flow before implementing it.

## 👨‍💻 Kai — Frontend Engineer This is a backend concern, but it surfaces meaningfully in the frontend experience — here's what I need to handle. **Frontend implications of session invalidation:** - When a user's session is invalidated server-side (password change, role change, deactivation), their next request to a `+page.server.ts` load function will get a 401 (or be redirected to login). SvelteKit's `hooks.server.ts` should be the place that catches this and redirects cleanly — I need to confirm that path is already handled, or add it. - The affected user's browser currently has a stale session cookie. After invalidation, they should see the login page with a clear message — not a silent error or broken state. Do we have a standard "your session has expired" message pattern already? **Questions:** - When the backend invalidates a session, does the response include a `Set-Cookie` that clears the `JSESSIONID`, or does it just invalidate server-side? The client cookie will persist until it expires or is cleared — is there a signal we can rely on to detect this on the frontend? - For the password change flow specifically: after a successful password change, should the current user stay logged in (current session preserved, others invalidated) or be redirected to login? This affects the UX flow I design for the password change confirmation screen. - For the deactivation case: the admin who deactivates a user should see immediate feedback that the action is effective now. Is there any UI element that shows active session status? **No blockers on my end** — just want to design the right post-invalidation UX flow before implementing it.
Author
Owner

🏗️ Backend Engineer — Spring Boot / PostgreSQL Specialist

This is the right fix and it's non-trivial to implement correctly. Let me lay out the options clearly.

The core problem: Spring Security's default InMemorySessionRepository doesn't support lookup by principal name. To invalidate all sessions for a given user, we need a session store that indexes by principal.

Implementation options:

  1. Spring Session with JDBC backend — store sessions in a spring_session table. FindByIndexNameSessionRepository lets you look up all sessions by PRINCIPAL_NAME_INDEX_NAME. This is the cleanest approach and makes sessions durable across restarts. Adds a dependency (spring-session-jdbc) and requires a Flyway migration for the session tables.

  2. Spring Session with Redis — same API, faster, but adds Redis to the infrastructure. Overkill for v1 unless we already have Redis for something else.

  3. Custom in-memory session registry — register sessions manually on login in a ConcurrentHashMap<userId, Set<SessionId>> and invalidate programmatically. Simpler infra, but sessions are lost on restart and requires careful thread safety.

My recommendation for v1: Spring Session with JDBC. It's production-appropriate, uses our existing PostgreSQL, and the FindByIndexNameSessionRepository interface is exactly what this issue calls for.

Questions before implementation:

  • Do we have a Redis container in the Compose setup, or is JDBC the only viable option right now?
  • Should the password change invalidate the current session too, or only other sessions? The UX for self-service password change usually invalidates all sessions including current, then redirects to login — but that needs product sign-off.
  • Is admin-initiated password reset a different flow from self-service password change? Both paths in AuthService.java:64-83 need to be covered.

Flyway migration needed: If we go JDBC, the Spring Session schema migration needs to be added. Spring provides a standard SQL script for this — it just needs to be placed in the migrations directory with the correct version number.

## 🏗️ Backend Engineer — Spring Boot / PostgreSQL Specialist This is the right fix and it's non-trivial to implement correctly. Let me lay out the options clearly. **The core problem:** Spring Security's default `InMemorySessionRepository` doesn't support lookup by principal name. To invalidate all sessions for a given user, we need a session store that indexes by principal. **Implementation options:** 1. **Spring Session with JDBC backend** — store sessions in a `spring_session` table. `FindByIndexNameSessionRepository` lets you look up all sessions by `PRINCIPAL_NAME_INDEX_NAME`. This is the cleanest approach and makes sessions durable across restarts. Adds a dependency (`spring-session-jdbc`) and requires a Flyway migration for the session tables. 2. **Spring Session with Redis** — same API, faster, but adds Redis to the infrastructure. Overkill for v1 unless we already have Redis for something else. 3. **Custom in-memory session registry** — register sessions manually on login in a `ConcurrentHashMap<userId, Set<SessionId>>` and invalidate programmatically. Simpler infra, but sessions are lost on restart and requires careful thread safety. **My recommendation for v1:** Spring Session with JDBC. It's production-appropriate, uses our existing PostgreSQL, and the `FindByIndexNameSessionRepository` interface is exactly what this issue calls for. **Questions before implementation:** - Do we have a Redis container in the Compose setup, or is JDBC the only viable option right now? - Should the password change invalidate the *current* session too, or only *other* sessions? The UX for self-service password change usually invalidates all sessions including current, then redirects to login — but that needs product sign-off. - Is admin-initiated password reset a different flow from self-service password change? Both paths in `AuthService.java:64-83` need to be covered. **Flyway migration needed:** If we go JDBC, the Spring Session schema migration needs to be added. Spring provides a standard SQL script for this — it just needs to be placed in the migrations directory with the correct version number.
Author
Owner

🧪 QA Engineer

This is one of the harder scenarios to test because it requires multi-session state. Here's the test plan.

Unit tests:

  • These are limited here — the interesting behavior is at the integration level. Unit tests can verify that invalidateUserSessions(userId) is called with the correct ID during password change, role change, and deactivation. Use ArgumentCaptor or Mockito verify.

Integration tests — this is where the real coverage lives:

  • Password change invalidates existing sessions:

    1. Log in as User A, capture session cookie S1
    2. Change password (self-service or admin-initiated)
    3. Make a subsequent authenticated request using S1 → expect 401
    4. Log in with new password → expect 200
  • Role downgrade invalidates existing session:

    1. Log in as admin User B, capture session cookie S2
    2. Admin demotes User B from admin to user
    3. Make an admin-only request using S2 → expect 403 (if session persists with stale role) or 401 (if session invalidated)
    4. Confirm which behavior is intended and test for it explicitly
  • Deactivation invalidates existing session:

    1. Log in as User C, capture session cookie S3
    2. Admin deactivates User C
    3. Make an authenticated request using S3 → expect 401
  • Current session preservation (if applicable):
    If self-service password change preserves the current session, verify that only other sessions are invalidated, not the one that made the change request.

Edge cases:

  • User with no active sessions gets their password changed → no error, graceful no-op
  • User simultaneously logged in on multiple devices → all sessions invalidated
  • Session lookup by principal name — does it handle users with multiple concurrent sessions correctly?

Test infrastructure note: These tests require a session store that supports multi-session lookup (i.e., Spring Session JDBC or similar). The current InMemorySessionRepository likely can't support these integration tests either — the infra choice and the test strategy are coupled.

## 🧪 QA Engineer This is one of the harder scenarios to test because it requires multi-session state. Here's the test plan. **Unit tests:** - These are limited here — the interesting behavior is at the integration level. Unit tests can verify that `invalidateUserSessions(userId)` is called with the correct ID during password change, role change, and deactivation. Use `ArgumentCaptor` or Mockito verify. **Integration tests — this is where the real coverage lives:** - **Password change invalidates existing sessions:** 1. Log in as User A, capture session cookie S1 2. Change password (self-service or admin-initiated) 3. Make a subsequent authenticated request using S1 → expect `401` 4. Log in with new password → expect `200` - **Role downgrade invalidates existing session:** 1. Log in as admin User B, capture session cookie S2 2. Admin demotes User B from `admin` to `user` 3. Make an admin-only request using S2 → expect `403` (if session persists with stale role) or `401` (if session invalidated) 4. Confirm which behavior is intended and test for it explicitly - **Deactivation invalidates existing session:** 1. Log in as User C, capture session cookie S3 2. Admin deactivates User C 3. Make an authenticated request using S3 → expect `401` - **Current session preservation (if applicable):** If self-service password change preserves the current session, verify that only *other* sessions are invalidated, not the one that made the change request. **Edge cases:** - User with no active sessions gets their password changed → no error, graceful no-op - User simultaneously logged in on multiple devices → all sessions invalidated - Session lookup by principal name — does it handle users with multiple concurrent sessions correctly? **Test infrastructure note:** These tests require a session store that supports multi-session lookup (i.e., Spring Session JDBC or similar). The current `InMemorySessionRepository` likely can't support these integration tests either — the infra choice and the test strategy are coupled.
Author
Owner

🔒 Sable — Security Engineer

This is one of the highest-impact issues in the backlog. Let me add precision to the threat model and fix requirements.

Why both attack scenarios are critical:

  • Scenario 1 (deactivation): An admin discovers a breach and deactivates the compromised account. Without session invalidation, that action is security theater — the attacker's session keeps working until natural expiration (which could be hours or days). This is exactly the kind of incident response failure that turns a breach into a disaster.
  • Scenario 2 (role downgrade): A rogue admin gets discovered and is demoted. Their existing session still carries ROLE_ADMIN in the SecurityContextHolder. Any role checks against the SecurityContext (not the database) will still see admin privileges until the session expires.

Fix requirements from a security perspective:

  • Invalidation must be synchronous and immediate — not eventual. By the time the admin's "deactivate user" request returns 200, the target user's sessions must already be invalid.
  • The session invalidation must cover ALL sessions for the affected user — not just the most recent one. Mobile session, desktop session, any background tab.
  • After role change invalidation, the re-authentication must reload roles from the DB. If roles are cached in the session's Authentication object, re-login must re-fetch them.
  • Invalidation events must be logged in admin_audit_log with: admin who performed the action, affected user, reason (password change / role change / deactivation), timestamp.

Session fixation note: While implementing session management changes, confirm that sessionFixation().migrateSession() (or .newSession()) is configured in SecurityFilterChain. Session fixation protection is a related concern that should be audited alongside this fix.

Question: What is the current session expiration configured to? If it's very short (e.g., 15 minutes), the urgency of this fix is slightly lower. If it's hours or persistent ("remember me"), this is urgent. Either way the fix is correct — just affects prioritization.

## 🔒 Sable — Security Engineer This is one of the highest-impact issues in the backlog. Let me add precision to the threat model and fix requirements. **Why both attack scenarios are critical:** - **Scenario 1 (deactivation):** An admin discovers a breach and deactivates the compromised account. Without session invalidation, that action is security theater — the attacker's session keeps working until natural expiration (which could be hours or days). This is exactly the kind of incident response failure that turns a breach into a disaster. - **Scenario 2 (role downgrade):** A rogue admin gets discovered and is demoted. Their existing session still carries `ROLE_ADMIN` in the `SecurityContextHolder`. Any role checks against the SecurityContext (not the database) will still see admin privileges until the session expires. **Fix requirements from a security perspective:** - Invalidation must be synchronous and immediate — not eventual. By the time the admin's "deactivate user" request returns 200, the target user's sessions must already be invalid. - The session invalidation must cover ALL sessions for the affected user — not just the most recent one. Mobile session, desktop session, any background tab. - After role change invalidation, the re-authentication must reload roles from the DB. If roles are cached in the session's `Authentication` object, re-login must re-fetch them. - Invalidation events must be logged in `admin_audit_log` with: admin who performed the action, affected user, reason (password change / role change / deactivation), timestamp. **Session fixation note:** While implementing session management changes, confirm that `sessionFixation().migrateSession()` (or `.newSession()`) is configured in `SecurityFilterChain`. Session fixation protection is a related concern that should be audited alongside this fix. **Question:** What is the current session expiration configured to? If it's very short (e.g., 15 minutes), the urgency of this fix is slightly lower. If it's hours or persistent ("remember me"), this is urgent. Either way the fix is correct — just affects prioritization.
Author
Owner

🎨 Atlas — UI/UX Designer

The security fix is clear, but the UX around it needs deliberate design — especially the moments where a user discovers their session has been terminated without warning.

UX scenarios I want to design for:

  • Self-service password change → session invalidated → redirect to login:
    The user should see a clear, non-alarming message: "Password changed successfully. Please log in with your new password." This is expected behavior — just needs the right framing so it doesn't feel like an error.

  • Account deactivated while browsing → next request fails:
    The user hits a page, gets bounced to login. They try to log in and get an error. What does that error say? "Your account has been deactivated. Contact your administrator." — clear and actionable, not a generic auth failure.

  • Role downgraded while browsing → session invalidated → redirect to login:
    Similar pattern. The message could be neutral: "Your session has ended. Please log in again." No need to expose that their role was changed — that's an admin action they'll find out about through normal interaction.

Design questions:

  • Do we have a standard "session ended" screen or message pattern in the design system? If not, this issue is a good trigger to establish one.
  • For the admin deactivation action — should the admin UI show a confirmation modal that says "This will immediately terminate all active sessions for this user"? That sets appropriate expectations and makes the action feel consequential (which it is).
  • Is there a "logged out" or "session expired" landing state designed for the login screen? Different from a first-time visit to the login page — the messaging should differ.
## 🎨 Atlas — UI/UX Designer The security fix is clear, but the UX around it needs deliberate design — especially the moments where a user discovers their session has been terminated without warning. **UX scenarios I want to design for:** - **Self-service password change → session invalidated → redirect to login:** The user should see a clear, non-alarming message: "Password changed successfully. Please log in with your new password." This is expected behavior — just needs the right framing so it doesn't feel like an error. - **Account deactivated while browsing → next request fails:** The user hits a page, gets bounced to login. They try to log in and get an error. What does that error say? "Your account has been deactivated. Contact your administrator." — clear and actionable, not a generic auth failure. - **Role downgraded while browsing → session invalidated → redirect to login:** Similar pattern. The message could be neutral: "Your session has ended. Please log in again." No need to expose that their role was changed — that's an admin action they'll find out about through normal interaction. **Design questions:** - Do we have a standard "session ended" screen or message pattern in the design system? If not, this issue is a good trigger to establish one. - For the admin deactivation action — should the admin UI show a confirmation modal that says "This will immediately terminate all active sessions for this user"? That sets appropriate expectations and makes the action feel consequential (which it is). - Is there a "logged out" or "session expired" landing state designed for the login screen? Different from a first-time visit to the login page — the messaging should differ.
Sign in to join this conversation.