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:
@@ -126,17 +126,22 @@ func Login(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Logout forever-blacklists the presented bearer token.
|
||||
// Logout ends the request's session through classes.EndSession: every
|
||||
// session token it carries (the bearer header, or the token and auth_token
|
||||
// cookies the jwt guard also reads) is blacklisted for the rest of its
|
||||
// refresh window, so the next request with it is refused and /refresh
|
||||
// cannot revive it, and the session cookies are expired, on a 401 too.
|
||||
// It answers 200 {"message":"Logged out"} when the token the guard reads
|
||||
// verifies (an expired one too while it can still be refreshed) and names a
|
||||
// user, 401 otherwise, and 500 when the blacklist write fails.
|
||||
func Logout(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, secret, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
if _, _, ok := sessionDeps(w, app); !ok {
|
||||
return
|
||||
}
|
||||
raw := bearerToken(r)
|
||||
sub, _, exp, jti, err := bouncer.VerifyClaims(raw, secret)
|
||||
if err != nil || jti == "" {
|
||||
writeUnauthorized(w)
|
||||
sub, err := classes.EndSession(r.Context(), app, w, r)
|
||||
if err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
id, err := strconv.ParseUint(sub, 10, 64)
|
||||
@@ -149,12 +154,6 @@ func Logout(app *backpack.App) http.HandlerFunc {
|
||||
writeUnauthorized(w)
|
||||
return
|
||||
}
|
||||
if bl, ok := app.Lookup[bouncer.BlacklistStore](); ok && bl != nil {
|
||||
if err := bl.Add(r.Context(), jti, exp, time.Now()); err != nil {
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"message": "Logged out"})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user