Files
dance-lessons-coach/documentation/technical/user-management-system.md

15 KiB

User Management and Authentication System

Overview

The DanceLessonsCoach user management and authentication system provides secure user authentication, personalized experiences, and administrative capabilities. This document describes the system architecture, API endpoints, and integration points.

Architecture

graph TD
    A[Client] -->|HTTP Request| B[Authentication Middleware]
    B -->|Valid Token| C[Authorized Endpoints]
    B -->|Invalid Token| D[401 Unauthorized]
    C --> E[Greet Service]
    C --> F[User Profile Service]
    C --> G[Admin Service]
    E -->|Personalized Response| A
    F -->|User Data| H[PostgreSQL]
    G -->|Admin Operations| H

Core Components

1. User Model

Database Schema:

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMP WITH TIME ZONE,
    updated_at TIMESTAMP WITH TIME ZONE,
    deleted_at TIMESTAMP WITH TIME ZONE,
    username VARCHAR(50) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    description TEXT,
    current_goal TEXT,
    is_admin BOOLEAN DEFAULT FALSE,
    allow_password_reset BOOLEAN DEFAULT FALSE,
    last_login TIMESTAMP WITH TIME ZONE
);

Fields:

  • username: Unique identifier (3-50 alphanumeric characters)
  • password_hash: bcrypt-hashed password
  • description: User's personal description
  • current_goal: User's current dance learning goal
  • is_admin: Administrative privileges flag
  • allow_password_reset: Flag for password reset eligibility

2. Authentication Service

Features:

  • JWT token generation and validation
  • bcrypt password hashing (work factor 12)
  • 30-minute token expiration
  • Secure cookie-based token storage
  • Admin master password authentication

Environment Variables:

# JWT Configuration
DLC_JWT_SECRET="your-secure-random-secret-key"
DLC_JWT_EXPIRATION="30m"

# Admin Configuration
DLC_ADMIN_USERNAME="admin"
DLC_ADMIN_MASTER_PASSWORD="secure-master-password"

# Database Configuration
DLC_DB_HOST="localhost"
DLC_DB_PORT="5432"
DLC_DB_USER="dancecoach"
DLC_DB_PASSWORD="secure-password"
DLC_DB_NAME="dance_lessons_coach"
DLC_DB_SSL_MODE="disable"

API Endpoints

Authentication Endpoints

POST /api/v1/auth/register

Request:

{
  "username": "john_doe",
  "password": "securePassword123!"
}

Response (201 Created):

