RotateRefreshToken marks the refresh row inactive (via CAS) and ALSO revokes every access token for the same request_id. Without the cascade, an attacker who captured the pre-refresh access token could continue using it for up to its remaining lifetime (~1h) after the legitimate client rotated. The
(ctx context.Context, requestID, refreshTokenSignature string)
| 442 | // Runs through db(ctx) so when fosite wraps this call in |
| 443 | // MaybeBeginTx the two UPDATEs land in the same transaction. |
| 444 | func (s *Storage) RotateRefreshToken(ctx context.Context, requestID, refreshTokenSignature string) error { |
| 445 | db := s.db(ctx) |
| 446 | tag, err := db.Exec(ctx, |
| 447 | `UPDATE oauth_refresh_tokens SET active = FALSE WHERE signature = $1 AND active = TRUE`, |
| 448 | refreshTokenSignature, |
| 449 | ) |
| 450 | if err != nil { |
| 451 | return err |
| 452 | } |
| 453 | if tag.RowsAffected() == 0 { |
| 454 | return fosite.ErrInactiveToken |
| 455 | } |
| 456 | _, err = db.Exec(ctx, |
| 457 | `UPDATE oauth_access_tokens SET revoked_at = NOW() WHERE request_id = $1 AND revoked_at IS NULL`, |
| 458 | requestID, |
| 459 | ) |
| 460 | return err |
| 461 | } |
| 462 | |
| 463 | // ───────────────────────── TokenRevocationStorage ───────────────────────── |
| 464 |