Skip to main content

Overview

CompanyFlow implements a JWT (JSON Web Token) authentication system that provides stateless, secure authentication for employees across the API. Each token contains user identity, role, and tenant information.

Authentication Flow

1. Login Request

Employees authenticate using their email and password:
POST /auth/login
Content-Type: application/json

{
  "email": "[email protected]",
  "password": "securepassword123"
}

2. Credential Validation

The authentication service performs the following checks:
  1. User Lookup - Find employee by email
  2. Status Check - Verify employee status is active
  3. Password Verification - Compare bcrypt hash
  4. Token Generation - Create JWT with claims
/home/daytona/workspace/source/services/auth_service.go:42-62
employee, roleName, err := as.employeeRepo.GetEmployeeWithRoleByEmail(ctx, req.Email)
if err != nil {
    return nil, ErrInvalidCredentials
}

if employee.Status != "active" {
    return nil, ErrInvalidCredentials
}

if !utils.VerifyPassword(employee.PasswordHash, req.Password) {
    return nil, ErrInvalidCredentials
}

token, err := utils.GenerateToken(
    employee.ID.String(),
    roleName,
    employee.CompanyID.String(),
    employee.Email,
    employee.FirstName,
    employee.LastName,
    24,  // 24-hour expiry
)

3. Login Response

Successful authentication returns a JWT token along with user and company details:
{
  "success": true,
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "role": "HR Manager",
    "employee": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "first_name": "John",
      "last_name": "Doe",
      "email": "[email protected]"
    },
    "company": {
      "id": "660e8400-e29b-41d4-a716-446655440000",
      "name": "Acme Corporation",
      "slug": "acme"
    }
  }
}

JWT Token Structure

Token Claims

Each JWT contains the following claims:
/home/daytona/workspace/source/utils/utils.go:80-88
type AuthClaims struct {
    EmployeeID string `json:"employee_id"`  // Unique employee identifier
    Role       string `json:"role"`         // Role name (e.g., "Super Admin")
    CompanyID  string `json:"company_id"`   // Tenant isolation
    Email      string `json:"email"`        // Employee email
    FirstName  string `json:"first_name"`   // Display name
    LastName   string `json:"last_name"`    // Display name
    jwt.RegisteredClaims                    // Standard JWT claims (exp, iat, sub)
}
The CompanyID claim is critical for multi-tenant isolation. See Multi-Tenancy for details.

Token Generation

Tokens are signed using HMAC-SHA256 with a secret key:
/home/daytona/workspace/source/utils/utils.go:100-113
claims := AuthClaims{
    EmployeeID: employeeID,
    Role:       role,
    CompanyID:  companyID,
    Email:      email,
    FirstName:  firstName,
    LastName:   lastName,
    RegisteredClaims: jwt.RegisteredClaims{
        Subject:   employeeID,
        IssuedAt:  jwt.NewNumericDate(now),
        ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
    },
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
Token Properties:
  • Algorithm: HS256 (HMAC with SHA-256)
  • Expiry: 24 hours from issuance
  • Secret: Environment variable JWT_SECRET
The JWT_SECRET environment variable must be set and should be a strong, random string (minimum 32 characters).

Using Tokens

Making Authenticated Requests

Include the JWT token in the Authorization header using the Bearer scheme:
GET /employees/550e8400-e29b-41d4-a716-446655440000
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Token Validation

API handlers extract and validate tokens from the Authorization header:
/home/daytona/workspace/source/handlers/employee_handler.go:311-334
func authorizeEmployeeToken(r *http.Request, allowedRoles ...string) (*utils.AuthClaims, error) {
    authHeader := r.Header.Get("Authorization")
    if authHeader == "" {
        return nil, errors.New("missing authorization header")
    }

    parts := strings.SplitN(authHeader, " ", 2)
    if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
        return nil, errors.New("invalid authorization header")
    }

    claims, err := utils.ValidateToken(parts[1])
    if err != nil {
        return nil, errors.New("invalid token")
    }

    // Role-based authorization check
    for _, allowed := range allowedRoles {
        if claims.Role == allowed {
            return claims, nil
        }
    }

    return nil, errors.New("insufficient role")
}

Validation Steps

  1. Parse Token - Extract and decode JWT
  2. Verify Signature - Validate HMAC signature with secret
  3. Check Expiry - Ensure token hasn’t expired
  4. Extract Claims - Return AuthClaims for authorization
/home/daytona/workspace/source/utils/utils.go:134-157
parsed, err := jwt.ParseWithClaims(
    tokenString,
    &AuthClaims{},
    func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, errors.New("unexpected signing method")
        }
        return []byte(secret), nil
    },
)

claims, ok := parsed.Claims.(*AuthClaims)
if !ok || !parsed.Valid {
    return nil, errors.New("invalid token")
}

if claims.ExpiresAt != nil && claims.ExpiresAt.Before(time.Now()) {
    return nil, errors.New("token has expired")
}

Token Refresh

The API supports refreshing tokens before they expire:
/home/daytona/workspace/source/utils/utils.go:160-175
func RefreshToken(oldTokenString string) (string, error) {
    claims, err := ValidateToken(oldTokenString)
    if err != nil {
        return "", fmt.Errorf("cannot refresh invalid token: %w", err)
    }

    return GenerateToken(
        claims.EmployeeID,
        claims.Role,
        claims.CompanyID,
        claims.Email,
        claims.FirstName,
        claims.LastName,
        24,
    )
}
Refresh tokens proactively before expiry to avoid forcing users to re-authenticate.

Password Security

Passwords are hashed using bcrypt before storage:
/home/daytona/workspace/source/utils/utils.go:68-78
// Hash password during employee creation
func HashPassword(password string) (string, error) {
    hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(hashed), nil
}

// Verify password during login
func VerifyPassword(hashPassword, plainPassword string) bool {
    return bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(plainPassword)) == nil
}
Security Features:
  • Bcrypt - Adaptive hashing function resistant to brute-force
  • Default Cost - Cost factor 10 (2^10 = 1,024 iterations)
  • Salted Hashes - Unique salt per password prevents rainbow table attacks

Error Handling

Authentication can fail for several reasons:
ErrorStatus CodeDescription
missing authorization header401No Authorization header provided
invalid authorization header401Malformed Authorization header
invalid token401Token signature invalid or expired
insufficient role401User lacks required role for endpoint
invalid credentials401Email/password mismatch or inactive user
company_id mismatch400Token company_id doesn’t match requested resource
{
  "success": false,
  "error": "missing authorization header"
}

Best Practices

Secure Token Storage

Store tokens securely on the client side (e.g., httpOnly cookies or secure storage). Never expose tokens in URLs.

Use HTTPS

Always transmit tokens over HTTPS to prevent interception. Never send tokens over unencrypted connections.

Short Expiry Times

The 24-hour expiry balances security and user experience. Consider shorter expiry for sensitive operations.

Rotate Secrets

Periodically rotate the JWT_SECRET and invalidate old tokens to maintain security.

Authorization

Learn about role-based access control

Multi-Tenancy

Understand how tokens enforce tenant isolation

Build docs developers (and LLMs) love