package auth import ( "crypto/rand" "encoding/hex" "errors" "fmt" "sync" "time" "github.com/brasse-b/archivum/internal/config" "github.com/brasse-b/archivum/internal/db" "github.com/go-ldap/ldap/v3" "golang.org/x/crypto/bcrypt" ) // Session represents an authenticated user session held in memory. type Session struct { Username string Token string Role string ExpiresAt time.Time } // Manager handles session creation and validation. type Manager struct { cfg *config.Config mu sync.RWMutex sessions map[string]*Session } func NewManager(cfg *config.Config) *Manager { return &Manager{ cfg: cfg, sessions: make(map[string]*Session), } } // UpdateConfig replaces the stored config (used after LDAP config change). func (m *Manager) UpdateConfig(cfg *config.Config) { m.mu.Lock() m.cfg = cfg m.mu.Unlock() } // HashPassword returns a bcrypt hash of the given password. func HashPassword(password string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(b), err } // CheckPassword compares a bcrypt hashed password with its possible // plaintext equivalent. Returns nil on success, or an error on failure. func CheckPassword(hashedPassword, password string) error { return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) } // Login authenticates via local DB first, then falls back to LDAP if configured. // Returns the session token, the user's role, and any error. func (m *Manager) Login(database *db.DB, username, password string) (token, role string, err error) { // Try local account first. user, dbErr := database.GetUser(username) if dbErr == nil { if bcrypt.CompareHashAndPassword([]byte(user.PassHash), []byte(password)) != nil { return "", "", errors.New("invalid credentials") } tok, err := m.createSession(username, user.Role) return tok, user.Role, err } // If not found locally and LDAP is configured, try LDAP. m.mu.RLock() cfg := m.cfg m.mu.RUnlock() if cfg == nil || cfg.LDAP.Host == "" { return "", "", errors.New("invalid credentials") } if err := ldapBind(cfg.LDAP.Host, cfg.LDAP.Port, cfg.LDAP.BindDN, cfg.LDAP.BindPassword); err != nil { return "", "", errors.New("invalid credentials") } if err := ldapUserBind(cfg, username, password); err != nil { return "", "", errors.New("invalid credentials") } tok, err := m.createSession(username, "user") return tok, "user", err } // TestLDAP tests an LDAP configuration by performing a service-account bind. func TestLDAP(host string, port int, bindDN, bindPassword string) error { return ldapBind(host, port, bindDN, bindPassword) } func ldapBind(host string, port int, bindDN, bindPassword string) error { l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", host, port)) if err != nil { return err } defer l.Close() return l.Bind(bindDN, bindPassword) } func ldapUserBind(cfg *config.Config, username, password string) error { l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", cfg.LDAP.Host, cfg.LDAP.Port)) if err != nil { return err } defer l.Close() if err := l.Bind(cfg.LDAP.BindDN, cfg.LDAP.BindPassword); err != nil { return err } sr, err := l.Search(&ldap.SearchRequest{ BaseDN: cfg.LDAP.BaseDN, Filter: fmt.Sprintf("(uid=%s)", ldap.EscapeFilter(username)), Scope: ldap.ScopeWholeSubtree, }) if err != nil { return err } if len(sr.Entries) != 1 { return errors.New("user not found in LDAP") } return l.Bind(sr.Entries[0].DN, password) } func (m *Manager) createSession(username, role string) (string, error) { token, err := generateToken() if err != nil { return "", err } m.mu.Lock() m.sessions[token] = &Session{ Username: username, Token: token, Role: role, ExpiresAt: time.Now().Add(24 * time.Hour), } m.mu.Unlock() return token, nil } // Validate looks up a token and returns its session if valid and not expired. func (m *Manager) Validate(token string) (*Session, error) { m.mu.RLock() s, ok := m.sessions[token] m.mu.RUnlock() if !ok { return nil, errors.New("invalid token") } if time.Now().After(s.ExpiresAt) { m.mu.Lock() delete(m.sessions, token) m.mu.Unlock() return nil, errors.New("token expired") } return s, nil } // Logout removes a session. func (m *Manager) Logout(token string) { m.mu.Lock() delete(m.sessions, token) m.mu.Unlock() } func generateToken() (string, error) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "", err } return hex.EncodeToString(b), nil }