fix: logout ends cookie-carried sessions, clears the session cookies and keeps the token revoked for its refresh window
POST /logout read the token from the Authorization header only, while the jwt guard also accepts the token and auth_token cookies. A session carried by a cookie (golem15.sociallogin's token_cookie sets an httpOnly one that the browser cannot turn into a bearer header) was answered 401, nothing was revoked and the cookie kept authenticating jwt.auth routes until the access token expired (golem15.user.jwt.ttl, 60 minutes). Logout never cleared a cookie either, and it and EndRequestSession kept the jwt_blacklist row only until the access token's exp, so once the sweep removed the row POST /refresh revived the logged-out token for the rest of its refresh window. classes.EndSession, which Logout now runs, revokes every session token the request carries (the bearer header, the token and auth_token cookies) that verifies, an expired one too while it could still be refreshed, until the later of exp and iat plus refresh_ttl, plus a minute, as bouncer.Refresh and the admin logout do. The answer, a 401 included, expires the auth.cookie token cookie and any other session cookie the request carries, with that cookie's path, domain, SameSite and Secure. EndRequestSession is its best-effort form. Response bodies are unchanged; an expired but still refreshable token now logs out with 200 instead of 401.
This commit is contained in:
@@ -189,10 +189,11 @@ func TestRequestUser(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestEndRequestSession pins classes.EndRequestSession (D-04, D-05) without
|
||||
// a database: the session's jti is blacklisted until the token expires, the
|
||||
// forget cookie carries the configured Domain and Path, a request without a
|
||||
// token still forgets the cookie, and a blacklist failure never fails the
|
||||
// call.
|
||||
// a database: the session's jti is blacklisted until its refresh window
|
||||
// closes (not only until the access token expires, or /refresh could revive
|
||||
// it), the forget cookie carries the configured Domain and Path, a request
|
||||
// without a token still forgets the cookie, and a blacklist failure never
|
||||
// fails the call.
|
||||
func TestEndRequestSession(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
app := sessionUserApp(t,
|
||||
@@ -234,14 +235,15 @@ func TestEndRequestSession(t *testing.T) {
|
||||
if blocked, _ := bl.IsBlacklisted(ctx, jti); !blocked {
|
||||
t.Fatalf("%s: the jti is not blacklisted", kind)
|
||||
}
|
||||
// Until the token's expiry, and no longer.
|
||||
_ = bl.Sweep(ctx, time.Now())
|
||||
// Past the token's expiry, until its refresh window closes, and no
|
||||
// longer.
|
||||
_ = bl.Sweep(ctx, time.Now().Add(time.Hour+2*time.Minute))
|
||||
if blocked, _ := bl.IsBlacklisted(ctx, jti); !blocked {
|
||||
t.Fatalf("%s: the entry left before the token expired", kind)
|
||||
t.Fatalf("%s: the entry left when the access token expired", kind)
|
||||
}
|
||||
_ = bl.Sweep(ctx, time.Now().Add(time.Hour+time.Minute))
|
||||
_ = bl.Sweep(ctx, time.Now().Add(controllers.RefreshTTL(app)+2*time.Minute))
|
||||
if blocked, _ := bl.IsBlacklisted(ctx, jti); blocked {
|
||||
t.Fatalf("%s: the entry outlived the token", kind)
|
||||
t.Fatalf("%s: the entry outlived the refresh window", kind)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user