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

- 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:
sandrodachandClaude Opus 5.5 committed 2026-10-29 19:25:22 +01:00
1 parent b2ecda9896
commit 1af3a6e90a
25 files changed
+705 -75

No files matched your search

+4 -3
View File
@@ -22,6 +22,7 @@ const streamTicketTTL = 60 * time.Second
type streamTicket struct {
userID string
sessionID string
tokenVersion int
expires time.Time
}
@@ -31,7 +32,7 @@ type streamTickets struct {
tickets map[string]streamTicket
}
func (t *streamTickets) issue(userID string, tokenVersion int) (string, error) {
func (t *streamTickets) issue(userID, sessionID string, tokenVersion int) (string, error) {
buf := make([]byte, 24)
if _, err := rand.Read(buf); err != nil {
return "", err
@@ -48,7 +49,7 @@ func (t *streamTickets) issue(userID string, tokenVersion int) (string, error) {
delete(t.tickets, k)
}
}
t.tickets[id] = streamTicket{userID: userID, tokenVersion: tokenVersion, expires: now.Add(streamTicketTTL)}
t.tickets[id] = streamTicket{userID: userID, sessionID: sessionID, tokenVersion: tokenVersion, expires: now.Add(streamTicketTTL)}
return id, nil
}
@@ -70,7 +71,7 @@ func (s *Server) handleStreamTicket(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusUnauthorized, "Nicht autorisiert")
return
}
ticket, err := s.streamTickets.issue(user.ID, user.TokenVersion)
ticket, err := s.streamTickets.issue(user.ID, sessionIDFrom(r), user.TokenVersion)
if err != nil {
writeError(w, http.StatusInternalServerError, "Ticket konnte nicht erstellt werden")
return