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(); } }