Files
ophion/internal/auth/handlers.go
bigtux 547619a1a7 feat: Add JWT authentication and API key security
- Add users table for dashboard authentication (email, password_hash, role)
- Add api_keys table for agent authentication (key_hash, prefix, name)
- Implement JWT auth with 24h expiration
- Implement API key auth with SHA256 hashing
- Add auth endpoints: POST /api/v1/auth/login, POST /api/v1/auth/register
- Add API key endpoints: GET/POST/DELETE /api/v1/api-keys
- Protect all /api/v1/* routes (except /health and /auth/*)
- Create default admin user (admin@ophion.local)
- First registered user automatically becomes admin
- Use bcrypt for password hashing (cost 12)
- Use SHA256 for API key hashing
- Add security headers middleware
2026-02-06 14:37:04 -03:00

458 lines
13 KiB
Go

package auth
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"log"
"os"
"time"
"github.com/bigtux/ophion/internal/security"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
)
// ═══════════════════════════════════════════════════════════
// 🔐 AUTH HANDLERS
// ═══════════════════════════════════════════════════════════
type AuthHandler struct {
db *sql.DB
authService *AuthService
}
type User struct {
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
}
type APIKey struct {
ID string `json:"id"`
KeyHash string `json:"-"`
Prefix string `json:"prefix"`
Name string `json:"name"`
UserID string `json:"user_id"`
CreatedAt time.Time `json:"created_at"`
LastUsed *time.Time `json:"last_used,omitempty"`
}
// NewAuthHandler cria novo handler de autenticação
func NewAuthHandler(db *sql.DB, jwtSecret string) *AuthHandler {
config := AuthConfig{
JWTSecret: []byte(jwtSecret),
JWTExpiration: 24 * time.Hour,
RefreshExpiration: 7 * 24 * time.Hour,
Issuer: "ophion",
}
// Implementação simples do APIKeyStore
store := &DBAPIKeyStore{db: db}
authService := NewAuthService(config, nil, store)
return &AuthHandler{
db: db,
authService: authService,
}
}
// GetAuthService retorna o serviço de autenticação
func (h *AuthHandler) GetAuthService() *AuthService {
return h.authService
}
// ─────────────────────────────────────────────────────────────
// Login
// ─────────────────────────────────────────────────────────────
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
ExpiresIn int `json:"expires_in"`
User struct {
ID string `json:"id"`
Email string `json:"email"`
Role string `json:"role"`
} `json:"user"`
}
func (h *AuthHandler) Login(c *fiber.Ctx) error {
var req LoginRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
}
if req.Email == "" || req.Password == "" {
return c.Status(400).JSON(fiber.Map{"error": "Email and password are required"})
}
// Sanitize email
email, err := security.SanitizeEmail(req.Email)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid email format"})
}
// Check brute force protection
allowed, remaining, _ := h.authService.CheckLoginAllowed(email)
if !allowed {
return c.Status(429).JSON(fiber.Map{
"error": "Too many failed attempts",
"message": "Account temporarily locked. Try again in " + remaining.String(),
})
}
// Find user
var user User
err = h.db.QueryRow(`
SELECT id, email, password_hash, role, created_at
FROM users WHERE email = $1
`, email).Scan(&user.ID, &user.Email, &user.PasswordHash, &user.Role, &user.CreatedAt)
if err == sql.ErrNoRows {
h.authService.RecordLoginAttempt(email, false)
return c.Status(401).JSON(fiber.Map{"error": "Invalid credentials"})
}
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Database error"})
}
// Verify password
if !security.VerifyPassword(req.Password, user.PasswordHash) {
blocked, remaining := h.authService.RecordLoginAttempt(email, false)
if blocked {
return c.Status(429).JSON(fiber.Map{
"error": "Too many failed attempts",
"message": "Account temporarily locked",
})
}
return c.Status(401).JSON(fiber.Map{
"error": "Invalid credentials",
"remaining_attempts": remaining,
})
}
// Successful login
h.authService.RecordLoginAttempt(email, true)
// Generate token
token, _, err := h.authService.GenerateTokenPair(user.ID, "", user.Email, user.Role)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to generate token"})
}
resp := LoginResponse{
Token: token,
ExpiresIn: 86400, // 24 hours
}
resp.User.ID = user.ID
resp.User.Email = user.Email
resp.User.Role = user.Role
return c.JSON(resp)
}
// ─────────────────────────────────────────────────────────────
// Register
// ─────────────────────────────────────────────────────────────
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func (h *AuthHandler) Register(c *fiber.Ctx) error {
var req RegisterRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
}
if req.Email == "" || req.Password == "" {
return c.Status(400).JSON(fiber.Map{"error": "Email and password are required"})
}
// Sanitize email
email, err := security.SanitizeEmail(req.Email)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid email format"})
}
// Check password (relaxed policy for simplicity)
if len(req.Password) < 6 {
return c.Status(400).JSON(fiber.Map{"error": "Password must be at least 6 characters"})
}
// Check if any users exist (first user becomes admin)
var userCount int
h.db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&userCount)
role := "user"
if userCount == 0 {
role = "admin"
}
// Check if email already exists
var existingID string
err = h.db.QueryRow(`SELECT id FROM users WHERE email = $1`, email).Scan(&existingID)
if err != sql.ErrNoRows {
return c.Status(409).JSON(fiber.Map{"error": "Email already registered"})
}
// Hash password
passwordHash, err := security.HashPassword(req.Password)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to hash password"})
}
// Create user
userID := uuid.New().String()
_, err = h.db.Exec(`
INSERT INTO users (id, email, password_hash, role, created_at)
VALUES ($1, $2, $3, $4, NOW())
`, userID, email, passwordHash, role)
if err != nil {
log.Printf("Error creating user: %v", err)
return c.Status(500).JSON(fiber.Map{"error": "Failed to create user"})
}
// Generate token
token, _, err := h.authService.GenerateTokenPair(userID, "", email, role)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to generate token"})
}
return c.Status(201).JSON(fiber.Map{
"message": "User registered successfully",
"token": token,
"user": fiber.Map{
"id": userID,
"email": email,
"role": role,
},
})
}
// ─────────────────────────────────────────────────────────────
// API Keys
// ─────────────────────────────────────────────────────────────
type CreateAPIKeyRequest struct {
Name string `json:"name"`
}
func (h *AuthHandler) CreateAPIKey(c *fiber.Ctx) error {
var req CreateAPIKeyRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Invalid request body"})
}
if req.Name == "" {
return c.Status(400).JSON(fiber.Map{"error": "Name is required"})
}
userID := GetUserID(c)
if userID == "" {
return c.Status(401).JSON(fiber.Map{"error": "Unauthorized"})
}
// Generate API key
key, keyHash, prefix := security.GenerateAPIKey()
keyID := uuid.New().String()
_, err := h.db.Exec(`
INSERT INTO api_keys (id, key_hash, prefix, name, user_id, created_at)
VALUES ($1, $2, $3, $4, $5, NOW())
`, keyID, keyHash, prefix, req.Name, userID)
if err != nil {
log.Printf("Error creating API key: %v", err)
return c.Status(500).JSON(fiber.Map{"error": "Failed to create API key"})
}
return c.Status(201).JSON(fiber.Map{
"message": "API key created successfully",
"key": key, // Only shown once!
"id": keyID,
"name": req.Name,
"prefix": prefix,
})
}
func (h *AuthHandler) ListAPIKeys(c *fiber.Ctx) error {
userID := GetUserID(c)
role := GetRole(c)
var rows *sql.Rows
var err error
// Admin can see all keys
if role == "admin" {
rows, err = h.db.Query(`
SELECT id, prefix, name, user_id, created_at, last_used
FROM api_keys ORDER BY created_at DESC
`)
} else {
rows, err = h.db.Query(`
SELECT id, prefix, name, user_id, created_at, last_used
FROM api_keys WHERE user_id = $1 ORDER BY created_at DESC
`, userID)
}
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Database error"})
}
defer rows.Close()
var keys []fiber.Map
for rows.Next() {
var k APIKey
if err := rows.Scan(&k.ID, &k.Prefix, &k.Name, &k.UserID, &k.CreatedAt, &k.LastUsed); err == nil {
keys = append(keys, fiber.Map{
"id": k.ID,
"prefix": k.Prefix,
"name": k.Name,
"user_id": k.UserID,
"created_at": k.CreatedAt,
"last_used": k.LastUsed,
})
}
}
if keys == nil {
keys = []fiber.Map{}
}
return c.JSON(fiber.Map{"api_keys": keys})
}
func (h *AuthHandler) DeleteAPIKey(c *fiber.Ctx) error {
keyID := c.Params("id")
userID := GetUserID(c)
role := GetRole(c)
var ownerID string
err := h.db.QueryRow(`SELECT user_id FROM api_keys WHERE id = $1`, keyID).Scan(&ownerID)
if err == sql.ErrNoRows {
return c.Status(404).JSON(fiber.Map{"error": "API key not found"})
}
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Database error"})
}
// Check permission
if role != "admin" && ownerID != userID {
return c.Status(403).JSON(fiber.Map{"error": "Permission denied"})
}
_, err = h.db.Exec(`DELETE FROM api_keys WHERE id = $1`, keyID)
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": "Failed to delete API key"})
}
return c.JSON(fiber.Map{"message": "API key deleted"})
}
// ─────────────────────────────────────────────────────────────
// Me (current user)
// ─────────────────────────────────────────────────────────────
func (h *AuthHandler) Me(c *fiber.Ctx) error {
userID := GetUserID(c)
email := c.Locals("email")
role := GetRole(c)
return c.JSON(fiber.Map{
"id": userID,
"email": email,
"role": role,
})
}
// ═══════════════════════════════════════════════════════════
// 🗄️ DB API KEY STORE
// ═══════════════════════════════════════════════════════════
type DBAPIKeyStore struct {
db *sql.DB
}
func (s *DBAPIKeyStore) ValidateKey(ctx context.Context, keyHash string) (*APIKeyInfo, error) {
var info APIKeyInfo
err := s.db.QueryRowContext(ctx, `
SELECT id, name FROM api_keys WHERE key_hash = $1
`, keyHash).Scan(&info.ID, &info.Name)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
info.Scopes = []string{"*"} // Full access for now
return &info, nil
}
func (s *DBAPIKeyStore) UpdateLastUsed(ctx context.Context, keyID string) error {
_, err := s.db.ExecContext(ctx, `
UPDATE api_keys SET last_used = NOW() WHERE id = $1
`, keyID)
return err
}
// ═══════════════════════════════════════════════════════════
// 🔧 ADMIN USER SETUP
// ═══════════════════════════════════════════════════════════
// CreateDefaultAdmin cria usuário admin padrão se não existir
func CreateDefaultAdmin(db *sql.DB) error {
// Check if any users exist
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count); err != nil {
// Table might not exist yet
return nil
}
if count > 0 {
return nil // Admin already exists
}
adminEmail := "admin@ophion.local"
adminPassword := os.Getenv("ADMIN_PASSWORD")
if adminPassword == "" {
adminPassword = "ophion123"
}
passwordHash, err := security.HashPassword(adminPassword)
if err != nil {
return err
}
userID := uuid.New().String()
_, err = db.Exec(`
INSERT INTO users (id, email, password_hash, role, created_at)
VALUES ($1, $2, $3, 'admin', NOW())
ON CONFLICT (email) DO NOTHING
`, userID, adminEmail, passwordHash)
if err != nil {
return err
}
log.Printf("✓ Default admin user created: %s", adminEmail)
return nil
}
// HashAPIKey creates SHA256 hash of API key
func HashAPIKey(key string) string {
hash := sha256.Sum256([]byte(key))
return hex.EncodeToString(hash[:])
}