mirror of
https://github.com/mudler/LocalAI.git
synced 2026-03-31 13:15:51 -04:00
* feat: add distributed mode (experimental) Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix data races, mutexes, transactions Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix events and tool stream in agent chat Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * use ginkgo Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix(cron): compute correctly time boundaries avoiding re-triggering Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not flood of healthy checks Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * do not list obvious backends as text backends Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * tests fixups Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * refactoring and consolidation Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * Drop redundant healthcheck Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * enhancements, refactorings Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
183 lines
4.9 KiB
Go
183 lines
4.9 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
sessionDuration = 30 * 24 * time.Hour // 30 days
|
|
sessionIDBytes = 32 // 32 bytes = 64 hex chars
|
|
sessionCookie = "session"
|
|
sessionRotationInterval = 1 * time.Hour
|
|
)
|
|
|
|
// CreateSession creates a new session for the given user, returning the
|
|
// plaintext token (64-char hex string). The stored session ID is the
|
|
// HMAC-SHA256 hash of the token.
|
|
func CreateSession(db *gorm.DB, userID, hmacSecret string) (string, error) {
|
|
b := make([]byte, sessionIDBytes)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("failed to generate session ID: %w", err)
|
|
}
|
|
|
|
plaintext := hex.EncodeToString(b)
|
|
hash := HashAPIKey(plaintext, hmacSecret)
|
|
|
|
now := time.Now()
|
|
session := Session{
|
|
ID: hash,
|
|
UserID: userID,
|
|
ExpiresAt: now.Add(sessionDuration),
|
|
RotatedAt: now,
|
|
}
|
|
|
|
if err := db.Create(&session).Error; err != nil {
|
|
return "", fmt.Errorf("failed to create session: %w", err)
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// ValidateSession hashes the plaintext token and looks up the session.
|
|
// Returns the associated user and session, or (nil, nil) if not found/expired.
|
|
func ValidateSession(db *gorm.DB, token, hmacSecret string) (*User, *Session) {
|
|
hash := HashAPIKey(token, hmacSecret)
|
|
|
|
var session Session
|
|
if err := db.Preload("User").Where("id = ? AND expires_at > ?", hash, time.Now()).First(&session).Error; err != nil {
|
|
return nil, nil
|
|
}
|
|
if session.User.Status != StatusActive {
|
|
return nil, nil
|
|
}
|
|
return &session.User, &session
|
|
}
|
|
|
|
// DeleteSession removes a session by hashing the plaintext token.
|
|
func DeleteSession(db *gorm.DB, token, hmacSecret string) error {
|
|
hash := HashAPIKey(token, hmacSecret)
|
|
return db.Where("id = ?", hash).Delete(&Session{}).Error
|
|
}
|
|
|
|
// CleanExpiredSessions removes all sessions that have passed their expiry time.
|
|
func CleanExpiredSessions(db *gorm.DB) error {
|
|
return db.Where("expires_at < ?", time.Now()).Delete(&Session{}).Error
|
|
}
|
|
|
|
// DeleteUserSessions removes all sessions for the given user.
|
|
func DeleteUserSessions(db *gorm.DB, userID string) error {
|
|
return db.Where("user_id = ?", userID).Delete(&Session{}).Error
|
|
}
|
|
|
|
// RotateSession creates a new session for the same user, deletes the old one,
|
|
// and returns the new plaintext token.
|
|
func RotateSession(db *gorm.DB, oldSession *Session, hmacSecret string) (string, error) {
|
|
b := make([]byte, sessionIDBytes)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", fmt.Errorf("failed to generate session ID: %w", err)
|
|
}
|
|
|
|
plaintext := hex.EncodeToString(b)
|
|
hash := HashAPIKey(plaintext, hmacSecret)
|
|
|
|
now := time.Now()
|
|
newSession := Session{
|
|
ID: hash,
|
|
UserID: oldSession.UserID,
|
|
ExpiresAt: oldSession.ExpiresAt,
|
|
RotatedAt: now,
|
|
}
|
|
|
|
err := db.Transaction(func(tx *gorm.DB) error {
|
|
if err := tx.Create(&newSession).Error; err != nil {
|
|
return err
|
|
}
|
|
return tx.Where("id = ?", oldSession.ID).Delete(&Session{}).Error
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to rotate session: %w", err)
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// MaybeRotateSession checks if the session should be rotated and does so if needed.
|
|
// Called from the auth middleware after successful cookie-based authentication.
|
|
func MaybeRotateSession(c echo.Context, db *gorm.DB, session *Session, hmacSecret string) {
|
|
if session == nil {
|
|
return
|
|
}
|
|
|
|
rotatedAt := session.RotatedAt
|
|
if rotatedAt.IsZero() {
|
|
rotatedAt = session.CreatedAt
|
|
}
|
|
|
|
if time.Since(rotatedAt) < sessionRotationInterval {
|
|
return
|
|
}
|
|
|
|
newToken, err := RotateSession(db, session, hmacSecret)
|
|
if err != nil {
|
|
// Rotation failure is non-fatal; the old session remains valid
|
|
return
|
|
}
|
|
|
|
SetSessionCookie(c, newToken)
|
|
}
|
|
|
|
// isSecure returns true when the request arrived over HTTPS, either directly
|
|
// or via a reverse proxy that sets X-Forwarded-Proto.
|
|
func isSecure(c echo.Context) bool {
|
|
return c.Scheme() == "https"
|
|
}
|
|
|
|
// SetSessionCookie sets the session cookie on the response.
|
|
func SetSessionCookie(c echo.Context, sessionID string) {
|
|
cookie := &http.Cookie{
|
|
Name: sessionCookie,
|
|
Value: sessionID,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: isSecure(c),
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: int(sessionDuration.Seconds()),
|
|
}
|
|
c.SetCookie(cookie)
|
|
}
|
|
|
|
// SetTokenCookie sets an httpOnly "token" cookie for legacy API key auth.
|
|
func SetTokenCookie(c echo.Context, token string) {
|
|
cookie := &http.Cookie{
|
|
Name: "token",
|
|
Value: token,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: isSecure(c),
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: int(sessionDuration.Seconds()),
|
|
}
|
|
c.SetCookie(cookie)
|
|
}
|
|
|
|
// ClearSessionCookie clears the session cookie.
|
|
func ClearSessionCookie(c echo.Context) {
|
|
cookie := &http.Cookie{
|
|
Name: sessionCookie,
|
|
Value: "",
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
Secure: isSecure(c),
|
|
SameSite: http.SameSiteLaxMode,
|
|
MaxAge: -1,
|
|
}
|
|
c.SetCookie(cookie)
|
|
}
|