fix: /fetch, /refresh, /update and /change-password accept the session cookies the jwt guard reads

The four routes (and the other signed-in routes through authenticate) read the bearer header, then the token and auth_token cookies, through classes.SessionToken and one cookie-aware guard.
A bearer header still wins and no credentials stays 401 with the old bodies.
A cookie-carried /refresh answers the rotated token in the body and writes no cookie.
This commit is contained in:
Jakub Zych
2026-10-22 21:45:52 +02:00
parent 15ff9793ba
commit 8e4b863a3f
5 changed files with 330 additions and 47 deletions

View File

@@ -20,6 +20,14 @@ import (
// the Authorization header, in the same order (plugin.go Boot).
var sessionCookieNames = []string{"token", "auth_token"}
// SessionCookieNames returns a copy of the cookie names the plugin's "jwt"
// guard (plugin.go Boot) reads after the Authorization header, in order.
// A handler that builds its own guard passes them to bouncer.NewJWTGuard so
// it reads the same token as SessionToken and jwt.auth.
func SessionCookieNames() []string {
return slices.Clone(sessionCookieNames)
}
// RequestUser returns the signed-in user of r for handlers that run without
// the jwt.auth middleware and must also serve anonymous requests (an OAuth
// provider's authorize page, for example). The token is read and checked
@@ -35,7 +43,7 @@ func RequestUser(ctx context.Context, app *backpack.App, r *http.Request) (*mode
if r == nil {
return nil, nil
}
raw := sessionToken(r)
raw := SessionToken(r)
if raw == "" {
return nil, nil
}
@@ -209,9 +217,12 @@ func refreshWindow(app *backpack.App) time.Duration {
return time.Duration(mins) * time.Minute
}
// sessionToken extracts the session JWT as the "jwt" guard does: the
// Authorization bearer header, then each session cookie in order.
func sessionToken(r *http.Request) string {
// SessionToken returns the session JWT the plugin's "jwt" guard reads from r:
// the Authorization bearer header, then each cookie of SessionCookieNames in
// order, trimmed, empty values skipped. It returns "" when r carries none.
// A controller that builds its own guard passes SessionCookieNames() to
// bouncer.NewJWTGuard, so the guard and SessionToken read the same token.
func SessionToken(r *http.Request) string {
if tokens := sessionTokens(r); len(tokens) > 0 {
return tokens[0]
}