mirror of
https://github.com/mudler/LocalAI.git
synced 2026-04-01 05:36:49 -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>
122 lines
3.6 KiB
Go
122 lines
3.6 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const (
|
|
apiKeyPrefix = "lai-"
|
|
apiKeyRandBytes = 32 // 32 bytes = 64 hex chars
|
|
keyPrefixLen = 8 // display prefix length (from the random part)
|
|
)
|
|
|
|
// GenerateAPIKey generates a new API key. Returns the plaintext key,
|
|
// its HMAC-SHA256 hash, and a display prefix.
|
|
func GenerateAPIKey(hmacSecret string) (plaintext, hash, prefix string, err error) {
|
|
b := make([]byte, apiKeyRandBytes)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", "", "", fmt.Errorf("failed to generate API key: %w", err)
|
|
}
|
|
|
|
randHex := hex.EncodeToString(b)
|
|
plaintext = apiKeyPrefix + randHex
|
|
hash = HashAPIKey(plaintext, hmacSecret)
|
|
prefix = plaintext[:len(apiKeyPrefix)+keyPrefixLen]
|
|
|
|
return plaintext, hash, prefix, nil
|
|
}
|
|
|
|
// HashAPIKey returns the HMAC-SHA256 hex digest of the given plaintext key.
|
|
// If hmacSecret is empty, falls back to plain SHA-256 for backward compatibility.
|
|
func HashAPIKey(plaintext, hmacSecret string) string {
|
|
if hmacSecret == "" {
|
|
h := sha256.Sum256([]byte(plaintext))
|
|
return hex.EncodeToString(h[:])
|
|
}
|
|
mac := hmac.New(sha256.New, []byte(hmacSecret))
|
|
mac.Write([]byte(plaintext))
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
// CreateAPIKey generates and stores a new API key for the given user.
|
|
// Returns the plaintext key (shown once) and the database record.
|
|
func CreateAPIKey(db *gorm.DB, userID, name, role, hmacSecret string, expiresAt *time.Time) (string, *UserAPIKey, error) {
|
|
plaintext, hash, prefix, err := GenerateAPIKey(hmacSecret)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
record := &UserAPIKey{
|
|
ID: uuid.New().String(),
|
|
UserID: userID,
|
|
Name: name,
|
|
KeyHash: hash,
|
|
KeyPrefix: prefix,
|
|
Role: role,
|
|
ExpiresAt: expiresAt,
|
|
}
|
|
|
|
if err := db.Create(record).Error; err != nil {
|
|
return "", nil, fmt.Errorf("failed to store API key: %w", err)
|
|
}
|
|
|
|
return plaintext, record, nil
|
|
}
|
|
|
|
// ValidateAPIKey looks up an API key by hashing the plaintext and searching
|
|
// the database. Returns the key record if found, or an error.
|
|
// Updates LastUsed on successful validation.
|
|
func ValidateAPIKey(db *gorm.DB, plaintext, hmacSecret string) (*UserAPIKey, error) {
|
|
hash := HashAPIKey(plaintext, hmacSecret)
|
|
|
|
var key UserAPIKey
|
|
if err := db.Preload("User").Where("key_hash = ?", hash).First(&key).Error; err != nil {
|
|
return nil, fmt.Errorf("invalid API key")
|
|
}
|
|
|
|
if key.ExpiresAt != nil && time.Now().After(*key.ExpiresAt) {
|
|
return nil, fmt.Errorf("API key expired")
|
|
}
|
|
|
|
if key.User.Status != StatusActive {
|
|
return nil, fmt.Errorf("user account is not active")
|
|
}
|
|
|
|
// Update LastUsed
|
|
now := time.Now()
|
|
db.Model(&key).Update("last_used", now)
|
|
|
|
return &key, nil
|
|
}
|
|
|
|
// ListAPIKeys returns all API keys for the given user (without plaintext).
|
|
func ListAPIKeys(db *gorm.DB, userID string) ([]UserAPIKey, error) {
|
|
var keys []UserAPIKey
|
|
if err := db.Where("user_id = ?", userID).Order("created_at DESC").Find(&keys).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// RevokeAPIKey deletes an API key. Only the owner can revoke their own key.
|
|
func RevokeAPIKey(db *gorm.DB, keyID, userID string) error {
|
|
result := db.Where("id = ? AND user_id = ?", keyID, userID).Delete(&UserAPIKey{})
|
|
if result.RowsAffected == 0 {
|
|
return fmt.Errorf("API key not found or not owned by user")
|
|
}
|
|
return result.Error
|
|
}
|
|
|
|
// CleanExpiredAPIKeys removes all API keys that have passed their expiry time.
|
|
func CleanExpiredAPIKeys(db *gorm.DB) error {
|
|
return db.Where("expires_at IS NOT NULL AND expires_at < ?", time.Now()).Delete(&UserAPIKey{}).Error
|
|
}
|