package auth import ( "crypto/rand" "encoding/hex" "errors" "net/http" "os" "time" "github.com/golang-jwt/jwt/v5" "golang.org/x/crypto/bcrypt" ) var ( ErrInvalidCredentials = errors.New("invalid email or password") ErrInvalidToken = errors.New("invalid or expired token") ErrUserNotFound = errors.New("user not found") ErrUserExists = errors.New("user already exists") ) // Config holds auth configuration type Config struct { JWTSecret []byte CookieDomain string CookieSecure bool TokenDuration time.Duration } // User represents an authenticated user type User struct { ID string `json:"id"` Email string `json:"email"` Password string `json:"-"` // Never expose password hash CompanyDetails string `json:"company_details"` BankDetails string `json:"bank_details"` CreatedAt string `json:"created_at"` } // Claims represents JWT claims type Claims struct { UserID string `json:"user_id"` Email string `json:"email"` jwt.RegisteredClaims } // Service handles authentication type Service struct { config Config users UserStore } // UserStore interface for user persistence type UserStore interface { CreateUser(email, passwordHash string) (*User, error) GetUserByEmail(email string) (*User, error) GetUserByID(id string) (*User, error) } // NewService creates a new auth service func NewService(users UserStore) *Service { secret := os.Getenv("JWT_SECRET") if secret == "" { // Generate a random secret if not provided (not recommended for production) b := make([]byte, 32) rand.Read(b) secret = hex.EncodeToString(b) } domain := os.Getenv("COOKIE_DOMAIN") secure := os.Getenv("COOKIE_SECURE") == "true" return &Service{ config: Config{ JWTSecret: []byte(secret), CookieDomain: domain, CookieSecure: secure, TokenDuration: 24 * time.Hour, }, users: users, } } // HashPassword hashes a password using bcrypt with high cost func HashPassword(password string) (string, error) { // Use cost of 12 for good security/performance balance hash, err := bcrypt.GenerateFromPassword([]byte(password), 12) if err != nil { return "", err } return string(hash), nil } // CheckPassword verifies a password against a hash func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } // Register creates a new user account func (s *Service) Register(email, password string) (*User, error) { // Check if user exists existing, _ := s.users.GetUserByEmail(email) if existing != nil { return nil, ErrUserExists } // Validate password strength if len(password) < 8 { return nil, errors.New("password must be at least 8 characters") } // Hash password hash, err := HashPassword(password) if err != nil { return nil, err } return s.users.CreateUser(email, hash) } // Login authenticates a user and returns a JWT token func (s *Service) Login(email, password string) (string, error) { user, err := s.users.GetUserByEmail(email) if err != nil || user == nil { return "", ErrInvalidCredentials } if !CheckPassword(password, user.Password) { return "", ErrInvalidCredentials } return s.generateToken(user) } // generateToken creates a new JWT token for a user func (s *Service) generateToken(user *User) (string, error) { now := time.Now() claims := &Claims{ UserID: user.ID, Email: user.Email, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(now.Add(s.config.TokenDuration)), IssuedAt: jwt.NewNumericDate(now), NotBefore: jwt.NewNumericDate(now), Issuer: "billit", Subject: user.ID, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString(s.config.JWTSecret) } // ValidateToken validates a JWT token and returns the claims func (s *Service) ValidateToken(tokenString string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, ErrInvalidToken } return s.config.JWTSecret, nil }) if err != nil { return nil, ErrInvalidToken } if claims, ok := token.Claims.(*Claims); ok && token.Valid { return claims, nil } return nil, ErrInvalidToken } // CreateAuthCookie creates an HTTP-only secure cookie for the token func (s *Service) CreateAuthCookie(token string) *http.Cookie { return &http.Cookie{ Name: "auth_token", Value: token, Path: "/", Domain: s.config.CookieDomain, MaxAge: int(s.config.TokenDuration.Seconds()), HttpOnly: true, // Prevents JavaScript access Secure: s.config.CookieSecure, // Only send over HTTPS in production SameSite: http.SameSiteStrictMode, } } // ClearAuthCookie returns a cookie that clears the auth token func (s *Service) ClearAuthCookie() *http.Cookie { return &http.Cookie{ Name: "auth_token", Value: "", Path: "/", Domain: s.config.CookieDomain, MaxAge: -1, HttpOnly: true, Secure: s.config.CookieSecure, SameSite: http.SameSiteStrictMode, } } // GetUserFromToken retrieves the user from a valid token func (s *Service) GetUserFromToken(tokenString string) (*User, error) { claims, err := s.ValidateToken(tokenString) if err != nil { return nil, err } return s.users.GetUserByID(claims.UserID) }