Skip to content

Authentication Example

This example demonstrates how to implement JWT-based authentication in a GOE application.

JWT Service

JWT Configuration

go
package jwt

import (
    "time"
    "go.oease.dev/goe/v2/contract"
)

type JWTConfig struct {
    Secret     string
    Expiration time.Duration
    Issuer     string
}

func NewJWTConfig(config contract.Config) *JWTConfig {
    return &JWTConfig{
        Secret:     config.GetString("JWT_SECRET"),
        Expiration: config.GetDurationWithDefault("JWT_EXPIRATION", 24*time.Hour),
        Issuer:     config.GetStringWithDefault("JWT_ISSUER", "goe-app"),
    }
}

JWT Service Implementation

go
package jwt

import (
    "errors"
    "time"
    "github.com/golang-jwt/jwt/v5"
    "go.oease.dev/goe/v2/contract"
)

type JWTService interface {
    GenerateToken(userID uint, email string) (string, error)
    ValidateToken(tokenString string) (*Claims, error)
}

type Claims struct {
    UserID uint   `json:"user_id"`
    Email  string `json:"email"`
    jwt.RegisteredClaims
}

type jwtService struct {
    config *JWTConfig
    logger contract.Logger
}

func NewJWTService(config *JWTConfig, logger contract.Logger) JWTService {
    return &jwtService{
        config: config,
        logger: logger,
    }
}

func (s *jwtService) GenerateToken(userID uint, email string) (string, error) {
    claims := &Claims{
        UserID: userID,
        Email:  email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(s.config.Expiration)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            NotBefore: jwt.NewNumericDate(time.Now()),
            Issuer:    s.config.Issuer,
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString([]byte(s.config.Secret))
    
    if err != nil {
        s.logger.Error("Failed to generate JWT token", "error", err)
        return "", err
    }
    
    s.logger.Info("JWT token generated", "user_id", userID, "email", email)
    return tokenString, nil
}

func (s *jwtService) 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, errors.New("invalid signing method")
        }
        return []byte(s.config.Secret), nil
    })
    
    if err != nil {
        s.logger.Error("Failed to parse JWT token", "error", err)
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, errors.New("invalid token")
}

Authentication Service

User Model

go
package model

import (
    "time"
    "gorm.io/gorm"
    "golang.org/x/crypto/bcrypt"
)

