fix: a self-service password change revokes every OAuth grant, however recently refreshed

ChangePassword fired the access-revoked event with Before = the calling
session's iat, so an OAuth/MCP grant whose client refreshed it after that
instant kept working. Before is now the instant of the request; the stored
tokens_valid_after still keeps the calling session.
This commit is contained in:
2026-10-23 20:48:38 +02:00
parent 38551a4104
commit 30f7a01d3d
3 changed files with 26 additions and 16 deletions

View File

@@ -716,10 +716,12 @@ func ChangePassword(app *backpack.App) http.HandlerFunc {
return
}
// The cutoff keeps the current session (issued at iat) and every
// token issued since its sign-in second; the access-revoked event
// revokes OAuth grants created before that second, in the same
// transaction.
// session token issued since that second. OAuth grants are revoked
// as of now, whatever their age: the request needs none, and a
// grant its client refreshed after iat would otherwise survive the
// change (its rotated tokens are newer than the session).
cutoff := iat.Add(-time.Second)
revokedAt := time.Now()
if err := lagoon.Transaction(r.Context(), gdb, func(ctx context.Context, tx *gorm.DB) error {
if err := tx.WithContext(ctx).Model(&user).Updates(map[string]any{
"password": hashed,
@@ -729,7 +731,7 @@ func ChangePassword(app *backpack.App) http.HandlerFunc {
}).Error; err != nil {
return err
}
return classes.FireAccessRevoked(ctx, app, tx, []uint{user.ID}, cutoff.Add(time.Second), classes.AccessRevokedPasswordChanged)
return classes.FireAccessRevoked(ctx, app, tx, []uint{user.ID}, revokedAt, classes.AccessRevokedPasswordChanged)
}); err != nil {
writeOpaque500(w)
return