Skip to main content

Overview

The authentication system uses JWT (JSON Web Tokens) with bcrypt password hashing. Tokens are stored in HTTP-only cookies for security.

Architecture

Authentication is handled by the auth package with the following components:
  • auth.go - JWT token generation and cookie management
  • controller.go - Business logic for login and registration
  • routes.go - HTTP handlers for auth endpoints
  • configs.go - Session store configuration

JWT Token Generation

Claims Structure

The JWT token contains custom claims defined in auth/auth.go:22-26:
auth/auth.go
type Claims struct {
    Name   string `json:"name"`
    UserId string `json:"user_id"`
    jwt.Claims
}
Name
string
User’s display name
UserId
string
Unique user identifier (UUID)
jwt.Claims
jwt.MapClaims
Standard JWT claims including expiration time

Token Generation Flow

The GenerateTokensAndSetCookies function in auth/auth.go:29-39 orchestrates token creation:
auth/auth.go
func GenerateTokensAndSetCookies(user *users.User, c echo.Context) (string, error) {
    accessToken, exp, err := generateAccessToken(user)
    if err != nil {
        return "", err
    }

    setTokenCookie(accessTokenCookieName, accessToken, exp, c)
    setUserCookie(user, exp, c)

    return accessToken, nil
}

Access Token Creation

Tokens are valid for 24 hours (auth/auth.go:41-46):
auth/auth.go
func generateAccessToken(user *users.User) (string, time.Time, error) {
    // Declare the expiration time of the token (1h).
    expirationTime := time.Now().Add(24 * time.Hour)

    return generateToken(user, expirationTime, []byte(GetJWTSecret()))
}
Despite the comment saying “1h”, the actual expiration is set to 24 hours.

Core Token Generation

The main JWT signing logic in auth/auth.go:49-68:
auth/auth.go
func generateToken(user *users.User, expirationTime time.Time, secret []byte) (string, time.Time, error) {
    // Create the JWT claims, which includes the username and expiry time.
    claims := &Claims{
        UserId: user.ID,
        Claims: jwt.MapClaims{
            "exp": expirationTime.Unix(),
        },
    }

    // Declare the token with the HS256 algorithm used for signing, and the claims.
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

    // Create the JWT string.
    tokenString, err := token.SignedString(secret)
    if err != nil {
        return "", time.Now(), err
    }

    return tokenString, expirationTime, nil
}
Key Details:
  • Algorithm: HS256 (HMAC with SHA-256)
  • Secret: Retrieved from SESSION_KEY environment variable
  • Claims: User ID and expiration timestamp
The JWT token is stored in an HTTP-only cookie (auth/auth.go:71-81):
auth/auth.go
func setTokenCookie(name, token string, expiration time.Time, c echo.Context) {
    cookie := new(http.Cookie)
    cookie.Name = name
    cookie.Value = token
    cookie.Expires = expiration
    cookie.Path = "/"
    // Http-only helps mitigate the risk of client side script accessing the protected cookie.
    cookie.HttpOnly = true

    c.SetCookie(cookie)
}
Name
string
default:"Bearer"
Cookie name for the JWT token
HttpOnly
bool
default:true
Prevents JavaScript access to the cookie, mitigating XSS attacks
Path
string
default:"/"
Cookie available across the entire domain
The Secure flag is not set, which means cookies can be sent over HTTP. In production, enable this flag to require HTTPS.
A separate cookie stores the user ID for client-side access (auth/auth.go:84-91):
auth/auth.go
func setUserCookie(user *users.User, expiration time.Time, c echo.Context) {
    cookie := new(http.Cookie)
    cookie.Name = "user"
    cookie.Value = user.ID
    cookie.Expires = expiration
    cookie.Path = "/"
    c.SetCookie(cookie)
}
This cookie is NOT HttpOnly, making it accessible to JavaScript for UI personalization.

User Registration

Registration Endpoint

POST /register - Creates a new user account Handler in auth/routes.go:29-39:
auth/routes.go
func HandleUserRegistration(ctx echo.Context) error {
    email := ctx.FormValue("email")
    password := ctx.FormValue("password")

    _, err := createNewUser(email, []byte(password))
    if err != nil {
        return ctx.JSON(401, err)
    }

    return ctx.JSON(201, "Success")
}
email
string
required
User’s email address
password
string
required
Plain text password (will be hashed)

