fix(09): WR-14 let logout revoke an expired token that is still refreshable and always clear the cookie

This commit is contained in:
Jakub Zych
2026-10-01 21:23:42 +02:00
parent c9bb14944a
commit 299d220b51
9 changed files with 157 additions and 9 deletions

View File

@@ -8,6 +8,7 @@ import (
"log/slog"
"net"
"net/http"
"strconv"
"strings"
"sync"
"time"
@@ -249,10 +250,17 @@ func (s *service) refresh(w http.ResponseWriter, r *http.Request) {
// logout blacklists the presented token's jti and always expires the admin
// cookie, so a browser session ends even when only the Bearer was revoked.
//
// Logout is mounted outside the backend guard, which rejects an expired access
// token before any handler runs. The token is verified here instead, with
// exp unchecked, so a token whose access lifetime has passed but whose refresh
// window is still open (and which /auth/refresh would still accept) can be
// revoked. The cookie is expired on every outcome, including a refusal.
func (s *service) logout(w http.ResponseWriter, r *http.Request) {
s.expireSessionCookie(w)
raw, _ := sessionToken(r)
_, iat, exp, jti, err := bouncer.VerifyClaimsAudience(raw, s.secret, bouncer.AudienceBackend)
if err != nil || jti == "" {
sub, iat, exp, jti, err := bouncer.VerifyRefreshableClaimsAudience(raw, s.secret, bouncer.AudienceBackend, s.refreshTTL)
if err != nil {
WriteError(w, http.StatusUnauthorized, "unauthenticated", msgUnauthenticated)
return
}
@@ -267,11 +275,10 @@ func (s *service) logout(w http.ResponseWriter, r *http.Request) {
}
}
id := uint(0)
if principal, ok := bouncer.User(r.Context()); ok {
id = principal.ID
if n, err := strconv.ParseUint(sub, 10, 64); err == nil {
id = uint(n)
}
s.logAuth(r, "success", id)
s.expireSessionCookie(w)
WriteData(w, http.StatusOK, AdminLogoutData{Status: "logged_out"}, map[string]any{})
}