feat(auth): server-side sessions and current password for account changes (FLAW-30, FLAW-31)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
- Every login is a session (device, IP, last use); the token's jti is its ID. Sessions end on logout, per device from the profile, or after 30 idle days. - Tokens older than a day are renewed via the X-Session-Token header. - Changing password or login e-mail needs the current password; a new password logs out all other devices and mails the old address. - Demo reset keeps the demo visitors' sessions. - revoked_tokens is replaced by sessions (migration 4); everyone is logged out once after the upgrade. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b2ecda9896
commit
1af3a6e90a
25 files changed
+705
-75
No files matched your search
@@ -303,7 +303,7 @@ func (s *Store) DeleteUser(userID string) error {
|
||||
return err
|
||||
}
|
||||
if err := s.execInTx(
|
||||
stmt(`DELETE FROM revoked_tokens WHERE user_id = ?`, userID),
|
||||
stmt(`DELETE FROM sessions WHERE user_id = ?`, userID),
|
||||
stmt(`DELETE FROM task_assignees WHERE user_id = ?`, userID),
|
||||
stmt(`DELETE FROM password_reset_tokens WHERE user_id = ?`, userID),
|
||||
stmt(`DELETE FROM users WHERE id = ?`, userID),
|
||||
@@ -315,33 +315,12 @@ func (s *Store) DeleteUser(userID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeToken adds an individual JWT to the blacklist until its expiration date
|
||||
func (s *Store) RevokeToken(tokenStr, userID string, expiresAt time.Time) error {
|
||||
hash := auth.HashAPIKey(tokenStr)
|
||||
_, err := s.db.Exec(
|
||||
`INSERT OR REPLACE INTO revoked_tokens (token_hash, user_id, expires_at, revoked_at) VALUES (?, ?, ?, ?)`,
|
||||
hash, userID, expiresAt.UTC().Format(time.RFC3339), time.Now().UTC().Format(time.RFC3339),
|
||||
)
|
||||
// Idempotently prune old expired tokens
|
||||
_, _ = s.db.Exec(`DELETE FROM revoked_tokens WHERE expires_at < ?`, time.Now().UTC().Format(time.RFC3339))
|
||||
return err
|
||||
}
|
||||
|
||||
// IsTokenRevoked checks if a specific JWT token has been explicitly blacklisted
|
||||
func (s *Store) IsTokenRevoked(tokenStr string) bool {
|
||||
hash := auth.HashAPIKey(tokenStr)
|
||||
var count int
|
||||
err := s.db.QueryRow(
|
||||
`SELECT COUNT(1) FROM revoked_tokens WHERE token_hash = ? AND expires_at >= ?`,
|
||||
hash, time.Now().UTC().Format(time.RFC3339),
|
||||
).Scan(&count)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
// RevokeAllUserTokens increments the user's token_version, instantly invalidating all existing JWTs for that user
|
||||
func (s *Store) RevokeAllUserTokens(userID string) error {
|
||||
_, err := s.db.Exec(`UPDATE users SET token_version = token_version + 1 WHERE id = ?`, userID)
|
||||
return err
|
||||
return s.execInTx(
|
||||
stmt(`UPDATE users SET token_version = token_version + 1 WHERE id = ?`, userID),
|
||||
stmt(`UPDATE sessions SET revoked_at = ? WHERE user_id = ? AND revoked_at = ''`, nowUTC(), userID),
|
||||
)
|
||||
}
|
||||
|
||||
// --- USER INVITES ---
|
||||
|
||||
Reference in new issue
Block a user