fix(auth): recover refresh rotations and configure session lifetime
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user