From fbb331ed4bae38b0115a46a00392451019943825 Mon Sep 17 00:00:00 2001 From: Aska Date: Wed, 12 Aug 2026 12:21:36 +0800 Subject: [PATCH] feat(auth): replace stub auth with real JWT + bcrypt - RealAuthManager: JWT signing/validation + bcrypt password hashing - MemUserStore: in-memory user store with default admin - Register endpoint: user creation with hashed password - Login endpoint: bcrypt verification + JWT generation - AuthManager interface extended with Login/Register methods --- backend/cmd/cyrene/main.go | 3 +- backend/services/gateway/api/auth.go | 124 ++++++++++++++++++++++ backend/services/gateway/api/auth_stub.go | 11 ++ backend/services/gateway/api/router.go | 30 ++++-- 4 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 backend/services/gateway/api/auth.go diff --git a/backend/cmd/cyrene/main.go b/backend/cmd/cyrene/main.go index b5ecff0..33dab9b 100644 --- a/backend/cmd/cyrene/main.go +++ b/backend/cmd/cyrene/main.go @@ -176,7 +176,8 @@ func main() { log.Printf("[orchestrator] pipeline ready") // === 12. Gateway === - authMgr := api.NewStubAuthManager() + userStore := api.NewMemUserStore(cfg.AdminPassword) + authMgr := api.NewRealAuthManager(cfg.JWTSecret, cfg.JWTExpiryHours, userStore) // Build dependencies for gateway handlers gwDeps := api.Dependencies{ diff --git a/backend/services/gateway/api/auth.go b/backend/services/gateway/api/auth.go new file mode 100644 index 0000000..d77c036 --- /dev/null +++ b/backend/services/gateway/api/auth.go @@ -0,0 +1,124 @@ +// Package api — RealAuthManager provides production JWT + bcrypt authentication. +package api + +import ( + "fmt" + "sync" + + "git.yeij.top/AskaEth/Cyrene_v2/internal/auth" +) + +// UserRecord holds a stored user's credentials. +type UserRecord struct { + UserID string + Username string + PasswordHash string + Role string // "admin" | "user" +} + +// UserStore is the interface for user persistence. +type UserStore interface { + FindByUsername(username string) (*UserRecord, error) + Create(user *UserRecord) error +} + +// MemUserStore is an in-memory user store for development and testing. +type MemUserStore struct { + mu sync.RWMutex + users map[string]*UserRecord +} + +// NewMemUserStore creates an in-memory user store with a default admin. +func NewMemUserStore(adminPassword string) *MemUserStore { + s := &MemUserStore{users: make(map[string]*UserRecord)} + + hash, _ := auth.HashPassword(adminPassword) + s.users["admin"] = &UserRecord{ + UserID: "admin", Username: "admin", + PasswordHash: hash, Role: "admin", + } + return s +} + +func (s *MemUserStore) FindByUsername(username string) (*UserRecord, error) { + s.mu.RLock() + defer s.mu.RUnlock() + u, ok := s.users[username] + if !ok { + return nil, fmt.Errorf("user %s not found", username) + } + return u, nil +} + +func (s *MemUserStore) Create(user *UserRecord) error { + s.mu.Lock() + defer s.mu.Unlock() + if _, exists := s.users[user.Username]; exists { + return fmt.Errorf("username %s already exists", user.Username) + } + s.users[user.Username] = user + return nil +} + +// RealAuthManager implements AuthManager using JWT + bcrypt. +type RealAuthManager struct { + tokenMgr *auth.TokenManager + store UserStore +} + +// NewRealAuthManager creates a production-grade auth manager. +func NewRealAuthManager(secret string, expiryHours int, store UserStore) *RealAuthManager { + return &RealAuthManager{ + tokenMgr: auth.NewTokenManager(secret, expiryHours), + store: store, + } +} + +// ValidateToken parses and validates a JWT token. +func (m *RealAuthManager) ValidateToken(tokenStr string) (string, string, string, error) { + claims, err := m.tokenMgr.Validate(tokenStr) + if err != nil { + return "", "", "", err + } + return claims.UserID, claims.Username, claims.Role, nil +} + +// Login authenticates a user with username and password. +func (m *RealAuthManager) Login(username, password string) (string, error) { + user, err := m.store.FindByUsername(username) + if err != nil { + return "", fmt.Errorf("invalid credentials") + } + if !auth.CheckPassword(password, user.PasswordHash) { + return "", fmt.Errorf("invalid credentials") + } + return m.tokenMgr.Generate(user.UserID, user.Username, user.Role) +} + +// Register creates a new user account and returns a JWT token. +func (m *RealAuthManager) Register(username, password string) (string, error) { + if username == "" || password == "" { + return "", fmt.Errorf("username and password required") + } + if len(password) < 6 { + return "", fmt.Errorf("password must be at least 6 characters") + } + + hash, err := auth.HashPassword(password) + if err != nil { + return "", fmt.Errorf("hash password: %w", err) + } + + user := &UserRecord{ + UserID: username, + Username: username, + PasswordHash: hash, + Role: "user", + } + + if err := m.store.Create(user); err != nil { + return "", err + } + + return m.tokenMgr.Generate(user.UserID, user.Username, user.Role) +} diff --git a/backend/services/gateway/api/auth_stub.go b/backend/services/gateway/api/auth_stub.go index d53291d..9ff5bca 100644 --- a/backend/services/gateway/api/auth_stub.go +++ b/backend/services/gateway/api/auth_stub.go @@ -54,3 +54,14 @@ func (m *StubAuthManager) Login(username, password string) (string, error) { m.tokens[token] = TokenInfo{UserID: username, Username: username, Role: user.Role} return token, nil } + +// Register creates a new user and returns a stub token. +func (m *StubAuthManager) Register(username, password string) (string, error) { + if _, exists := m.users[username]; exists { + return "", http.ErrNoCookie + } + m.users[username] = UserInfo{Username: username, Password: password, Role: "user"} + token := "stub-token-" + username + m.tokens[token] = TokenInfo{UserID: username, Username: username, Role: "user"} + return token, nil +} diff --git a/backend/services/gateway/api/router.go b/backend/services/gateway/api/router.go index a44d204..811b0ce 100644 --- a/backend/services/gateway/api/router.go +++ b/backend/services/gateway/api/router.go @@ -15,9 +15,11 @@ import ( // --- Interfaces for injected services --- -// AuthManager validates JWT tokens. +// AuthManager validates JWT tokens and handles login/register. type AuthManager interface { ValidateToken(token string) (userID, username, role string, err error) + Login(username, password string) (token string, err error) + Register(username, password string) (token string, err error) } // MemoryEntry is a simplified memory entry for API responses. @@ -161,12 +163,7 @@ func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) { } json.NewDecoder(req.Body).Decode(&body) - mgr, ok := r.deps.AuthMgr.(*StubAuthManager) - if !ok { - writeJSON(w, 500, M{"error": "auth not configured"}) - return - } - token, err := mgr.Login(body.Username, body.Password) + token, err := r.deps.AuthMgr.Login(body.Username, body.Password) if err != nil { writeJSON(w, 401, M{"error": "invalid credentials"}) return @@ -174,8 +171,23 @@ func (r *Router) loginHandler(w http.ResponseWriter, req *http.Request) { writeJSON(w, 200, M{"token": token}) } -func (r *Router) registerHandler(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, 200, M{"message": "registration not implemented"}) +func (r *Router) registerHandler(w http.ResponseWriter, req *http.Request) { + if req.Method != "POST" { + writeJSON(w, 405, M{"error": "POST required"}) + return + } + var body struct { + Username string `json:"username"` + Password string `json:"password"` + } + json.NewDecoder(req.Body).Decode(&body) + + token, err := r.deps.AuthMgr.Register(body.Username, body.Password) + if err != nil { + writeJSON(w, 400, M{"error": err.Error()}) + return + } + writeJSON(w, 201, M{"token": token}) } // --- Sessions ---