fix(auth): recover refresh rotations and configure session lifetime

This commit is contained in:
2026-10-15 17:22:03 +08:00
parent 60ff16bcdd
commit 7aa540af04
10 changed files with 295 additions and 41 deletions
+71 -5
View File
@@ -107,11 +107,11 @@ func TestAccessRefreshLifecycle(t *testing.T) {
if cookieValue(fresh, "dsh_refresh") == cookieValue(cs, "dsh_refresh") {
t.Fatal("not rotated")
}
if w = req("POST", "/api/refresh", "", csrf, cs); w.Code != 409 {
t.Fatal("replay accepted", w.Code)
if w = req("POST", "/api/refresh", "", csrf, cs); w.Code != 200 {
t.Fatal("retry rejected", w.Code)
}
if len(w.Result().Cookies()) != 0 || !strings.Contains(w.Body.String(), "refresh_conflict") {
t.Fatal("conflict must not issue credentials", w.Header(), w.Body.String())
if cookieValue(w.Result().Cookies(), "dsh_refresh") != cookieValue(fresh, "dsh_refresh") {
t.Fatal("retry must return the same successor")
}
if w = req("GET", "/api/me", "", "", fresh); w.Code != 200 {
t.Fatal(w.Code)
@@ -126,6 +126,72 @@ func TestAccessRefreshLifecycle(t *testing.T) {
t.Fatal("logout refresh valid")
}
}
func TestPersistentCookiesRecoverAfterBrowserRestart(t *testing.T) {
a, req := authFixture(t)
w := req("POST", "/api/login", `{"username":"alice","password":"password-123"}`, "", nil)
if w.Code != 200 {
t.Fatal(w.Code)
}
cookies := w.Result().Cookies()
sid := cookieValue(cookies, "dsh_session")
var expires int64
if err := a.s.DB.QueryRow("SELECT expires FROM sessions WHERE id=?", sid).Scan(&expires); err != nil {
t.Fatal(err)
}
var restored []*http.Cookie
for _, c := range cookies {
if c.MaxAge <= 0 || c.Expires.IsZero() {
t.Fatalf("nonpersistent cookie: %s", c.Name)
}
if c.Name == "dsh_access" {
continue
} // Access has expired while backgrounded.
if c.Expires.Unix() != expires {
t.Fatalf("wrong expiry: %s", c.Name)
}
restored = append(restored, c)
}
if w = req("GET", "/api/me", "", "", restored); w.Code != 401 {
t.Fatal(w.Code)
}
// Simulate an older session without sleeping; rotation must not reset its TTL.
expires = time.Now().Add(time.Hour).Unix()
if _, err := a.s.DB.Exec("UPDATE sessions SET expires=? WHERE id=?", expires, sid); err != nil {
t.Fatal(err)
}
w = req("POST", "/api/refresh", "", cookieValue(restored, "dsh_csrf"), restored)
if w.Code != 200 {
t.Fatal(w.Code, w.Body.String())
}
fresh := w.Result().Cookies()
for _, c := range fresh {
if c.Name == "dsh_access" {
if c.MaxAge != int(accessLifetime.Seconds()) {
t.Fatal("access lifetime changed")
}
continue
}
if c.Expires.Unix() != expires || c.MaxAge <= 0 || c.MaxAge > 3600 {
t.Fatalf("session extended: %s", c.Name)
}
}
if w = req("GET", "/api/me", "", "", fresh); w.Code != 200 {
t.Fatal(w.Code)
}
w = req("POST", "/api/logout", "", cookieValue(fresh, "dsh_csrf"), fresh)
if w.Code != 200 {
t.Fatal(w.Code)
}
for _, c := range w.Result().Cookies() {
if c.MaxAge >= 0 {
t.Fatalf("cookie not cleared: %s", c.Name)
}
}
if w = req("GET", "/api/me", "", "", fresh); w.Code != 401 {
t.Fatal("revoked session accepted")
}
}
func TestAccountLockAndPersistentKey(t *testing.T) {
a, req := authFixture(t)
for i := 0; i < 5; i++ {
@@ -198,7 +264,7 @@ func TestRefreshOriginConcurrentRotationAndJWTBinding(t *testing.T) {
for code := range codes {
counts[code]++
}
if counts[200] != 1 || counts[409] != 1 {
if counts[200] != 2 {
t.Fatal(counts)
}
b := New(a.s, a.root)