# User Management and Authentication System ## Overview The dance-lessons-coach 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 ```mermaid 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:** ```sql 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:** ```bash # 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:** ```json { "username": "john_doe", "password": "securePassword123!" } ``` **Response (201 Created):** ```json { "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:** ```json { "username": "john_doe", "password": "securePassword123!" } ``` **Response (200 OK):** ```json { "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):** ```json { "username": "john_doe", "new_password": "newSecurePassword456!" } ``` **Response (200 OK):** ```json { "message": "Password reset successfully" } ``` ### User Profile Endpoints #### GET `/api/v1/users/me` **Headers:** ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` **Response (200 OK):** ```json { "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:** ```json { "description": "Passionate dancer learning multiple styles", "current_goal": "Prepare for salsa competition" } ``` **Response (200 OK):** ```json { "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:** ```json { "current_password": "securePassword123!", "new_password": "evenMoreSecurePassword456!" } ``` **Response (200 OK):** ```json { "message": "Password updated successfully" } ``` ### Admin Endpoints #### GET `/api/v1/admin/users` **Headers:** ``` Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... X-Admin-Key: master-admin-key ``` **Response (200 OK):** ```json { "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):** ```json { "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):** ```json { "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:** ```go 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 ```mermaid 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 ```json { "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 ```yaml 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 ```bash # 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 ```gherkin 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 ```bash # 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 ```yaml # 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 ```bash # 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:** ```bash 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 - [GORM Documentation](https://gorm.io/) - [JWT RFC 7519](https://tools.ietf.org/html/rfc7519) - [bcrypt Documentation](https://pkg.go.dev/golang.org/x/crypto/bcrypt) - [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) - [Chi Router Middleware](https://github.com/go-chi/chi) ## Appendix ### Username Validation Regex ```go var usernameRegex = regexp.MustCompile(`^[a-zA-Z0-9]{3,50}$`) ``` ### Password Hashing Example ```go 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 ```go 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)) } ```