fix: a /refresh carried by the auth.cookie session cookie rewrites that cookie, so an HttpOnly session survives the rotation
8e4b863 answered a cookie-carried refresh in the body only and blacklisted the old jti, so an HttpOnly cookie (golem15.sociallogin token_cookie) kept a dead token. Now the cookie named auth.cookie.name gets the rotated token and the body is {}; bearer and auth_token refreshes are unchanged, and refusals write no cookie.
This commit is contained in:
@@ -190,12 +190,17 @@ func Fetch(app *backpack.App) http.HandlerFunc {
|
||||
|
||||
// Refresh slides a still-refreshable session token of a user who may still
|
||||
// sign in. The token is read as the plugin's jwt guard reads it: the bearer
|
||||
// header, then the token and auth_token cookies. The rotated token is
|
||||
// answered in the body only; no cookie is written. The old token is
|
||||
// blacklisted after golem15.user.jwt.blacklist_grace, so a caller that keeps
|
||||
// the session in a cookie stores the new token there. Deactivated and banned
|
||||
// users and tokens issued before the user's tokens_valid_after are refused
|
||||
// with the token-failure 401 body.
|
||||
// header, then the token and auth_token cookies. When it came from the cookie
|
||||
// named auth.cookie.name with no bearer header, that cookie is rewritten with
|
||||
// the rotated token (bouncer.LoadCookieConfig attributes, HttpOnly, Max-Age
|
||||
// golem15.user.jwt.refresh_ttl) and the body is {}, whoever wrote the
|
||||
// cookie. A bearer-carried or other-cookie refresh answers the rotated token
|
||||
// in the body and writes no cookie; the old token is blacklisted after
|
||||
// golem15.user.jwt.blacklist_grace, so that caller stores the new token
|
||||
// itself. Refusals write and forget no cookie, and a broken auth.cookie
|
||||
// block answers a cookie-carried refresh with the opaque 500 before anything
|
||||
// is rotated. Deactivated and banned users and tokens issued before the
|
||||
// user's tokens_valid_after are refused with the token-failure 401 body.
|
||||
func Refresh(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
@@ -207,6 +212,20 @@ func Refresh(app *backpack.App) http.HandlerFunc {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "Token not found"})
|
||||
return
|
||||
}
|
||||
// The auth.cookie block is read before bouncer.Refresh, so a broken
|
||||
// block rotates and blacklists nothing; a bearer refresh never reads it.
|
||||
var rotate *bouncer.CookieConfig
|
||||
if name := classes.SessionTokenCookie(r); name != "" {
|
||||
cc, err := tokenCookieConfig(app)
|
||||
if err != nil {
|
||||
pluginLog.Error("golem15.user: refresh: auth.cookie config", "err", err)
|
||||
writeOpaque500(w)
|
||||
return
|
||||
}
|
||||
if cc.Name == name {
|
||||
rotate = &cc
|
||||
}
|
||||
}
|
||||
var bl bouncer.BlacklistStore
|
||||
if store, ok := app.Lookup[bouncer.BlacklistStore](); ok {
|
||||
bl = store
|
||||
@@ -242,7 +261,7 @@ func Refresh(app *backpack.App) http.HandlerFunc {
|
||||
if err := classes.TouchLastSeen(r.Context(), gdb, userID); err != nil {
|
||||
pluginLog.Warn("golem15.user: stamp last_seen on refresh", "user_id", userID, "err", err)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"token": next})
|
||||
writeSession(w, app, rotate, next, map[string]any{})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1183,6 +1202,31 @@ func remoteIP(r *http.Request) string {
|
||||
return host
|
||||
}
|
||||
|
||||
// tokenCookieConfig is the auth.cookie block the plugin writes the session
|
||||
// cookie with: bouncer.LoadCookieConfig of the application config, or its
|
||||
// defaults when there is no config.
|
||||
func tokenCookieConfig(app *backpack.App) (bouncer.CookieConfig, error) {
|
||||
if app == nil || app.Config == nil {
|
||||
return bouncer.LoadCookieConfig(nil)
|
||||
}
|
||||
return bouncer.LoadCookieConfig(app.Config)
|
||||
}
|
||||
|
||||
// writeSession answers 200 with a session token, the user plugin's twin of
|
||||
// golem15.sociallogin's writeSession. With cc nil the token goes into
|
||||
// body["token"], the key set the session routes have always answered. With
|
||||
// cc set it goes only into the HttpOnly cookie cc writes (Max-Age
|
||||
// golem15.user.jwt.refresh_ttl), so the token never reaches JavaScript, and
|
||||
// body is answered without it.
|
||||
func writeSession(w http.ResponseWriter, app *backpack.App, cc *bouncer.CookieConfig, token string, body map[string]any) {
|
||||
if cc == nil {
|
||||
body["token"] = token
|
||||
} else {
|
||||
http.SetCookie(w, cc.TokenCookie(token, refreshTTL(app)))
|
||||
}
|
||||
writeJSON(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Cache-Control", "no-cache, private")
|
||||
wire.WriteJSON(w, status, v)
|
||||
|
||||
Reference in New Issue
Block a user