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
This commit is contained in:
1 parent
6811480786
commit
fbb331ed4b
4 files changed
+158
-10
No files matched your search
@@ -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{
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 ---
|
||||
|
||||
Reference in new issue
Block a user