fix(security): refuse the access token of a blocked or deleted user

activeUsers cache, 30 s. Core still accepts the token until it expires; X4 (batch 4) tells Core.
This commit is contained in:
lobo committed 2026-09-30 13:41:57 +02:00
1 parent 509280da14
commit 1f94c87a20
10 files changed
+109 -12

No files matched your search

@@ -7,10 +7,12 @@ import auth_service.shared.security.TokenGenerator;
import auth_service.shared.api.AuthCookies;
import auth_service.shared.test.AbstractIntegrationTest;
import jakarta.servlet.http.Cookie;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
@@ -92,8 +94,21 @@ class RoleCheckFlowTest extends AbstractIntegrationTest {
@Autowired
private TokenGenerator tokenGenerator;
@Autowired
private JdbcTemplate jdbc;
private long userId;
// The filter accepts only the token of an existing, active user.
@BeforeEach
void seedUser() {
jdbc.update("DELETE FROM users WHERE email = 'role-check@example.com'");
userId = jdbc.queryForObject("INSERT INTO users (email, password, enabled) VALUES ('role-check@example.com', 'x', true) "
+ "RETURNING id", Long.class);
}
private String token(Role role) {
return tokenGenerator.generateToken("4711", null, role);
return tokenGenerator.generateToken(String.valueOf(userId), null, role);
}
private ResultActions call(String path, Role role) throws Exception {
@@ -156,6 +156,8 @@ class DeletedAccountFlowTest extends AbstractIntegrationTest {
MockHttpServletResponse session = login();
String access = session.getCookie(AuthCookies.ACCESS_COOKIE).getValue();
String refresh = session.getCookie(AuthCookies.REFRESH_COOKIE).getValue();
mockMvc.perform(get("/api/users/me").cookie(new Cookie(AuthCookies.ACCESS_COOKIE, access)))
.andExpect(status().isOk());
long usersBefore = totalUsers();
MockHttpServletResponse deleted = deleteAccount(access, PASSWORD).andExpect(status().isNoContent())
@@ -168,6 +170,8 @@ class DeletedAccountFlowTest extends AbstractIntegrationTest {
.cookie(new Cookie(AuthCookies.REFRESH_COOKIE, refresh)))
.andExpect(status().isUnauthorized());
assertThat(login().getStatus()).isEqualTo(401);
mockMvc.perform(get("/api/users/me").cookie(new Cookie(AuthCookies.ACCESS_COOKIE, access)))
.andExpect(status().isUnauthorized());
mockMvc.perform(get("/api/public/profiles/" + NAME)).andExpect(status().isNotFound());
assertThat(totalUsers()).isEqualTo(usersBefore - 1);
}
@@ -169,9 +169,12 @@ class AdminFlowTest extends AbstractIntegrationTest {
void block_EndsTheSessionsAndTheLoginTellsWhy() throws Exception {
Session admin = Session.of(login(ADMIN));
Session player = Session.of(login(PLAYER));
assertThat(me(player)).isEqualTo(200);
block(admin, playerId, true).andExpect(status().isNoContent());
assertThat(me(player)).isEqualTo(401);
assertThat(login(PLAYER).getContentAsString()).contains("ACCOUNT_BLOCKED");
assertThat(login(PLAYER).getStatus()).isEqualTo(403);
assertThat(mockMvc.perform(post("/api/auth/refresh").header(ClientHeaderFilter.CLIENT_HEADER, "web")
@@ -181,11 +184,32 @@ class AdminFlowTest extends AbstractIntegrationTest {
block(admin, playerId, false).andExpect(status().isNoContent());
assertThat(login(PLAYER).getStatus()).isEqualTo(200);
assertThat(me(player)).isEqualTo(200);
}
@Test
void blockedCookieNextToAValidBearer_UsesTheBearer() throws Exception {
Session admin = Session.of(login(ADMIN));
Session player = Session.of(login(PLAYER));
block(admin, playerId, true).andExpect(status().isNoContent());
mockMvc.perform(get("/api/users/me")
.cookie(new Cookie(AuthCookies.ACCESS_COOKIE, player.access()))
.header("Authorization", "Bearer " + admin.access()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value(ADMIN));
}
private int me(Session session) throws Exception {
return mockMvc.perform(get("/api/users/me").cookie(new Cookie(AuthCookies.ACCESS_COOKIE, session.access())))
.andReturn().getResponse().getStatus();
}
@Test
void delete_FlagsTheUser() throws Exception {
Session admin = Session.of(login(ADMIN));
Session player = Session.of(login(PLAYER));
assertThat(me(player)).isEqualTo(200);
mockMvc.perform(delete("/api/admin/users/" + playerId).header(ClientHeaderFilter.CLIENT_HEADER, "web")
.cookie(new Cookie(AuthCookies.ACCESS_COOKIE, admin.access())))
@@ -193,6 +217,7 @@ class AdminFlowTest extends AbstractIntegrationTest {
assertThat(jdbc.queryForObject("SELECT deleted_at FROM users WHERE id = ?", Object.class, playerId)).isNotNull();
assertThat(login(PLAYER).getStatus()).isEqualTo(401);
assertThat(me(player)).isEqualTo(401);
assertThat(audits("DELETE", playerId)).isEqualTo(1);
}