package auth import ( "crypto/rand" "encoding/base64" "encoding/json" "net/http" "strings" "sync" "time" ) // Credentials for HTTP Basic and fleet bearer auth. type Credentials struct { BasicUsername string BasicPassword string FleetSecret string } // TicketStore issues short-lived WebSocket tickets after Basic login. type TicketStore struct { mu sync.Mutex tickets map[string]time.Time ttl time.Duration } // NewTicketStore creates a ticket store with the default TTL. func NewTicketStore(ttl ...time.Duration) *TicketStore { d := 5 * time.Minute if len(ttl) > 0 && ttl[0] > 0 { d = ttl[0] } return &TicketStore{ tickets: make(map[string]time.Time), ttl: d, } } func (s *TicketStore) Issue() (string, error) { b := make([]byte, 24) if _, err := rand.Read(b); err != nil { return "", err } ticket := base64.RawURLEncoding.EncodeToString(b) s.mu.Lock() defer s.mu.Unlock() s.tickets[ticket] = time.Now().Add(s.ttl) s.gcLocked() return ticket, nil } func (s *TicketStore) Validate(ticket string) bool { s.mu.Lock() defer s.mu.Unlock() exp, ok := s.tickets[ticket] if !ok || time.Now().After(exp) { delete(s.tickets, ticket) return false } return true } // Consume validates and removes a one-time ticket. func (s *TicketStore) Consume(ticket string) bool { s.mu.Lock() defer s.mu.Unlock() exp, ok := s.tickets[ticket] if !ok || time.Now().After(exp) { delete(s.tickets, ticket) return false } delete(s.tickets, ticket) return true } func (s *TicketStore) gcLocked() { now := time.Now() for k, exp := range s.tickets { if now.After(exp) { delete(s.tickets, k) } } } // ExtractBearer returns the token from an Authorization Bearer header. func ExtractBearer(r *http.Request) string { auth := r.Header.Get("Authorization") if !strings.HasPrefix(auth, "Bearer ") { return "" } return strings.TrimPrefix(auth, "Bearer ") } // BasicAuthMiddleware protects routes with HTTP Basic credentials. func BasicAuthMiddleware(username, password string) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user, pass, ok := r.BasicAuth() if !ok || user != username || pass != password { w.Header().Set("WWW-Authenticate", `Basic realm="forge-mesh"`) http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } } // BasicAuthMiddlewareCreds is the struct-based variant for router wiring. func BasicAuthMiddlewareCreds(creds Credentials) func(http.Handler) http.Handler { return BasicAuthMiddleware(creds.BasicUsername, creds.BasicPassword) } // FleetSecretMiddleware validates agent bearer tokens. func FleetSecretMiddleware(secret string, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := ExtractBearer(r) if token == "" || token != secret { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } // JSON writes a JSON response. func JSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) }