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
+14
-2
@@ -97,7 +97,7 @@ func (s *Server) Router() http.Handler {
|
||||
AllowedOrigins: allowedOrigins,
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
|
||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-API-Key"},
|
||||
ExposedHeaders: []string{"Link"},
|
||||
ExposedHeaders: []string{"Link", "X-Session-Token"},
|
||||
AllowCredentials: false,
|
||||
MaxAge: 300,
|
||||
}))
|
||||
@@ -125,6 +125,8 @@ func (s *Server) Router() http.Handler {
|
||||
r.Get("/api/v1/auth/me", s.handleMe)
|
||||
r.Post("/api/v1/events/rooms", s.handleSSERooms)
|
||||
r.Post("/api/v1/auth/stream-ticket", s.handleStreamTicket)
|
||||
r.Get("/api/v1/auth/sessions", s.handleListSessions)
|
||||
r.Delete("/api/v1/auth/sessions/{id}", s.handleEndSession)
|
||||
r.Post("/api/v1/auth/logout", s.handleLogout)
|
||||
r.Post("/api/v1/auth/revoke-all", s.handleRevokeAllSessions)
|
||||
r.Patch("/api/v1/auth/settings", s.handleUpdateUserSettings)
|
||||
@@ -417,7 +419,7 @@ func (s *Server) authenticateToken(token string) (user *domain.User, keyScopes [
|
||||
return u, scopes
|
||||
}
|
||||
claims, err := auth.ValidateToken(s.cfg.JWTSecret, token)
|
||||
if err != nil || claims == nil || s.store.IsTokenRevoked(token) {
|
||||
if err != nil || claims == nil {
|
||||
return nil, nil
|
||||
}
|
||||
u, err := s.store.GetUserByID(claims.UserID)
|
||||
@@ -427,6 +429,10 @@ func (s *Server) authenticateToken(token string) (user *domain.User, keyScopes [
|
||||
if claims.TokenVersion != 0 && claims.TokenVersion != u.TokenVersion {
|
||||
return nil, nil
|
||||
}
|
||||
// Logged out, ended from the session list or idle for 30 days
|
||||
if !s.store.SessionActive(claims.ID, u.ID) {
|
||||
return nil, nil
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
@@ -478,6 +484,12 @@ func (s *Server) authMiddleware(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
ctx := auth.WithUserID(r.Context(), user.ID)
|
||||
if keyScopes == nil {
|
||||
if claims, err := auth.ValidateToken(s.cfg.JWTSecret, token); err == nil {
|
||||
ctx = withSession(ctx, claims.ID)
|
||||
s.renewSessionToken(w, user, claims, false)
|
||||
}
|
||||
}
|
||||
if keyScopes != nil {
|
||||
if !apiKeyAllowed(r) {
|
||||
writeError(w, http.StatusForbidden, "API-Keys dürfen nur Daten einliefern (POST /api/v1/inbox oder /api/v1/webhooks/ingest)")
|
||||
|
||||
Reference in new issue
Block a user