fix(09): WR-14 let logout revoke an expired token that is still refreshable and always clear the cookie
This commit is contained in:
@@ -576,3 +576,57 @@ func TestLoginAmbiguousIdentifier(t *testing.T) {
|
||||
t.Fatalf("admin B by login = %d %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminLogoutRevokesExpiredRefreshableToken pins WR-14: a token whose
|
||||
// access lifetime has passed but whose refresh window is open can still be
|
||||
// revoked through logout (the guard would reject it with 401 before any
|
||||
// handler), so a leaked copy cannot be refreshed afterwards. An unusable token
|
||||
// is a 401 that still clears the session cookie.
|
||||
func TestAdminLogoutRevokesExpiredRefreshableToken(t *testing.T) {
|
||||
gdb := adminGorm(t)
|
||||
h := adminHandler(t, gdb, nil)
|
||||
user := insertAdmin(t, gdb, "stale", "stale@example.test", adminTestPassword, true, false)
|
||||
token, jti, err := bouncer.MintAudience(adminTestSecret, strconv.FormatUint(uint64(user.ID), 10), "https://app.test/backend/api/v1/auth/login", -time.Minute, bouncer.AudienceBackend)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, _, _, err := bouncer.VerifyClaimsAudience(token, adminTestSecret, bouncer.AudienceBackend); err == nil {
|
||||
t.Fatal("fixture token is not expired")
|
||||
}
|
||||
if me := postAuth(t, h, http.MethodGet, adminAPI("/auth/me"), token, nil); me.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expired token on a guarded route = %d, want 401", me.Code)
|
||||
}
|
||||
|
||||
out := postAuth(t, h, http.MethodPost, adminAPI("/auth/logout"), token, nil)
|
||||
if out.Code != http.StatusOK {
|
||||
t.Fatalf("logout of an expired refreshable token = %d %s", out.Code, out.Body.String())
|
||||
}
|
||||
var n int
|
||||
if err := gdb.Raw(`SELECT COUNT(*) FROM backend_jwt_blacklist WHERE jti = ?`, jti).Scan(&n).Error; err != nil || n != 1 {
|
||||
t.Fatalf("blacklist rows for the expired token = %d, %v; want 1", n, err)
|
||||
}
|
||||
if again := postAuth(t, h, http.MethodPost, adminAPI("/auth/refresh"), token, nil); again.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("refresh after logout = %d %s, want 401", again.Code, again.Body.String())
|
||||
}
|
||||
|
||||
// Not a token at all is refused, and the cookie is cleared either way.
|
||||
cookie := &http.Cookie{Name: cabana.AdminCookieName, Value: "not-a-token"}
|
||||
for name, rec := range map[string]*httptest.ResponseRecorder{
|
||||
"garbage bearer": postAuth(t, h, http.MethodPost, adminAPI("/auth/logout"), "not-a-token", nil),
|
||||
"garbage cookie": phase10Send(t, h, http.MethodPost, adminAPI("/auth/logout"), nil, cookie, true),
|
||||
"no token": phase10Send(t, h, http.MethodPost, adminAPI("/auth/logout"), nil, nil, true),
|
||||
} {
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("%s logout = %d, want 401", name, rec.Code)
|
||||
}
|
||||
cleared := false
|
||||
for _, c := range rec.Result().Cookies() {
|
||||
if c.Name == cabana.AdminCookieName && c.MaxAge < 0 {
|
||||
cleared = true
|
||||
}
|
||||
}
|
||||
if !cleared {
|
||||
t.Fatalf("%s logout did not clear the session cookie", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user