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:
@@ -186,16 +186,21 @@ func Fetch(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh slides a still-refreshable bearer token of a user who may still
|
||||
// sign in: deactivated and banned users and tokens issued before the user's
|
||||
// tokens_valid_after are refused with the token-failure 401 body.
|
||||
// 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.
|
||||
func Refresh(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
raw := bearerToken(r)
|
||||
raw := classes.SessionToken(r)
|
||||
if raw == "" {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "Token not found"})
|
||||
return
|
||||
@@ -583,24 +588,29 @@ func Update(app *backpack.App) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// ChangePassword replaces a self-set password and invalidates older tokens.
|
||||
// The token that made this call stays valid.
|
||||
// The token is read as the plugin's jwt guard reads it (the bearer header,
|
||||
// then the token and auth_token cookies), and the token that made this call,
|
||||
// header or cookie, stays valid.
|
||||
func ChangePassword(app *backpack.App) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
raw := bearerToken(r)
|
||||
raw := classes.SessionToken(r)
|
||||
sub, iat, _, _, err := bouncer.VerifyClaims(raw, secret)
|
||||
if err != nil {
|
||||
writeUnauthorized(w)
|
||||
return
|
||||
}
|
||||
var bl bouncer.BlacklistStore
|
||||
if store, ok := app.Lookup[bouncer.BlacklistStore](); ok {
|
||||
bl = store
|
||||
principal, err := sessionGuard(app, secret).Authenticate(r)
|
||||
if err != nil || principal == nil {
|
||||
writeUnauthorized(w)
|
||||
return
|
||||
}
|
||||
if _, err := bouncer.NewJWTGuard(secret, classes.GormUsers{App: app}, bl).Authenticate(r); err != nil {
|
||||
// The guard and SessionToken read the same token; a differing
|
||||
// subject fails closed.
|
||||
if strconv.FormatUint(uint64(principal.ID), 10) != sub {
|
||||
writeUnauthorized(w)
|
||||
return
|
||||
}
|
||||
@@ -877,38 +887,41 @@ func appConfig(app *backpack.App) *compass.Config {
|
||||
return app.Config
|
||||
}
|
||||
|
||||
// authenticate resolves the signed-in user of r for the session routes that
|
||||
// run without jwt.auth: /fetch, /update, /activate, /marketing-consent,
|
||||
// /avatar and /avatar/remove. It reads the token as the plugin's jwt guard
|
||||
// does (the bearer header, then the token and auth_token cookies), applies
|
||||
// the blacklist and the user's tokens_valid_after cutoff, and answers 401
|
||||
// otherwise.
|
||||
func authenticate(w http.ResponseWriter, r *http.Request, app *backpack.App) (*models.User, bool) {
|
||||
gdb, secret, ok := sessionDeps(w, app)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
raw := bearerToken(r)
|
||||
sub, _, _, _, err := bouncer.VerifyClaims(raw, secret)
|
||||
if err != nil {
|
||||
writeUnauthorized(w)
|
||||
return nil, false
|
||||
}
|
||||
id, err := strconv.ParseUint(sub, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
writeUnauthorized(w)
|
||||
return nil, false
|
||||
}
|
||||
var bl bouncer.BlacklistStore
|
||||
if store, ok := app.Lookup[bouncer.BlacklistStore](); ok {
|
||||
bl = store
|
||||
}
|
||||
if _, err := bouncer.NewJWTGuard(secret, classes.GormUsers{App: app}, bl).Authenticate(r); err != nil {
|
||||
principal, err := sessionGuard(app, secret).Authenticate(r)
|
||||
if err != nil || principal == nil || principal.ID == 0 {
|
||||
writeUnauthorized(w)
|
||||
return nil, false
|
||||
}
|
||||
var user models.User
|
||||
if err := gdb.WithContext(r.Context()).Where("id = ?", id).Take(&user).Error; err != nil {
|
||||
if err := gdb.WithContext(r.Context()).Where("id = ?", principal.ID).Take(&user).Error; err != nil {
|
||||
writeUnauthorized(w)
|
||||
return nil, false
|
||||
}
|
||||
return &user, true
|
||||
}
|
||||
|
||||
// sessionGuard is the plugin's jwt guard as plugin.go Boot registers it: the
|
||||
// bearer header, then the cookies of classes.SessionCookieNames, checked
|
||||
// against the published bouncer.BlacklistStore when there is one.
|
||||
func sessionGuard(app *backpack.App, secret string) bouncer.Guard {
|
||||
var bl bouncer.BlacklistStore
|
||||
if store, ok := app.Lookup[bouncer.BlacklistStore](); ok {
|
||||
bl = store
|
||||
}
|
||||
return bouncer.NewJWTGuard(secret, classes.GormUsers{App: app}, bl, classes.SessionCookieNames()...)
|
||||
}
|
||||
|
||||
func sessionDeps(w http.ResponseWriter, app *backpack.App) (*gorm.DB, string, bool) {
|
||||
if app == nil {
|
||||
writeOpaque500(w)
|
||||
@@ -1135,16 +1148,6 @@ func jsonNumber(v any) string {
|
||||
}
|
||||
}
|
||||
|
||||
func bearerToken(r *http.Request) string {
|
||||
h := strings.TrimSpace(r.Header.Get("Authorization"))
|
||||
token, ok := strings.CutPrefix(h, "Bearer ")
|
||||
token = strings.TrimSpace(token)
|
||||
if !ok || token == "" {
|
||||
return ""
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func requestURL(r *http.Request) string {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
|
||||
Reference in New Issue
Block a user