{
  "id": 1,
  "username": "john_doe",
  "created_at": "2024-04-06T10:00:00Z",
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

Validation Rules:

  • username: Required, 3-50 chars, alphanumeric only
  • password: Required, min 8 chars

POST /api/v1/auth/login

Request:

{
  "username": "john_doe",
  "password": "securePassword123!"
}

Response (200 OK):

{
  "id": 1,
  "username": "john_doe",
  "is_admin": false,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expires_at": "2024-04-06T10:30:00Z"
}

POST /api/v1/auth/reset-password

Request (for flagged users only):

{
  "username": "john_doe",
  "new_password": "newSecurePassword456!"
}

Response (200 OK):

{
  "message": "Password reset successfully"
}

User Profile Endpoints

GET /api/v1/users/me

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response (200 OK):

{
  "id": 1,
  "username": "john_doe",
  "description": "Dance enthusiast learning salsa",
  "current_goal": "Master basic salsa steps",
  "created_at": "2024-04-06T10:00:00Z",
  "last_login": "2024-04-06T10:15:00Z"
}

PUT /api/v1/users/me

Request:

{
  "description": "Passionate dancer learning multiple styles",
  "current_goal": "Prepare for salsa competition"
}

Response (200 OK):

{
  "id": 1,
  "username": "john_doe",
  "description": "Passionate dancer learning multiple styles",
  "current_goal": "Prepare for salsa competition",
  "updated_at": "2024-04-06T10:30:00Z"
}

PUT /api/v1/users/me/password

Request:

{
  "current_password": "securePassword123!",
  "new_password": "evenMoreSecurePassword456!"
}

Response (200 OK):

{
  "message": "Password updated successfully"
}

Admin Endpoints

GET /api/v1/admin/users

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Admin-Key: master-admin-key

Response (200 OK):

{
  "users": [
    {
      "id": 1,
      "username": "john_doe",
      "is_admin": false,
      "allow_password_reset": false,
      "created_at": "2024-04-06T10:00:00Z"
    },
    {
      "id": 2,
      "username": "jane_smith",
      "is_admin": true,
      "allow_password_reset": true,
      "created_at": "2024-04-05T15:30:00Z"
    }
  ],
  "total": 2
}

POST /api/v1/admin/users/{username}/allow-reset

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Admin-Key: master-admin-key

Response (200 OK):

{
  "message": "Password reset allowed for user john_doe"
}

DELETE /api/v1/admin/users/{username}

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
X-Admin-Key: master-admin-key

Response (200 OK):

{
  "message": "User john_doe deleted successfully"
}

Integration with Greet Service

Current Behavior

GET /api/v1/greet/John
Response: {"message": "Hello John!"}

New Behavior with Authentication

GET /api/v1/greet
Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Response: {"message": "Hello john_doe!"}

Implementation:

func (s *Service) Greet(ctx context.Context, name string) string {
    // Extract authenticated username from context
    username := auth.GetUsernameFromContext(ctx)
    
    if username != "" {
        return "Hello " + username + "!"
    }
    
    // Fallback to original behavior
    if name == "" {
        return "Hello world!"
    }
    return "Hello " + name + "!"
}

Password Reset Workflow

sequenceDiagram
    participant User
    participant Admin
    participant System
    participant Database

    User->>System: Forgot password (no auth)
    System-->>User: 403 Forbidden
    
    User->>Admin: Request password reset
    Admin->>System: POST /api/v1/admin/users/john_doe/allow-reset
    System->>Database: Set allow_password_reset = true
    Database-->>System: Success
    System-->>Admin: 200 OK
    
    User->>System: POST /api/v1/auth/reset-password
    System->>Database: Check allow_password_reset flag
    Database-->>System: Flag is true
    System->>Database: Update password_hash
    Database-->>System: Success
    System->>Database: Set allow_password_reset = false
    System-->>User: 200 OK

Security Considerations

Password Storage

  • Algorithm: bcrypt with work factor 12
  • Implementation: golang.org/x/crypto/bcrypt
  • Salt: Automatic per-password salt

JWT Security

  • Algorithm: HS256 with secure random key
  • Expiration: 30 minutes (configurable)
  • Storage: HTTP-only, Secure cookies
  • Claims: User ID, username, admin flag, expiration

Rate Limiting

  • Authentication Endpoints: 5 requests per minute per IP
  • Password Reset: 3 attempts per hour per user
  • Implementation: Chi middleware

Input Validation

  • Username: 3-50 alphanumeric characters
  • Password: Minimum 8 characters
  • Description/Goal: Maximum 500 characters

Error Handling

Standard Error Format

{
  "error": "error_code",
  "message": "Human-readable message",
  "details": [
    {
      "field": "username",
      "message": "Username must be at least 3 characters"
    }
  ]
}

Common Error Codes

  • auth_invalid_credentials: Invalid username/password
  • auth_token_expired: JWT token expired
  • auth_token_invalid: Invalid JWT token
  • auth_unauthorized: Missing or invalid authorization
  • validation_failed: Input validation failed
  • user_not_found: User does not exist
  • user_exists: Username already taken
  • password_reset_not_allowed: User not flagged for reset
  • admin_required: Admin privileges required

Database Setup

Docker Compose

version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: dance-lessons-coach-db
    environment:
      POSTGRES_USER: dancecoach
      POSTGRES_PASSWORD: secure-password
      POSTGRES_DB: dance_lessons_coach
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dancecoach"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  postgres_data:

Database Migration

# Initialize database
go run cmd/server/main.go migrate

# Run migrations
goose -dir migrations/postgres up

Testing Strategy

Unit Tests

  • Password hashing/verification
  • JWT token generation/validation
  • User model validation
  • Repository methods

Integration Tests

  • Authentication flow
  • Authorization middleware
  • Database operations
  • Password reset workflow

BDD Tests

Feature: User Authentication
  Scenario: Successful user registration
    Given I am not authenticated
    When I register with valid credentials
    Then I should receive a JWT token
    And my user account should be created

  Scenario: Successful login
    Given I have a registered account
    When I login with correct credentials
    Then I should receive a JWT token
    And the token should expire in 30 minutes

  Scenario: Personalized greeting for authenticated user
    Given I am authenticated as "john_doe"
    When I request the default greeting
    Then the response should be "{"message":"Hello john_doe!"}"

CI/CD Integration

New Dependencies

# Add to go.mod
require (
    github.com/golang-jwt/jwt/v5 v5.0.0
    golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
    gorm.io/gorm v1.25.0
    gorm.io/driver/postgres v1.5.0
)

Pipeline Changes

  1. Add PostgreSQL service to CI environment
  2. Run database migrations before tests
  3. Include authentication tests in test suite
  4. Add security scanning for dependencies

Deployment Considerations

Configuration

# config.yaml
database:
  host: localhost
  port: 5432
  user: dancecoach
  password: secure-password
  name: dance_lessons_coach
  ssl_mode: disable

auth:
  jwt_secret: your-secure-random-secret-key
  jwt_expiration: 30m
  admin_username: admin
  admin_master_password: secure-master-password

Environment Variables

# Database
export DLC_DB_HOST="localhost"
export DLC_DB_PORT="5432"
export DLC_DB_USER="dancecoach"
export DLC_DB_PASSWORD="secure-password"
export DLC_DB_NAME="dance_lessons_coach"
export DLC_DB_SSL_MODE="disable"

# Authentication
export DLC_JWT_SECRET="your-secure-random-secret-key"
export DLC_JWT_EXPIRATION="30m"
export DLC_ADMIN_USERNAME="admin"
export DLC_ADMIN_MASTER_PASSWORD="secure-master-password"

Monitoring and Observability

Metrics

  • auth_login_success_total: Successful logins
  • auth_login_failure_total: Failed login attempts
  • auth_register_total: User registrations
  • auth_token_issued_total: JWT tokens issued
  • user_active_total: Active users

Logging

  • Authentication attempts (success/failure)
  • Password changes
  • Admin operations
  • Rate limiting events

Alerts

  • Multiple failed login attempts
  • Admin account modifications
  • Unusual password reset activity

Future Enhancements

Short-term (Next 3 Months)

  1. Refresh Tokens: Long-lived refresh tokens with rotation
  2. Rate Limiting: IP-based rate limiting for auth endpoints
  3. Password Strength: Enforce stronger password requirements
  4. Account Lockout: Temporary lockout after failed attempts

Medium-term (Next 6 Months)

  1. Multi-factor Authentication: TOTP or backup codes
  2. User Activity Logging: Comprehensive audit trails
  3. Session Management: View and revoke active sessions
  4. Password Expiration: Enforce periodic password changes

Long-term (Future)

  1. OAuth Integration: Google, Facebook, Apple sign-in
  2. Social Features: User profiles, followers, messaging
  3. Role-Based Access: Fine-grained permissions
  4. User Preferences: Theme, language, notifications

Troubleshooting

Common Issues

Issue: Authentication fails with valid credentials

  • Check: Password hash comparison logic
  • Check: JWT secret key configuration
  • Check: Database connection

Issue: Password reset not working

  • Check: User has allow_password_reset flag set
  • Check: Admin has set the flag correctly
  • Check: Rate limiting not blocking requests

Issue: Personalized greeting not showing username

  • Check: Authentication middleware is properly configured
  • Check: JWT token is valid and not expired
  • Check: Context contains username after authentication

Migration Guide

From No Authentication to User System

  1. Add Dependencies:

    go get github.com/golang-jwt/jwt/v5
    go get golang.org/x/crypto
    go get gorm.io/gorm
    go get gorm.io/driver/postgres
    
  2. Update Configuration:

    • Add database configuration
    • Add JWT configuration
    • Add admin configuration
  3. Update Server:

    • Add authentication middleware
    • Add user repository initialization
    • Add auth routes
  4. Update Greet Service:

    • Modify to check for authenticated username
    • Maintain backward compatibility
  5. Update Tests:

    • Add authentication scenarios
    • Update existing tests for new behavior
    • Add BDD tests for user management
  6. Update CI/CD:

    • Add PostgreSQL to test environment
    • Update test scripts
    • Add security scanning

References

Appendix

Username Validation Regex

var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9]{3,50}$`)

Password Hashing Example

import "golang.org/x/crypto/bcrypt"

func HashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 12)
    return string(bytes), err
}

func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

JWT Token Example

import (
    "github.com/golang-jwt/jwt/v5"
    "time"
)

type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    IsAdmin  bool   `json:"is_admin"`
    jwt.RegisteredClaims
}

func GenerateJWT(user *User, secret string, expiration time.Duration) (string, error) {
    claims := &Claims{
        UserID:   user.ID,
        Username: user.Username,
        IsAdmin:  user.IsAdmin,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(expiration)),
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(secret))
}