Add refresh-tokens module: JWT refresh token rotation, token families and reuse detection on Spring Security 7.1
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6XzpJxZ25cDyuw1Uk
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package com.ankurm.refresh;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
|
||||
/** Output 07: logout on one device, logout everywhere, and the access token that outlives both. */
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@Import(MutableClock.Config.class)
|
||||
class LogoutTest extends FlowSupport {
|
||||
|
||||
int logout(String refreshToken) throws Exception {
|
||||
return mvc.perform(post("/api/auth/logout").contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"refresh_token\":\"%s\"}".formatted(refreshToken))).andReturn().getResponse().getStatus();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutOneDeviceThenEverywhere() throws Exception {
|
||||
Transcript t = new Transcript("07-logout");
|
||||
Tokens phone = login("alice");
|
||||
Tokens laptop = login("alice");
|
||||
Tokens tablet = login("alice");
|
||||
Tokens bob = login("bob");
|
||||
t.line("alice is logged in on phone, laptop and tablet (3 families); bob on one device.");
|
||||
|
||||
t.line("POST /api/auth/logout with the phone's refresh token: HTTP %d", logout(phone.refresh()));
|
||||
t.line(" phone refresh: HTTP %d", refreshRaw(phone.refresh()).getResponse().getStatus());
|
||||
Tokens laptop2 = refresh(laptop.refresh());
|
||||
t.line(" laptop refresh: HTTP 200");
|
||||
|
||||
t.line("POST /api/auth/logout with a made-up token: HTTP %d (same answer, no oracle)", logout("not-a-real-token"));
|
||||
|
||||
MvcResult all = mvc.perform(post("/api/auth/logout-all").header("Authorization", "Bearer " + laptop2.access())).andReturn();
|
||||
t.line("POST /api/auth/logout-all with the laptop's access token: HTTP %d %s",
|
||||
all.getResponse().getStatus(), all.getResponse().getContentAsString());
|
||||
t.line(" laptop refresh: HTTP %d", refreshRaw(laptop2.refresh()).getResponse().getStatus());
|
||||
t.line(" tablet refresh: HTTP %d", refreshRaw(tablet.refresh()).getResponse().getStatus());
|
||||
int bobRefresh = refreshRaw(bob.refresh()).getResponse().getStatus();
|
||||
t.line(" bob refresh: HTTP %d (untouched)", bobRefresh);
|
||||
int stillValid = me(laptop2.access());
|
||||
t.line("alice's laptop ACCESS token on /api/me after logout-all: HTTP %d", stillValid);
|
||||
t.line(" (it is a signed JWT with 5 minutes left; nothing in the request path asked the store)");
|
||||
|
||||
assertThat(all.getResponse().getStatus()).isEqualTo(200);
|
||||
assertThat(bobRefresh).isEqualTo(200);
|
||||
assertThat(stillValid).isEqualTo(200);
|
||||
t.write();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user