Password Hashing

Passwords are hashed with bcrypt using cost factor 14 (auth/controller.go:30-48):
auth/controller.go
func createNewUser(email string, password []byte) (users.User, error) {
    uid := uuid.New().String()
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    if err != nil {
        return users.User{}, err
    }
    user := users.User{
        Email:     email,
        Status:    "pending",
        Password:  hashedPassword,
        ID:        uid,
        CreatedAt: time.Now(),
    }
    err = user.CreateNewUser()
    if err != nil {
        return users.User{}, err
    }
    return user, nil
}
Security Features:
  • Bcrypt cost 14 - High security with ~180ms hashing time
  • UUID v4 - Cryptographically random user IDs
  • Pending status - New users require activation
New users are created with status: "pending". They cannot log in until their status is changed to "active".

User Login

Login Endpoint

POST /login - Authenticate user and receive token Handler in auth/routes.go:13-27:
auth/routes.go
type loginResult struct {
    token  string
    status bool
    error  error
}

func HandleUserLogin(ctx echo.Context) error {
    email := ctx.FormValue("email")
    password := ctx.FormValue("password")

    token, status, err := processUserLogin(email, password)
    if err != nil {
        return ctx.JSON(401, err)
    }
    rslt := loginResult{
        token:  token,
        status: status,
        error:  nil,
    }
    return ctx.JSON(200, rslt)
}
email
string
required
User’s email address
password
string
required
Plain text password

Login Validation

The processUserLogin function validates credentials (auth/controller.go:13-28):
auth/controller.go
func processUserLogin(email, password string) (string, bool, error) {
    user, err := users.GetUserByEmail(email)
    if err != nil {
        return "", false, err
    }
    if user.Status != "active" {
        return "", false, fmt.Errorf("user is not active")
    }
    errr := bcrypt.CompareHashAndPassword(user.Password, []byte(password))
    if errr != nil {
        return "", false, fmt.Errorf("the password provided is not valid")
    }

    return user.ID, true, nil
}
Validation Steps:
  1. Fetch user by email from database
  2. Check if user status is "active"
  3. Compare password hash using bcrypt
  4. Return user ID on success
The function currently returns the user ID instead of the JWT token. The token generation needs to be integrated here or in the route handler.

Session Store

The session store uses Gorilla Sessions with cookie-based storage (auth/configs.go:9):
auth/configs.go
var SessionStore = sessions.NewCookieStore([]byte(configs.GetSessionKey()))
The session store is initialized but not actively used in the current authentication flow. JWT tokens in cookies serve as the primary session mechanism.

Security Best Practices

Current Implementation

HTTP-only cookies - Prevents XSS token theft
Bcrypt hashing - Strong password protection
UUID user IDs - Non-sequential identifiers
Status checks - Inactive users cannot log in
Consider implementing these security enhancements:
  1. Secure flag on cookies - Require HTTPS in production
  2. SameSite attribute - Prevent CSRF attacks
  3. Token refresh mechanism - Short-lived access tokens with refresh tokens
  4. Rate limiting - Prevent brute force attacks
  5. Email verification - Validate email addresses before activation
  6. Password requirements - Enforce minimum complexity

Environment Configuration

Authentication requires the SESSION_KEY environment variable:
auth/auth.go
func GetJWTSecret() string {
    return configs.GetSessionKey()
}
SESSION_KEY
string
required
Secret key for JWT signing. Must be a strong, random string (minimum 32 characters recommended).
Never commit the SESSION_KEY to version control. Use different keys for development, staging, and production.

Testing Authentication

Register a New User

curl -X POST http://localhost:8080/register \
  -d "[email protected]" \
  -d "password=securepassword123"
Response:
"Success"

Login

curl -X POST http://localhost:8080/login \
  -d "[email protected]" \
  -d "password=securepassword123" \
  -c cookies.txt
Response:
{
  "token": "user-id-string",
  "status": true,
  "error": null
}
The JWT token is stored in the Bearer cookie and can be used for subsequent authenticated requests.

Next Steps

User Model

Learn about the User struct and database operations

Build docs developers (and LLMs) love