Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b6XzpJxZ25cDyuw1Uk
57 lines
2.8 KiB
Java
57 lines
2.8 KiB
Java
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();
|
|
}
|
|
}
|