mirror of
https://github.com/mudler/LocalAI.git
synced 2026-04-01 13:42:20 -04:00
* feat(ui): add users and authentication support Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat: allow the admin user to impersonificate users Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: ui improvements, disable 'Users' button in navbar when no auth is configured Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat: add OIDC support Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: gate models Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: cache requests to optimize speed Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * small UI enhancements Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(ui): style improvements Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: cover other paths by auth Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: separate local auth, refactor Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * security hardening, approval mode Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * fix: fix tests and expectations Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: update localagi/localrecall Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
273 lines
7.9 KiB
Go
273 lines
7.9 KiB
Go
//go:build auth
|
|
|
|
package auth_test
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/core/http/auth"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var _ = Describe("Sessions", func() {
|
|
var (
|
|
db *gorm.DB
|
|
user *auth.User
|
|
)
|
|
|
|
// Use empty HMAC secret for basic tests
|
|
hmacSecret := ""
|
|
|
|
BeforeEach(func() {
|
|
db = testDB()
|
|
user = createTestUser(db, "session@example.com", auth.RoleUser, auth.ProviderGitHub)
|
|
})
|
|
|
|
Describe("CreateSession", func() {
|
|
It("creates a session and returns 64-char hex plaintext token", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(token).To(HaveLen(64))
|
|
})
|
|
|
|
It("stores the hash (not plaintext) in the DB", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
var session auth.Session
|
|
err = db.First(&session, "id = ?", hash).Error
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(session.UserID).To(Equal(user.ID))
|
|
// The plaintext token should NOT be stored as the ID
|
|
Expect(session.ID).ToNot(Equal(token))
|
|
Expect(session.ID).To(Equal(hash))
|
|
})
|
|
|
|
It("sets expiry to approximately 30 days from now", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
var session auth.Session
|
|
db.First(&session, "id = ?", hash)
|
|
|
|
expectedExpiry := time.Now().Add(30 * 24 * time.Hour)
|
|
Expect(session.ExpiresAt).To(BeTemporally("~", expectedExpiry, time.Minute))
|
|
})
|
|
|
|
It("sets RotatedAt on creation", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
var session auth.Session
|
|
db.First(&session, "id = ?", hash)
|
|
|
|
Expect(session.RotatedAt).To(BeTemporally("~", time.Now(), time.Minute))
|
|
})
|
|
|
|
It("associates session with correct user", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
var session auth.Session
|
|
db.First(&session, "id = ?", hash)
|
|
Expect(session.UserID).To(Equal(user.ID))
|
|
})
|
|
})
|
|
|
|
Describe("ValidateSession", func() {
|
|
It("returns user for valid session", func() {
|
|
token := createTestSession(db, user.ID)
|
|
|
|
found, session := auth.ValidateSession(db, token, hmacSecret)
|
|
Expect(found).ToNot(BeNil())
|
|
Expect(found.ID).To(Equal(user.ID))
|
|
Expect(session).ToNot(BeNil())
|
|
})
|
|
|
|
It("returns nil for non-existent session", func() {
|
|
found, session := auth.ValidateSession(db, "nonexistent-session-id", hmacSecret)
|
|
Expect(found).To(BeNil())
|
|
Expect(session).To(BeNil())
|
|
})
|
|
|
|
It("returns nil for expired session", func() {
|
|
token := createTestSession(db, user.ID)
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
|
|
// Manually expire the session
|
|
db.Model(&auth.Session{}).Where("id = ?", hash).
|
|
Update("expires_at", time.Now().Add(-1*time.Hour))
|
|
|
|
found, _ := auth.ValidateSession(db, token, hmacSecret)
|
|
Expect(found).To(BeNil())
|
|
})
|
|
})
|
|
|
|
Describe("DeleteSession", func() {
|
|
It("removes the session from DB", func() {
|
|
token := createTestSession(db, user.ID)
|
|
|
|
err := auth.DeleteSession(db, token, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
found, _ := auth.ValidateSession(db, token, hmacSecret)
|
|
Expect(found).To(BeNil())
|
|
})
|
|
|
|
It("does not error on non-existent session", func() {
|
|
err := auth.DeleteSession(db, "nonexistent", hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
})
|
|
})
|
|
|
|
Describe("CleanExpiredSessions", func() {
|
|
It("removes expired sessions", func() {
|
|
token := createTestSession(db, user.ID)
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
|
|
// Manually expire the session
|
|
db.Model(&auth.Session{}).Where("id = ?", hash).
|
|
Update("expires_at", time.Now().Add(-1*time.Hour))
|
|
|
|
err := auth.CleanExpiredSessions(db)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var count int64
|
|
db.Model(&auth.Session{}).Where("id = ?", hash).Count(&count)
|
|
Expect(count).To(Equal(int64(0)))
|
|
})
|
|
|
|
It("keeps active sessions", func() {
|
|
token := createTestSession(db, user.ID)
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
|
|
err := auth.CleanExpiredSessions(db)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
var count int64
|
|
db.Model(&auth.Session{}).Where("id = ?", hash).Count(&count)
|
|
Expect(count).To(Equal(int64(1)))
|
|
})
|
|
})
|
|
|
|
Describe("RotateSession", func() {
|
|
It("creates a new session and deletes the old one", func() {
|
|
token := createTestSession(db, user.ID)
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
|
|
// Get the old session
|
|
var oldSession auth.Session
|
|
db.First(&oldSession, "id = ?", hash)
|
|
|
|
newToken, err := auth.RotateSession(db, &oldSession, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
Expect(newToken).To(HaveLen(64))
|
|
Expect(newToken).ToNot(Equal(token))
|
|
|
|
// Old session should be gone
|
|
var count int64
|
|
db.Model(&auth.Session{}).Where("id = ?", hash).Count(&count)
|
|
Expect(count).To(Equal(int64(0)))
|
|
|
|
// New session should exist and validate
|
|
found, _ := auth.ValidateSession(db, newToken, hmacSecret)
|
|
Expect(found).ToNot(BeNil())
|
|
Expect(found.ID).To(Equal(user.ID))
|
|
})
|
|
|
|
It("preserves user ID and expiry", func() {
|
|
token := createTestSession(db, user.ID)
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
|
|
var oldSession auth.Session
|
|
db.First(&oldSession, "id = ?", hash)
|
|
|
|
newToken, err := auth.RotateSession(db, &oldSession, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
newHash := auth.HashAPIKey(newToken, hmacSecret)
|
|
var newSession auth.Session
|
|
db.First(&newSession, "id = ?", newHash)
|
|
|
|
Expect(newSession.UserID).To(Equal(oldSession.UserID))
|
|
Expect(newSession.ExpiresAt).To(BeTemporally("~", oldSession.ExpiresAt, time.Second))
|
|
})
|
|
})
|
|
|
|
Context("with HMAC secret", func() {
|
|
hmacSecret := "test-hmac-secret-123"
|
|
|
|
It("creates and validates sessions with HMAC secret", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
found, session := auth.ValidateSession(db, token, hmacSecret)
|
|
Expect(found).ToNot(BeNil())
|
|
Expect(found.ID).To(Equal(user.ID))
|
|
Expect(session).ToNot(BeNil())
|
|
})
|
|
|
|
It("does not validate with wrong HMAC secret", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
found, _ := auth.ValidateSession(db, token, "wrong-secret")
|
|
Expect(found).To(BeNil())
|
|
})
|
|
|
|
It("does not validate with empty HMAC secret", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
found, _ := auth.ValidateSession(db, token, "")
|
|
Expect(found).To(BeNil())
|
|
})
|
|
|
|
It("session created with empty secret does not validate with non-empty secret", func() {
|
|
token, err := auth.CreateSession(db, user.ID, "")
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
found, _ := auth.ValidateSession(db, token, hmacSecret)
|
|
Expect(found).To(BeNil())
|
|
})
|
|
|
|
It("deletes session with correct HMAC secret", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
err = auth.DeleteSession(db, token, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
found, _ := auth.ValidateSession(db, token, hmacSecret)
|
|
Expect(found).To(BeNil())
|
|
})
|
|
|
|
It("rotates session with HMAC secret", func() {
|
|
token, err := auth.CreateSession(db, user.ID, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
hash := auth.HashAPIKey(token, hmacSecret)
|
|
var oldSession auth.Session
|
|
db.First(&oldSession, "id = ?", hash)
|
|
|
|
newToken, err := auth.RotateSession(db, &oldSession, hmacSecret)
|
|
Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Old token should not validate
|
|
found, _ := auth.ValidateSession(db, token, hmacSecret)
|
|
Expect(found).To(BeNil())
|
|
|
|
// New token should validate
|
|
found, _ = auth.ValidateSession(db, newToken, hmacSecret)
|
|
Expect(found).ToNot(BeNil())
|
|
Expect(found.ID).To(Equal(user.ID))
|
|
})
|
|
})
|
|
})
|