type User struct {
    ID        uint           `gorm:"primaryKey" json:"id"`
    Email     string         `gorm:"size:100;uniqueIndex;not null" json:"email"`
    Password  string         `gorm:"size:255;not null" json:"-"`
    Name      string         `gorm:"size:100;not null" json:"name"`
    Role      string         `gorm:"size:50;default:user" json:"role"`
    Active    bool           `gorm:"default:true" json:"active"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

func (u *User) CheckPassword(password string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
    return err == nil
}

func (u *User) SetPassword(password string) error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    u.Password = string(hashedPassword)
    return nil
}

Authentication Service

go
package service

import (
    "errors"
    "go.oease.dev/goe/v2/contract"
    "your-app/jwt"
    "your-app/model"
    "your-app/repository"
)

var (
    ErrInvalidCredentials = errors.New("invalid credentials")
    ErrUserNotFound      = errors.New("user not found")
    ErrUserInactive      = errors.New("user is inactive")
)

type AuthService struct {
    userRepo   *repository.UserRepository
    jwtService jwt.JWTService
    logger     contract.Logger
}

func NewAuthService(userRepo *repository.UserRepository, jwtService jwt.JWTService, logger contract.Logger) *AuthService {
    return &AuthService{
        userRepo:   userRepo,
        jwtService: jwtService,
        logger:     logger,
    }
}

func (s *AuthService) Register(name, email, password string) (*model.User, string, error) {
    s.logger.Info("User registration attempt", "email", email)
    
    // Check if user already exists
    if existingUser, err := s.userRepo.GetByEmail(email); err == nil && existingUser != nil {
        return nil, "", errors.New("user already exists")
    }
    
    // Create new user
    user := &model.User{
        Name:   name,
        Email:  email,
        Role:   "user",
        Active: true,
    }
    
    if err := user.SetPassword(password); err != nil {
        s.logger.Error("Failed to hash password", "error", err)
        return nil, "", err
    }
    
    if err := s.userRepo.Create(user); err != nil {
        s.logger.Error("Failed to create user", "error", err)
        return nil, "", err
    }
    
    // Generate JWT token
    token, err := s.jwtService.GenerateToken(user.ID, user.Email)
    if err != nil {
        s.logger.Error("Failed to generate token", "error", err)
        return nil, "", err
    }
    
    s.logger.Info("User registered successfully", "user_id", user.ID, "email", email)
    return user, token, nil
}

func (s *AuthService) Login(email, password string) (*model.User, string, error) {
    s.logger.Info("User login attempt", "email", email)
    
    // Find user by email
    user, err := s.userRepo.GetByEmail(email)
    if err != nil {
        s.logger.Error("User not found", "email", email, "error", err)
        return nil, "", ErrUserNotFound
    }
    
    // Check if user is active
    if !user.Active {
        s.logger.Warn("Login attempt for inactive user", "email", email)
        return nil, "", ErrUserInactive
    }
    
    // Check password
    if !user.CheckPassword(password) {
        s.logger.Warn("Invalid password", "email", email)
        return nil, "", ErrInvalidCredentials
    }
    
    // Generate JWT token
    token, err := s.jwtService.GenerateToken(user.ID, user.Email)
    if err != nil {
        s.logger.Error("Failed to generate token", "error", err)
        return nil, "", err
    }
    
    s.logger.Info("User logged in successfully", "user_id", user.ID, "email", email)
    return user, token, nil
}

func (s *AuthService) ValidateToken(tokenString string) (*model.User, error) {
    claims, err := s.jwtService.ValidateToken(tokenString)
    if err != nil {
        return nil, err
    }
    
    user, err := s.userRepo.GetByID(claims.UserID)
    if err != nil {
        return nil, err
    }
    
    if !user.Active {
        return nil, ErrUserInactive
    }
    
    return user, nil
}

Authentication Middleware

go
package middleware

import (
    "strings"
    "github.com/gofiber/fiber/v3"
    "go.oease.dev/goe/v2/contract"
    "your-app/service"
)

type AuthMiddleware struct {
    authService *service.AuthService
    logger      contract.Logger
}

func NewAuthMiddleware(authService *service.AuthService, logger contract.Logger) *AuthMiddleware {
    return &AuthMiddleware{
        authService: authService,
        logger:      logger,
    }
}

func (m *AuthMiddleware) RequireAuth(c fiber.Ctx) error {
    // Get token from header
    authHeader := c.Get("Authorization")
    if authHeader == "" {
        return c.Status(401).JSON(fiber.Map{
            "error": "Authorization header required",
        })
    }
    
    // Extract token from "Bearer <token>"
    tokenParts := strings.Split(authHeader, " ")
    if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
        return c.Status(401).JSON(fiber.Map{
            "error": "Invalid authorization header format",
        })
    }
    
    token := tokenParts[1]
    
    // Validate token
    user, err := m.authService.ValidateToken(token)
    if err != nil {
        m.logger.Warn("Invalid token", "error", err)
        return c.Status(401).JSON(fiber.Map{
            "error": "Invalid token",
        })
    }
    
    // Store user in context
    c.Locals("user", user)
    c.Locals("user_id", user.ID)
    
    return c.Next()
}

func (m *AuthMiddleware) RequireRole(role string) fiber.Handler {
    return func(c fiber.Ctx) error {
        user, ok := c.Locals("user").(*model.User)
        if !ok {
            return c.Status(401).JSON(fiber.Map{
                "error": "Authentication required",
            })
        }
        
        if user.Role != role {
            return c.Status(403).JSON(fiber.Map{
                "error": "Insufficient permissions",
            })
        }
        
        return c.Next()
    }
}

func (m *AuthMiddleware) OptionalAuth(c fiber.Ctx) error {
    authHeader := c.Get("Authorization")
    if authHeader == "" {
        return c.Next()
    }
    
    tokenParts := strings.Split(authHeader, " ")
    if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
        return c.Next()
    }
    
    token := tokenParts[1]
    
    user, err := m.authService.ValidateToken(token)
    if err != nil {
        return c.Next()
    }
    
    c.Locals("user", user)
    c.Locals("user_id", user.ID)
    
    return c.Next()
}

Authentication Handlers

go
package handler

import (
    "github.com/gofiber/fiber/v3"
    "go.oease.dev/goe/v2/contract"
    "your-app/model"
    "your-app/service"
)

type AuthHandler struct {
    authService *service.AuthService
    logger      contract.Logger
}

func NewAuthHandler(authService *service.AuthService, logger contract.Logger) *AuthHandler {
    return &AuthHandler{
        authService: authService,
        logger:      logger,
    }
}

type RegisterRequest struct {
    Name     string `json:"name" validate:"required,min=2,max=100"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8"`
}

type LoginRequest struct {
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required"`
}

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 err := validate.Struct(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Validation failed",
            "details": err.Error(),
        })
    }
    
    user, token, err := h.authService.Register(req.Name, req.Email, req.Password)
    if err != nil {
        h.logger.Error("Registration failed", "error", err)
        return c.Status(400).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    
    return c.Status(201).JSON(fiber.Map{
        "user":  user,
        "token": token,
    })
}

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 err := validate.Struct(&req); err != nil {
        return c.Status(400).JSON(fiber.Map{
            "error": "Validation failed",
            "details": err.Error(),
        })
    }
    
    user, token, err := h.authService.Login(req.Email, req.Password)
    if err != nil {
        h.logger.Error("Login failed", "error", err)
        return c.Status(401).JSON(fiber.Map{
            "error": err.Error(),
        })
    }
    
    return c.JSON(fiber.Map{
        "user":  user,
        "token": token,
    })
}

func (h *AuthHandler) Profile(c fiber.Ctx) error {
    user, ok := c.Locals("user").(*model.User)
    if !ok {
        return c.Status(401).JSON(fiber.Map{
            "error": "Authentication required",
        })
    }
    
    return c.JSON(user)
}

func (h *AuthHandler) RefreshToken(c fiber.Ctx) error {
    user, ok := c.Locals("user").(*model.User)
    if !ok {
        return c.Status(401).JSON(fiber.Map{
            "error": "Authentication required",
        })
    }
    
    token, err := h.authService.jwtService.GenerateToken(user.ID, user.Email)
    if err != nil {
        return c.Status(500).JSON(fiber.Map{
            "error": "Failed to refresh token",
        })
    }
    
    return c.JSON(fiber.Map{
        "token": token,
    })
}

Main Application

go
package main

import (
    "go.oease.dev/goe/v2"
    "go.oease.dev/goe/v2/contract"
    "your-app/handler"
    "your-app/jwt"
    "your-app/middleware"
    "your-app/model"
    "your-app/repository"
    "your-app/service"
)

func main() {
    goe.New(goe.Options{
        WithHTTP: true,
        WithDB:   true,
        Providers: []any{
            jwt.NewJWTConfig,
            jwt.NewJWTService,
            repository.NewUserRepository,
            service.NewAuthService,
            handler.NewAuthHandler,
            middleware.NewAuthMiddleware,
        },
        Invokers: []any{
            AutoMigrate,
            RegisterRoutes,
        },
    })
    
    goe.Run()
}

func AutoMigrate(db contract.DB) {
    db.Instance().AutoMigrate(&model.User{})
}

func RegisterRoutes(
    httpKernel contract.HTTPKernel,
    authHandler *handler.AuthHandler,
    authMiddleware *middleware.AuthMiddleware,
) {
    app := httpKernel.App()
    
    // Public routes
    auth := app.Group("/auth")
    auth.Post("/register", authHandler.Register)
    auth.Post("/login", authHandler.Login)
    
    // Protected routes
    api := app.Group("/api")
    api.Use(authMiddleware.RequireAuth)
    api.Get("/profile", authHandler.Profile)
    api.Post("/refresh", authHandler.RefreshToken)
    
    // Admin routes
    admin := api.Group("/admin")
    admin.Use(authMiddleware.RequireRole("admin"))
    admin.Get("/users", func(c fiber.Ctx) error {
        return c.JSON(fiber.Map{"message": "Admin only endpoint"})
    })
}

Configuration

bash
# .env
JWT_SECRET=your-super-secret-jwt-key-here
JWT_EXPIRATION=24h
JWT_ISSUER=your-app-name

HTTP_PORT=8080
DB_DRIVER=sqlite
DB_PATH=./database.db
LOG_LEVEL=info

Testing the Authentication

Register a new user

bash
curl -X POST http://localhost:8080/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "password123"
  }'

Login

bash
curl -X POST http://localhost:8080/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "password": "password123"
  }'

Access protected route

bash
curl -X GET http://localhost:8080/api/profile \
  -H "Authorization: Bearer YOUR_JWT_TOKEN_HERE"

Testing Authentication

go
package service

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "your-app/model"
)

type MockUserRepository struct {
    mock.Mock
}

func (m *MockUserRepository) GetByEmail(email string) (*model.User, error) {
    args := m.Called(email)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*model.User), args.Error(1)
}

func (m *MockUserRepository) Create(user *model.User) error {
    args := m.Called(user)
    return args.Error(0)
}

func TestAuthService_Login(t *testing.T) {
    // Setup
    mockRepo := new(MockUserRepository)
    mockJWT := new(MockJWTService)
    mockLogger := new(MockLogger)
    
    service := NewAuthService(mockRepo, mockJWT, mockLogger)
    
    // Create test user
    user := &model.User{
        ID:     1,
        Email:  "test@example.com",
        Active: true,
    }
    user.SetPassword("password123")
    
    mockRepo.On("GetByEmail", "test@example.com").Return(user, nil)
    mockJWT.On("GenerateToken", uint(1), "test@example.com").Return("token123", nil)
    mockLogger.On("Info", mock.Anything, mock.Anything, mock.Anything)
    
    // Execute
    resultUser, token, err := service.Login("test@example.com", "password123")
    
    // Assert
    assert.NoError(t, err)
    assert.Equal(t, user, resultUser)
    assert.Equal(t, "token123", token)
    
    mockRepo.AssertExpectations(t)
    mockJWT.AssertExpectations(t)
}

This example demonstrates:

  • JWT token generation and validation
  • User registration and login
  • Password hashing and verification
  • Authentication middleware
  • Role-based access control
  • Protected routes
  • Proper error handling
  • Testing authentication logic

Released under the MIT License.