# JWT Authentication System Architecture Design **Version**: 1.0 **Author**: ColaFlow Architecture Team **Date**: 2025-11-03 **Status**: Design Phase (M1 Sprint 2 - Task 1.1) --- ## 1. Executive Summary This document defines the complete JWT authentication system architecture for ColaFlow, addressing the Critical security risk of unprotected API endpoints. The design follows Clean Architecture, Domain-Driven Design (DDD), and CQRS principles, ensuring separation of concerns, maintainability, and security. **Key Objectives**: - Protect all 23+ API endpoints with JWT authentication - Implement secure user registration and login - Support Access Token + Refresh Token pattern - Integrate with existing Clean Architecture layers - Enable frontend (Next.js 16) seamless authentication - Maintain OWASP security standards --- ## 2. Architecture Overview ### 2.1 System Context ``` ┌─────────────────────────────────────────────────────────────┐ │ Frontend Layer │ │ Next.js 16 App Router + React 19 + Zustand │ │ - Login/Register Pages │ │ - Protected Routes (Middleware) │ │ - Token Management (httpOnly cookies) │ │ - API Interceptor (TanStack Query) │ └──────────────────────┬──────────────────────────────────────┘ │ HTTPS + JWT Bearer Token │ ┌──────────────────────┴──────────────────────────────────────┐ │ API Layer │ │ ColaFlow.API (ASP.NET Core 9) │ │ - Authentication Middleware [JwtBearer] │ │ - Authorization Policies [Authorize] │ │ - AuthController (Register/Login/Refresh/Logout) │ │ - Protected Controllers (Projects, Epics, etc.) │ └──────────────────────┬──────────────────────────────────────┘ │ ┌──────────────────────┴──────────────────────────────────────┐ │ Application Layer (CQRS) │ │ ColaFlow.Application + Identity.Application Module │ │ - Commands: RegisterUserCommand, LoginCommand, etc. │ │ - Queries: GetCurrentUserQuery, ValidateTokenQuery │ │ - Command/Query Handlers (MediatR) │ │ - DTOs: UserDto, LoginResponseDto, etc. │ └──────────────────────┬──────────────────────────────────────┘ │ ┌──────────────────────┴──────────────────────────────────────┐ │ Domain Layer (DDD) │ │ ColaFlow.Domain + Identity.Domain Module │ │ - User (AggregateRoot) │ │ - RefreshToken (Entity) │ │ - Value Objects: Email, PasswordHash, Role │ │ - Domain Events: UserRegisteredEvent, UserLoggedInEvent │ │ - Interfaces: IUserRepository, IJwtService │ └──────────────────────┬──────────────────────────────────────┘ │ ┌──────────────────────┴──────────────────────────────────────┐ │ Infrastructure Layer │ │ ColaFlow.Infrastructure + Identity.Infrastructure │ │ - UserRepository (EF Core) │ │ - JwtService (Token generation/validation) │ │ - PasswordHasher (BCrypt/Argon2) │ │ - IdentityDbContext (EF Core) │ │ - Migrations │ └──────────────────────┬──────────────────────────────────────┘ │ ┌──────────────────────┴──────────────────────────────────────┐ │ Data Layer │ │ PostgreSQL 16 │ │ - users (id, email, password_hash, role, created_at, ...) │ │ - refresh_tokens (id, user_id, token, expires_at, ...) │ └─────────────────────────────────────────────────────────────┘ ``` ### 2.2 Module Structure Following ColaFlow's modular architecture pattern, authentication will be implemented as an **Identity Module**: ``` ColaFlow.Modules.Identity/ ├── ColaFlow.Modules.Identity.Domain/ │ ├── Aggregates/ │ │ ├── UserAggregate/ │ │ │ ├── User.cs (AggregateRoot) │ │ │ └── RefreshToken.cs (Entity) │ ├── ValueObjects/ │ │ ├── UserId.cs │ │ ├── Email.cs │ │ ├── PasswordHash.cs │ │ └── Role.cs │ ├── Events/ │ │ ├── UserRegisteredEvent.cs │ │ ├── UserLoggedInEvent.cs │ │ └── UserPasswordChangedEvent.cs │ ├── Repositories/ │ │ ├── IUserRepository.cs │ │ └── IUnitOfWork.cs │ └── Exceptions/ │ ├── InvalidCredentialsException.cs │ ├── UserAlreadyExistsException.cs │ └── RefreshTokenExpiredException.cs │ ├── ColaFlow.Modules.Identity.Application/ │ ├── Commands/ │ │ ├── RegisterUser/ │ │ │ ├── RegisterUserCommand.cs │ │ │ ├── RegisterUserCommandHandler.cs │ │ │ └── RegisterUserCommandValidator.cs │ │ ├── Login/ │ │ │ ├── LoginCommand.cs │ │ │ ├── LoginCommandHandler.cs │ │ │ └── LoginCommandValidator.cs │ │ ├── RefreshToken/ │ │ │ ├── RefreshTokenCommand.cs │ │ │ └── RefreshTokenCommandHandler.cs │ │ ├── Logout/ │ │ │ ├── LogoutCommand.cs │ │ │ └── LogoutCommandHandler.cs │ │ └── ChangePassword/ │ │ ├── ChangePasswordCommand.cs │ │ ├── ChangePasswordCommandHandler.cs │ │ └── ChangePasswordCommandValidator.cs │ ├── Queries/ │ │ ├── GetCurrentUser/ │ │ │ ├── GetCurrentUserQuery.cs │ │ │ └── GetCurrentUserQueryHandler.cs │ │ └── ValidateToken/ │ │ ├── ValidateTokenQuery.cs │ │ └── ValidateTokenQueryHandler.cs │ ├── DTOs/ │ │ ├── UserDto.cs │ │ ├── LoginResponseDto.cs │ │ └── RefreshTokenResponseDto.cs │ └── Services/ │ └── IJwtService.cs (Interface only) │ ├── ColaFlow.Modules.Identity.Infrastructure/ │ ├── Persistence/ │ │ ├── IdentityDbContext.cs │ │ ├── Configurations/ │ │ │ ├── UserConfiguration.cs │ │ │ └── RefreshTokenConfiguration.cs │ │ ├── Repositories/ │ │ │ └── UserRepository.cs │ │ ├── Migrations/ │ │ └── UnitOfWork.cs │ └── Services/ │ ├── JwtService.cs (Implementation) │ └── PasswordHasher.cs │ └── IdentityModule.cs (Module registration) ``` --- ## 3. Core Components Design ### 3.1 Domain Layer - User Aggregate #### User Entity (Aggregate Root) ```csharp // ColaFlow.Modules.Identity.Domain/Aggregates/UserAggregate/User.cs using ColaFlow.Shared.Kernel.Common; using ColaFlow.Modules.Identity.Domain.ValueObjects; using ColaFlow.Modules.Identity.Domain.Events; using ColaFlow.Modules.Identity.Domain.Exceptions; namespace ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; /// /// User Aggregate Root /// Enforces all business rules related to user authentication and identity /// public class User : AggregateRoot { public new UserId Id { get; private set; } public Email Email { get; private set; } public PasswordHash PasswordHash { get; private set; } public string FirstName { get; private set; } public string LastName { get; private set; } public Role Role { get; private set; } public bool IsActive { get; private set; } public DateTime CreatedAt { get; private set; } public DateTime? UpdatedAt { get; private set; } public DateTime? LastLoginAt { get; private set; } private readonly List _refreshTokens = new(); public IReadOnlyCollection RefreshTokens => _refreshTokens.AsReadOnly(); // EF Core constructor private User() { Id = null!; Email = null!; PasswordHash = null!; FirstName = null!; LastName = null!; Role = null!; } /// /// Factory method to create a new user /// public static User Create( Email email, string plainPassword, string firstName, string lastName, Role role) { // Validate password strength ValidatePasswordStrength(plainPassword); var user = new User { Id = UserId.Create(), Email = email, PasswordHash = PasswordHash.Create(plainPassword), FirstName = firstName, LastName = lastName, Role = role, IsActive = true, CreatedAt = DateTime.UtcNow }; // Raise domain event user.AddDomainEvent(new UserRegisteredEvent(user.Id, user.Email.Value)); return user; } /// /// Verify password and record login /// public void Login(string plainPassword) { if (!IsActive) throw new InvalidCredentialsException("User account is deactivated"); if (!PasswordHash.Verify(plainPassword)) throw new InvalidCredentialsException("Invalid email or password"); LastLoginAt = DateTime.UtcNow; UpdatedAt = DateTime.UtcNow; AddDomainEvent(new UserLoggedInEvent(Id, Email.Value, DateTime.UtcNow)); } /// /// Create a new refresh token for this user /// public RefreshToken CreateRefreshToken(int expiryDays = 7) { if (!IsActive) throw new InvalidCredentialsException("User account is deactivated"); // Revoke old tokens (keep only latest N) var oldTokens = _refreshTokens .Where(t => !t.IsRevoked) .OrderByDescending(t => t.CreatedAt) .Skip(4) // Keep max 5 active tokens per user .ToList(); foreach (var token in oldTokens) { token.Revoke(); } var refreshToken = RefreshToken.Create(Id, expiryDays); _refreshTokens.Add(refreshToken); return refreshToken; } /// /// Change user password /// public void ChangePassword(string currentPassword, string newPassword) { if (!PasswordHash.Verify(currentPassword)) throw new InvalidCredentialsException("Current password is incorrect"); ValidatePasswordStrength(newPassword); PasswordHash = PasswordHash.Create(newPassword); UpdatedAt = DateTime.UtcNow; // Revoke all refresh tokens (force re-login) foreach (var token in _refreshTokens.Where(t => !t.IsRevoked)) { token.Revoke(); } AddDomainEvent(new UserPasswordChangedEvent(Id)); } /// /// Deactivate user account /// public void Deactivate() { if (!IsActive) throw new DomainException("User is already deactivated"); IsActive = false; UpdatedAt = DateTime.UtcNow; // Revoke all refresh tokens foreach (var token in _refreshTokens.Where(t => !t.IsRevoked)) { token.Revoke(); } } /// /// Validate password strength (OWASP guidelines) /// private static void ValidatePasswordStrength(string password) { if (string.IsNullOrWhiteSpace(password)) throw new DomainException("Password cannot be empty"); if (password.Length < 8) throw new DomainException("Password must be at least 8 characters long"); if (password.Length > 128) throw new DomainException("Password cannot exceed 128 characters"); // Require at least one uppercase, one lowercase, one digit, one special char if (!password.Any(char.IsUpper)) throw new DomainException("Password must contain at least one uppercase letter"); if (!password.Any(char.IsLower)) throw new DomainException("Password must contain at least one lowercase letter"); if (!password.Any(char.IsDigit)) throw new DomainException("Password must contain at least one digit"); if (!password.Any(ch => !char.IsLetterOrDigit(ch))) throw new DomainException("Password must contain at least one special character"); } } ``` #### RefreshToken Entity ```csharp // ColaFlow.Modules.Identity.Domain/Aggregates/UserAggregate/RefreshToken.cs using ColaFlow.Shared.Kernel.Common; using ColaFlow.Modules.Identity.Domain.ValueObjects; using ColaFlow.Modules.Identity.Domain.Exceptions; using System.Security.Cryptography; namespace ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; /// /// RefreshToken entity - part of User aggregate /// Implements Refresh Token Rotation for security /// public class RefreshToken : Entity { public new Guid Id { get; private set; } public UserId UserId { get; private set; } public string Token { get; private set; } public DateTime ExpiresAt { get; private set; } public DateTime CreatedAt { get; private set; } public bool IsRevoked { get; private set; } public DateTime? RevokedAt { get; private set; } // Navigation property public User User { get; private set; } = null!; // EF Core constructor private RefreshToken() { Token = null!; UserId = null!; } /// /// Create a new refresh token /// public static RefreshToken Create(UserId userId, int expiryDays = 7) { return new RefreshToken { Id = Guid.NewGuid(), UserId = userId, Token = GenerateSecureToken(), ExpiresAt = DateTime.UtcNow.AddDays(expiryDays), CreatedAt = DateTime.UtcNow, IsRevoked = false }; } /// /// Validate if token is still usable /// public void Validate() { if (IsRevoked) throw new RefreshTokenExpiredException("Token has been revoked"); if (DateTime.UtcNow > ExpiresAt) throw new RefreshTokenExpiredException("Token has expired"); } /// /// Revoke this token (e.g., on logout or password change) /// public void Revoke() { if (IsRevoked) return; IsRevoked = true; RevokedAt = DateTime.UtcNow; } /// /// Generate cryptographically secure random token /// private static string GenerateSecureToken() { var randomBytes = new byte[64]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(randomBytes); return Convert.ToBase64String(randomBytes); } } ``` #### Value Objects ```csharp // ColaFlow.Modules.Identity.Domain/ValueObjects/Email.cs using ColaFlow.Shared.Kernel.Common; using System.Text.RegularExpressions; namespace ColaFlow.Modules.Identity.Domain.ValueObjects; public class Email : ValueObject { public string Value { get; } private Email(string value) { Value = value; } public static Email Create(string email) { if (string.IsNullOrWhiteSpace(email)) throw new DomainException("Email cannot be empty"); email = email.Trim().ToLowerInvariant(); if (!IsValidEmail(email)) throw new DomainException("Invalid email format"); if (email.Length > 254) // RFC 5321 throw new DomainException("Email cannot exceed 254 characters"); return new Email(email); } private static bool IsValidEmail(string email) { // RFC 5322 simplified pattern var pattern = @"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$"; return Regex.IsMatch(email, pattern, RegexOptions.IgnoreCase); } protected override IEnumerable GetEqualityComponents() { yield return Value; } } ``` ```csharp // ColaFlow.Modules.Identity.Domain/ValueObjects/PasswordHash.cs using ColaFlow.Shared.Kernel.Common; namespace ColaFlow.Modules.Identity.Domain.ValueObjects; /// /// Password hash value object /// Uses BCrypt for hashing (will be implemented in Infrastructure layer) /// public class PasswordHash : ValueObject { public string Value { get; } private PasswordHash(string value) { Value = value; } /// /// Create password hash from plain text password /// public static PasswordHash Create(string plainPassword) { if (string.IsNullOrWhiteSpace(plainPassword)) throw new DomainException("Password cannot be empty"); // Hash will be generated in Infrastructure layer // This is just a placeholder - actual hashing happens in PasswordHasher service var hash = HashPassword(plainPassword); return new PasswordHash(hash); } /// /// Verify plain password against this hash /// public bool Verify(string plainPassword) { return VerifyPassword(plainPassword, Value); } // These will delegate to Infrastructure layer's PasswordHasher // Placeholder implementations here private static string HashPassword(string plainPassword) { // This will be replaced with actual BCrypt hashing in Infrastructure return BCrypt.Net.BCrypt.HashPassword(plainPassword, workFactor: 12); } private static bool VerifyPassword(string plainPassword, string hash) { return BCrypt.Net.BCrypt.Verify(plainPassword, hash); } protected override IEnumerable GetEqualityComponents() { yield return Value; } } ``` ```csharp // ColaFlow.Modules.Identity.Domain/ValueObjects/Role.cs using ColaFlow.Shared.Kernel.Common; namespace ColaFlow.Modules.Identity.Domain.ValueObjects; /// /// User role enumeration /// public class Role : Enumeration { public static readonly Role Admin = new(1, "Admin"); public static readonly Role User = new(2, "User"); public static readonly Role Guest = new(3, "Guest"); private Role(int id, string name) : base(id, name) { } public static Role FromName(string name) { var role = GetAll().FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (role == null) throw new DomainException($"Invalid role: {name}"); return role; } public static Role FromId(int id) { var role = GetAll().FirstOrDefault(r => r.Id == id); if (role == null) throw new DomainException($"Invalid role id: {id}"); return role; } } ``` --- ### 3.2 Application Layer - Commands & Queries #### RegisterUserCommand ```csharp // ColaFlow.Modules.Identity.Application/Commands/RegisterUser/RegisterUserCommand.cs using MediatR; using ColaFlow.Modules.Identity.Application.DTOs; namespace ColaFlow.Modules.Identity.Application.Commands.RegisterUser; public record RegisterUserCommand( string Email, string Password, string FirstName, string LastName ) : IRequest; ``` ```csharp // ColaFlow.Modules.Identity.Application/Commands/RegisterUser/RegisterUserCommandValidator.cs using FluentValidation; namespace ColaFlow.Modules.Identity.Application.Commands.RegisterUser; public class RegisterUserCommandValidator : AbstractValidator { public RegisterUserCommandValidator() { RuleFor(x => x.Email) .NotEmpty() .EmailAddress() .MaximumLength(254); RuleFor(x => x.Password) .NotEmpty() .MinimumLength(8) .MaximumLength(128) .Matches(@"[A-Z]").WithMessage("Password must contain at least one uppercase letter") .Matches(@"[a-z]").WithMessage("Password must contain at least one lowercase letter") .Matches(@"\d").WithMessage("Password must contain at least one digit") .Matches(@"[^a-zA-Z0-9]").WithMessage("Password must contain at least one special character"); RuleFor(x => x.FirstName) .NotEmpty() .MaximumLength(100); RuleFor(x => x.LastName) .NotEmpty() .MaximumLength(100); } } ``` ```csharp // ColaFlow.Modules.Identity.Application/Commands/RegisterUser/RegisterUserCommandHandler.cs using MediatR; using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; using ColaFlow.Modules.Identity.Domain.Repositories; using ColaFlow.Modules.Identity.Domain.ValueObjects; using ColaFlow.Modules.Identity.Domain.Exceptions; using ColaFlow.Modules.Identity.Application.DTOs; namespace ColaFlow.Modules.Identity.Application.Commands.RegisterUser; public class RegisterUserCommandHandler : IRequestHandler { private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork; public RegisterUserCommandHandler(IUserRepository userRepository, IUnitOfWork unitOfWork) { _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); } public async Task Handle(RegisterUserCommand request, CancellationToken cancellationToken) { var email = Email.Create(request.Email); // Check if user already exists var existingUser = await _userRepository.GetByEmailAsync(email, cancellationToken); if (existingUser != null) { throw new UserAlreadyExistsException($"User with email {request.Email} already exists"); } // Create new user (default role: User) var user = User.Create( email, request.Password, request.FirstName, request.LastName, Role.User ); // Save to database await _userRepository.AddAsync(user, cancellationToken); await _unitOfWork.CommitAsync(cancellationToken); // Map to DTO return new UserDto { Id = user.Id.Value, Email = user.Email.Value, FirstName = user.FirstName, LastName = user.LastName, Role = user.Role.Name, IsActive = user.IsActive, CreatedAt = user.CreatedAt }; } } ``` #### LoginCommand ```csharp // ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommand.cs using MediatR; using ColaFlow.Modules.Identity.Application.DTOs; namespace ColaFlow.Modules.Identity.Application.Commands.Login; public record LoginCommand( string Email, string Password ) : IRequest; ``` ```csharp // ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandHandler.cs using MediatR; using ColaFlow.Modules.Identity.Domain.Repositories; using ColaFlow.Modules.Identity.Domain.ValueObjects; using ColaFlow.Modules.Identity.Domain.Exceptions; using ColaFlow.Modules.Identity.Application.DTOs; using ColaFlow.Modules.Identity.Application.Services; namespace ColaFlow.Modules.Identity.Application.Commands.Login; public class LoginCommandHandler : IRequestHandler { private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork; private readonly IJwtService _jwtService; public LoginCommandHandler( IUserRepository userRepository, IUnitOfWork unitOfWork, IJwtService jwtService) { _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); _jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService)); } public async Task Handle(LoginCommand request, CancellationToken cancellationToken) { var email = Email.Create(request.Email); // Get user by email var user = await _userRepository.GetByEmailAsync(email, cancellationToken); if (user == null) { throw new InvalidCredentialsException("Invalid email or password"); } // Verify password and record login (will throw if invalid) user.Login(request.Password); // Create refresh token var refreshToken = user.CreateRefreshToken(expiryDays: 7); // Generate JWT access token var accessToken = _jwtService.GenerateAccessToken(user); // Save changes (LastLoginAt, new refresh token) await _unitOfWork.CommitAsync(cancellationToken); return new LoginResponseDto { AccessToken = accessToken, RefreshToken = refreshToken.Token, ExpiresAt = _jwtService.GetTokenExpiration(), User = new UserDto { Id = user.Id.Value, Email = user.Email.Value, FirstName = user.FirstName, LastName = user.LastName, Role = user.Role.Name, IsActive = user.IsActive, CreatedAt = user.CreatedAt } }; } } ``` #### RefreshTokenCommand ```csharp // ColaFlow.Modules.Identity.Application/Commands/RefreshToken/RefreshTokenCommand.cs using MediatR; using ColaFlow.Modules.Identity.Application.DTOs; namespace ColaFlow.Modules.Identity.Application.Commands.RefreshToken; public record RefreshTokenCommand(string RefreshToken) : IRequest; ``` ```csharp // ColaFlow.Modules.Identity.Application/Commands/RefreshToken/RefreshTokenCommandHandler.cs using MediatR; using ColaFlow.Modules.Identity.Domain.Repositories; using ColaFlow.Modules.Identity.Domain.Exceptions; using ColaFlow.Modules.Identity.Application.DTOs; using ColaFlow.Modules.Identity.Application.Services; namespace ColaFlow.Modules.Identity.Application.Commands.RefreshToken; public class RefreshTokenCommandHandler : IRequestHandler { private readonly IUserRepository _userRepository; private readonly IUnitOfWork _unitOfWork; private readonly IJwtService _jwtService; public RefreshTokenCommandHandler( IUserRepository userRepository, IUnitOfWork unitOfWork, IJwtService jwtService) { _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); _jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService)); } public async Task Handle(RefreshTokenCommand request, CancellationToken cancellationToken) { // Find user by refresh token var user = await _userRepository.GetByRefreshTokenAsync(request.RefreshToken, cancellationToken); if (user == null) { throw new RefreshTokenExpiredException("Invalid refresh token"); } // Find the specific refresh token var refreshTokenEntity = user.RefreshTokens .FirstOrDefault(rt => rt.Token == request.RefreshToken); if (refreshTokenEntity == null) { throw new RefreshTokenExpiredException("Invalid refresh token"); } // Validate token (will throw if expired or revoked) refreshTokenEntity.Validate(); // Revoke old token (Refresh Token Rotation) refreshTokenEntity.Revoke(); // Create new refresh token var newRefreshToken = user.CreateRefreshToken(expiryDays: 7); // Generate new JWT access token var accessToken = _jwtService.GenerateAccessToken(user); // Save changes await _unitOfWork.CommitAsync(cancellationToken); return new LoginResponseDto { AccessToken = accessToken, RefreshToken = newRefreshToken.Token, ExpiresAt = _jwtService.GetTokenExpiration(), User = new UserDto { Id = user.Id.Value, Email = user.Email.Value, FirstName = user.FirstName, LastName = user.LastName, Role = user.Role.Name, IsActive = user.IsActive, CreatedAt = user.CreatedAt } }; } } ``` #### DTOs ```csharp // ColaFlow.Modules.Identity.Application/DTOs/UserDto.cs namespace ColaFlow.Modules.Identity.Application.DTOs; public class UserDto { public Guid Id { get; set; } public string Email { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public string Role { get; set; } = string.Empty; public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } } ``` ```csharp // ColaFlow.Modules.Identity.Application/DTOs/LoginResponseDto.cs namespace ColaFlow.Modules.Identity.Application.DTOs; public class LoginResponseDto { public string AccessToken { get; set; } = string.Empty; public string RefreshToken { get; set; } = string.Empty; public DateTime ExpiresAt { get; set; } public UserDto User { get; set; } = null!; } ``` #### IJwtService Interface ```csharp // ColaFlow.Modules.Identity.Application/Services/IJwtService.cs using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; namespace ColaFlow.Modules.Identity.Application.Services; /// /// JWT token generation and validation service /// Implemented in Infrastructure layer /// public interface IJwtService { /// /// Generate JWT access token for user (includes tenant context) /// string GenerateAccessToken(User user, Tenant tenant); /// /// Get token expiration time /// DateTime GetTokenExpiration(); /// /// Validate JWT token and extract user ID /// Guid? ValidateToken(string token); } ``` --- ### 3.3 Infrastructure Layer - JWT Service Implementation ```csharp // ColaFlow.Modules.Identity.Infrastructure/Services/JwtService.cs using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.IdentityModel.Tokens; using ColaFlow.Modules.Identity.Application.Services; using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; namespace ColaFlow.Modules.Identity.Infrastructure.Services; public class JwtService : IJwtService { private readonly string _secretKey; private readonly string _issuer; private readonly string _audience; private readonly int _expiryMinutes; public JwtService(IConfiguration configuration) { var jwtSection = configuration.GetSection("Jwt"); _secretKey = jwtSection["SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey not configured"); _issuer = jwtSection["Issuer"] ?? "ColaFlow"; _audience = jwtSection["Audience"] ?? "ColaFlow-API"; _expiryMinutes = int.Parse(jwtSection["ExpiryMinutes"] ?? "60"); // Validate key length (minimum 256 bits for HS256) if (_secretKey.Length < 32) { throw new InvalidOperationException("JWT SecretKey must be at least 32 characters (256 bits)"); } } public string GenerateAccessToken(User user, Tenant tenant) { var claims = new[] { // Standard JWT claims new Claim(JwtRegisteredClaimNames.Sub, user.Id.Value.ToString()), new Claim(JwtRegisteredClaimNames.Email, user.Email.Value), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // Unique token ID new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), // Issued at // User claims new Claim(ClaimTypes.Role, user.Role.Name), new Claim("firstName", user.FirstName), new Claim("lastName", user.LastName), // Multi-tenant claims (NEW) new Claim("tenant_id", user.TenantId.Value.ToString()), new Claim("tenant_slug", tenant.Slug.Value), new Claim("tenant_plan", tenant.Plan.ToString()), // SSO claims (if applicable) new Claim("auth_provider", user.AuthProvider.ToString()), new Claim("auth_provider_id", user.ExternalUserId ?? string.Empty) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey)); var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: _issuer, audience: _audience, claims: claims, expires: DateTime.UtcNow.AddMinutes(_expiryMinutes), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } public DateTime GetTokenExpiration() { return DateTime.UtcNow.AddMinutes(_expiryMinutes); } public Guid? ValidateToken(string token) { try { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.UTF8.GetBytes(_secretKey); var validationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = _issuer, ValidAudience = _audience, IssuerSigningKey = new SymmetricSecurityKey(key), ClockSkew = TimeSpan.Zero // No tolerance for expiration }; var principal = tokenHandler.ValidateToken(token, validationParameters, out _); var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub); if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId)) { return userId; } return null; } catch { return null; } } } ``` --- ### 3.4 Multi-Tenant JWT Claims Structure (UPDATED FOR MULTI-TENANCY) #### JWT Token Payload Example ```json { // Standard JWT claims "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", // User ID "email": "john.doe@acme.com", "jti": "f9e8d7c6-b5a4-3210-9876-543210fedcba", // Token ID "iat": 1704067200, // Issued at (Unix timestamp) "exp": 1704070800, // Expires at (Unix timestamp) // User claims "role": "User", "firstName": "John", "lastName": "Doe", // Multi-tenant claims (NEW) "tenant_id": "tenant-uuid-1234-5678-9abc-def0", "tenant_slug": "acme", "tenant_plan": "Enterprise", // SSO claims (if applicable) "auth_provider": "AzureAD", "auth_provider_id": "azure-user-id-123", // JWT standard claims "iss": "ColaFlow", "aud": "ColaFlow-API" } ``` #### Updated Login Flow with Tenant Context ```csharp // LoginCommandHandler - Updated to include tenant public async Task Handle(LoginCommand request, CancellationToken cancellationToken) { // 1. Resolve tenant from subdomain (via TenantResolutionMiddleware) var tenant = await _context.Tenants .IgnoreQueryFilters() .FirstAsync(t => t.Id == _tenantContext.CurrentTenantId, cancellationToken); // 2. Get user by email (within tenant scope) var email = Email.Create(request.Email); var user = await _userRepository.GetByEmailAsync(email, cancellationToken); if (user == null || user.TenantId != tenant.Id) { throw new InvalidCredentialsException("Invalid email or password"); } // 3. Verify password and record login user.Login(request.Password); // 4. Create refresh token var refreshToken = user.CreateRefreshToken(expiryDays: 7); // 5. Generate JWT with tenant context (UPDATED) var accessToken = _jwtService.GenerateAccessToken(user, tenant); // 6. Save changes await _unitOfWork.CommitAsync(cancellationToken); return new LoginResponseDto { AccessToken = accessToken, RefreshToken = refreshToken.Token, ExpiresAt = _jwtService.GetTokenExpiration(), User = new UserDto { Id = user.Id.Value, Email = user.Email.Value, FirstName = user.FirstName, LastName = user.LastName, Role = user.Role.Name, IsActive = user.IsActive, CreatedAt = user.CreatedAt, // Multi-tenant fields (NEW) TenantId = user.TenantId.Value, TenantName = tenant.Name.Value, TenantSlug = tenant.Slug.Value } }; } ``` #### Updated UserDto ```csharp // ColaFlow.Modules.Identity.Application/DTOs/UserDto.cs (Updated) public class UserDto { public Guid Id { get; set; } public string Email { get; set; } = string.Empty; public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public string Role { get; set; } = string.Empty; public bool IsActive { get; set; } public DateTime CreatedAt { get; set; } // Multi-tenant fields (NEW) public Guid TenantId { get; set; } public string TenantName { get; set; } = string.Empty; public string TenantSlug { get; set; } = string.Empty; // SSO fields (NEW) public string AuthProvider { get; set; } = "Local"; public string? ExternalUserId { get; set; } } ``` #### How Tenant Context is Injected 1. **Request arrives** at `acme.colaflow.com/api/v1/auth/login` 2. **TenantResolutionMiddleware** extracts subdomain `"acme"` 3. **Middleware queries** `tenants` table to find tenant with `slug = "acme"` 4. **Middleware injects** `TenantContext` into HTTP context items 5. **LoginCommandHandler** retrieves tenant from `TenantContext` 6. **JWT is generated** with tenant claims embedded 7. **All subsequent requests** use tenant claims from JWT for filtering #### Security Benefits - **Single JWT contains all context**: No need for additional database lookups - **Tenant isolation enforced**: Every API call validates tenant from JWT - **Cross-tenant attacks prevented**: User cannot access other tenant's data - **SSO integration ready**: Auth provider claims already included - **Audit-friendly**: Token contains complete identity context --- ### 3.5 Infrastructure Layer - Database Configuration #### EF Core Entity Configurations ```csharp // ColaFlow.Modules.Identity.Infrastructure/Persistence/Configurations/UserConfiguration.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; using ColaFlow.Modules.Identity.Domain.ValueObjects; namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Configurations; public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("users"); builder.HasKey(u => u.Id); builder.Property(u => u.Id) .HasConversion( id => id.Value, value => UserId.Create(value)) .HasColumnName("id"); builder.Property(u => u.Email) .HasConversion( email => email.Value, value => Email.Create(value)) .HasColumnName("email") .HasMaxLength(254) .IsRequired(); builder.HasIndex(u => u.Email) .IsUnique() .HasDatabaseName("ix_users_email"); builder.Property(u => u.PasswordHash) .HasConversion( hash => hash.Value, value => PasswordHash.Create(value)) // Note: This won't rehash, just wraps existing hash .HasColumnName("password_hash") .HasMaxLength(255) .IsRequired(); builder.Property(u => u.FirstName) .HasColumnName("first_name") .HasMaxLength(100) .IsRequired(); builder.Property(u => u.LastName) .HasColumnName("last_name") .HasMaxLength(100) .IsRequired(); builder.Property(u => u.Role) .HasConversion( role => role.Name, name => Role.FromName(name)) .HasColumnName("role") .HasMaxLength(50) .IsRequired(); builder.Property(u => u.IsActive) .HasColumnName("is_active") .IsRequired(); builder.Property(u => u.CreatedAt) .HasColumnName("created_at") .IsRequired(); builder.Property(u => u.UpdatedAt) .HasColumnName("updated_at"); builder.Property(u => u.LastLoginAt) .HasColumnName("last_login_at"); // One-to-many relationship with RefreshTokens builder.HasMany(u => u.RefreshTokens) .WithOne(rt => rt.User) .HasForeignKey(rt => rt.UserId) .OnDelete(DeleteBehavior.Cascade); // Ignore domain events (not persisted) builder.Ignore(u => u.DomainEvents); } } ``` ```csharp // ColaFlow.Modules.Identity.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; using ColaFlow.Modules.Identity.Domain.ValueObjects; namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Configurations; public class RefreshTokenConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("refresh_tokens"); builder.HasKey(rt => rt.Id); builder.Property(rt => rt.Id) .HasColumnName("id"); builder.Property(rt => rt.UserId) .HasConversion( id => id.Value, value => UserId.Create(value)) .HasColumnName("user_id") .IsRequired(); builder.Property(rt => rt.Token) .HasColumnName("token") .HasMaxLength(500) .IsRequired(); builder.HasIndex(rt => rt.Token) .IsUnique() .HasDatabaseName("ix_refresh_tokens_token"); builder.Property(rt => rt.ExpiresAt) .HasColumnName("expires_at") .IsRequired(); builder.Property(rt => rt.CreatedAt) .HasColumnName("created_at") .IsRequired(); builder.Property(rt => rt.IsRevoked) .HasColumnName("is_revoked") .IsRequired(); builder.Property(rt => rt.RevokedAt) .HasColumnName("revoked_at"); // Index for cleanup queries (find expired/revoked tokens) builder.HasIndex(rt => new { rt.IsRevoked, rt.ExpiresAt }) .HasDatabaseName("ix_refresh_tokens_cleanup"); } } ``` #### Database Context ```csharp // ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.cs using Microsoft.EntityFrameworkCore; using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; using ColaFlow.Modules.Identity.Infrastructure.Persistence.Configurations; namespace ColaFlow.Modules.Identity.Infrastructure.Persistence; public class IdentityDbContext : DbContext { public DbSet Users { get; set; } = null!; public DbSet RefreshTokens { get; set; } = null!; public IdentityDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Apply all configurations modelBuilder.ApplyConfiguration(new UserConfiguration()); modelBuilder.ApplyConfiguration(new RefreshTokenConfiguration()); } } ``` #### UserRepository Implementation ```csharp // ColaFlow.Modules.Identity.Infrastructure/Repositories/UserRepository.cs using Microsoft.EntityFrameworkCore; using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; using ColaFlow.Modules.Identity.Domain.Repositories; using ColaFlow.Modules.Identity.Domain.ValueObjects; using ColaFlow.Modules.Identity.Infrastructure.Persistence; namespace ColaFlow.Modules.Identity.Infrastructure.Repositories; public class UserRepository : IUserRepository { private readonly IdentityDbContext _context; public UserRepository(IdentityDbContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } public async Task GetByIdAsync(UserId id, CancellationToken cancellationToken = default) { return await _context.Users .Include(u => u.RefreshTokens) .FirstOrDefaultAsync(u => u.Id == id, cancellationToken); } public async Task GetByEmailAsync(Email email, CancellationToken cancellationToken = default) { return await _context.Users .Include(u => u.RefreshTokens) .FirstOrDefaultAsync(u => u.Email == email, cancellationToken); } public async Task GetByRefreshTokenAsync(string refreshToken, CancellationToken cancellationToken = default) { return await _context.Users .Include(u => u.RefreshTokens) .FirstOrDefaultAsync(u => u.RefreshTokens.Any(rt => rt.Token == refreshToken), cancellationToken); } public async Task AddAsync(User user, CancellationToken cancellationToken = default) { await _context.Users.AddAsync(user, cancellationToken); } public void Update(User user) { _context.Users.Update(user); } public void Remove(User user) { _context.Users.Remove(user); } } ``` --- ### 3.5 API Layer - Controllers & Middleware #### AuthController ```csharp // ColaFlow.API/Controllers/AuthController.cs using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using ColaFlow.Modules.Identity.Application.Commands.RegisterUser; using ColaFlow.Modules.Identity.Application.Commands.Login; using ColaFlow.Modules.Identity.Application.Commands.RefreshToken; using ColaFlow.Modules.Identity.Application.Commands.Logout; using ColaFlow.Modules.Identity.Application.Queries.GetCurrentUser; using ColaFlow.Modules.Identity.Application.DTOs; namespace ColaFlow.API.Controllers; /// /// Authentication API Controller /// [ApiController] [Route("api/v1/[controller]")] public class AuthController : ControllerBase { private readonly IMediator _mediator; public AuthController(IMediator mediator) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); } /// /// Register a new user /// [HttpPost("register")] [AllowAnonymous] [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task Register( [FromBody] RegisterUserCommand command, CancellationToken cancellationToken = default) { var result = await _mediator.Send(command, cancellationToken); return CreatedAtAction(nameof(GetCurrentUser), result); } /// /// Login with email and password /// [HttpPost("login")] [AllowAnonymous] [ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task Login( [FromBody] LoginCommand command, CancellationToken cancellationToken = default) { var result = await _mediator.Send(command, cancellationToken); // Set refresh token in httpOnly cookie SetRefreshTokenCookie(result.RefreshToken); return Ok(result); } /// /// Refresh access token using refresh token /// [HttpPost("refresh")] [AllowAnonymous] [ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task RefreshToken(CancellationToken cancellationToken = default) { // Get refresh token from cookie var refreshToken = Request.Cookies["refreshToken"]; if (string.IsNullOrEmpty(refreshToken)) { return Unauthorized("Refresh token not found"); } var command = new RefreshTokenCommand(refreshToken); var result = await _mediator.Send(command, cancellationToken); // Update refresh token cookie SetRefreshTokenCookie(result.RefreshToken); return Ok(result); } /// /// Logout and revoke refresh token /// [HttpPost("logout")] [Authorize] [ProducesResponseType(StatusCodes.Status204NoContent)] public async Task Logout(CancellationToken cancellationToken = default) { var refreshToken = Request.Cookies["refreshToken"]; if (!string.IsNullOrEmpty(refreshToken)) { var command = new LogoutCommand(refreshToken); await _mediator.Send(command, cancellationToken); } // Clear cookie Response.Cookies.Delete("refreshToken"); return NoContent(); } /// /// Get current authenticated user /// [HttpGet("me")] [Authorize] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task GetCurrentUser(CancellationToken cancellationToken = default) { var userId = GetUserIdFromClaims(); var query = new GetCurrentUserQuery(userId); var result = await _mediator.Send(query, cancellationToken); return Ok(result); } private void SetRefreshTokenCookie(string refreshToken) { var cookieOptions = new CookieOptions { HttpOnly = true, // Cannot be accessed by JavaScript Secure = true, // HTTPS only SameSite = SameSiteMode.Strict, // CSRF protection Expires = DateTimeOffset.UtcNow.AddDays(7) }; Response.Cookies.Append("refreshToken", refreshToken, cookieOptions); } private Guid GetUserIdFromClaims() { var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value; if (userIdClaim != null && Guid.TryParse(userIdClaim, out var userId)) { return userId; } throw new UnauthorizedAccessException("User ID not found in token"); } } ``` #### Program.cs - JWT Configuration ```csharp // ColaFlow.API/Program.cs (updated) using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using ColaFlow.API.Extensions; using ColaFlow.API.Handlers; using Scalar.AspNetCore; var builder = WebApplication.CreateBuilder(args); // Register ProjectManagement Module builder.Services.AddProjectManagementModule(builder.Configuration); // Register Identity Module (NEW) builder.Services.AddIdentityModule(builder.Configuration); // Add controllers builder.Services.AddControllers(); // Configure JWT Authentication (NEW) var jwtSection = builder.Configuration.GetSection("Jwt"); var secretKey = jwtSection["SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey not configured"); builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtSection["Issuer"], ValidAudience = jwtSection["Audience"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)), ClockSkew = TimeSpan.Zero }; options.Events = new JwtBearerEvents { OnAuthenticationFailed = context => { if (context.Exception is SecurityTokenExpiredException) { context.Response.Headers.Add("Token-Expired", "true"); } return Task.CompletedTask; } }; }); builder.Services.AddAuthorization(); // Configure exception handling (IExceptionHandler - .NET 8+) builder.Services.AddExceptionHandler(); builder.Services.AddProblemDetails(); // Configure CORS for frontend builder.Services.AddCors(options => { options.AddPolicy("AllowFrontend", policy => { policy.WithOrigins("http://localhost:3000") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // Required for cookies }); }); // Configure OpenAPI/Scalar builder.Services.AddOpenApi(); var app = builder.Build(); // Configure the HTTP request pipeline if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(); } // Global exception handler (should be first in pipeline) app.UseExceptionHandler(); // Enable CORS app.UseCors("AllowFrontend"); app.UseHttpsRedirection(); // Authentication & Authorization (NEW) app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run(); ``` #### Protecting Existing Controllers ```csharp // ColaFlow.API/Controllers/ProjectsController.cs (updated) using MediatR; using Microsoft.AspNetCore.Authorization; // NEW using Microsoft.AspNetCore.Mvc; using ColaFlow.Modules.ProjectManagement.Application.DTOs; using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject; using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById; using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects; namespace ColaFlow.API.Controllers; /// /// Projects API Controller /// [ApiController] [Route("api/v1/[controller]")] [Authorize] // NEW - Protect all endpoints public class ProjectsController : ControllerBase { private readonly IMediator _mediator; public ProjectsController(IMediator mediator) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); } // ... rest of the controller code remains the same } ``` --- ## 4. Database Schema ### SQL DDL ```sql -- Users table CREATE TABLE users ( id UUID PRIMARY KEY, email VARCHAR(254) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, role VARCHAR(50) NOT NULL, is_active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NULL, last_login_at TIMESTAMP NULL ); CREATE INDEX ix_users_email ON users(email); CREATE INDEX ix_users_role ON users(role); CREATE INDEX ix_users_is_active ON users(is_active); -- Refresh tokens table CREATE TABLE refresh_tokens ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token VARCHAR(500) NOT NULL UNIQUE, expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, is_revoked BOOLEAN NOT NULL DEFAULT FALSE, revoked_at TIMESTAMP NULL ); CREATE INDEX ix_refresh_tokens_token ON refresh_tokens(token); CREATE INDEX ix_refresh_tokens_user_id ON refresh_tokens(user_id); CREATE INDEX ix_refresh_tokens_cleanup ON refresh_tokens(is_revoked, expires_at); ``` ### EF Core Migration Command ```bash # In colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure dotnet ef migrations add InitialIdentitySchema --context IdentityDbContext --output-dir Persistence/Migrations # Apply migration dotnet ef database update --context IdentityDbContext ``` --- ## 5. Configuration ### appsettings.json ```json { "ConnectionStrings": { "DefaultConnection": "Host=localhost;Port=5432;Database=colaflow;Username=postgres;Password=postgres", "IdentityConnection": "Host=localhost;Port=5432;Database=colaflow;Username=postgres;Password=postgres" }, "Jwt": { "SecretKey": "YOUR-256-BIT-SECRET-KEY-MINIMUM-32-CHARACTERS-LONG-REPLACE-IN-PRODUCTION", "Issuer": "ColaFlow", "Audience": "ColaFlow-API", "ExpiryMinutes": 60 }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } ``` ### appsettings.Development.json ```json { "Jwt": { "SecretKey": "development-secret-key-32-chars-minimum-do-not-use-in-production", "ExpiryMinutes": 60 } } ``` ### Environment Variables (Production) ```bash # NEVER commit secrets to git export JWT__SECRETKEY="" export JWT__ISSUER="ColaFlow" export JWT__AUDIENCE="ColaFlow-API" export JWT__EXPIRYMINUTES="60" ``` **Generate Secret Key**: ```bash # PowerShell -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | % {[char]$_}) # Linux/Mac openssl rand -base64 48 ``` --- ## 6. Frontend Integration (Next.js 16) ### 6.1 Authentication Store (Zustand) ```typescript // colaflow-web/src/stores/authStore.ts import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface User { id: string; email: string; firstName: string; lastName: string; role: string; isActive: boolean; } interface AuthState { user: User | null; accessToken: string | null; isAuthenticated: boolean; login: (accessToken: string, user: User) => void; logout: () => void; updateToken: (accessToken: string) => void; } export const useAuthStore = create()( persist( (set) => ({ user: null, accessToken: null, isAuthenticated: false, login: (accessToken, user) => set({ accessToken, user, isAuthenticated: true, }), logout: () => set({ accessToken: null, user: null, isAuthenticated: false, }), updateToken: (accessToken) => set({ accessToken }), }), { name: 'auth-storage', partialize: (state) => ({ user: state.user, // Don't persist accessToken (security) }), } ) ); ``` ### 6.2 API Client with Token Refresh ```typescript // colaflow-web/src/lib/api-client.ts import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios'; import { useAuthStore } from '@/stores/authStore'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://localhost:7001/api/v1'; export const apiClient = axios.create({ baseURL: API_BASE_URL, withCredentials: true, // Include cookies (refresh token) headers: { 'Content-Type': 'application/json', }, }); // Request interceptor - Add access token apiClient.interceptors.request.use( (config: InternalAxiosRequestConfig) => { const { accessToken } = useAuthStore.getState(); if (accessToken && config.headers) { config.headers.Authorization = `Bearer ${accessToken}`; } return config; }, (error) => Promise.reject(error) ); // Response interceptor - Handle token refresh let isRefreshing = false; let refreshSubscribers: ((token: string) => void)[] = []; function subscribeTokenRefresh(cb: (token: string) => void) { refreshSubscribers.push(cb); } function onTokenRefreshed(token: string) { refreshSubscribers.forEach((cb) => cb(token)); refreshSubscribers = []; } apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; // If 401 and not already retrying if (error.response?.status === 401 && !originalRequest._retry) { if (isRefreshing) { // Wait for token refresh return new Promise((resolve) => { subscribeTokenRefresh((token: string) => { if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${token}`; } resolve(apiClient(originalRequest)); }); }); } originalRequest._retry = true; isRefreshing = true; try { // Call refresh endpoint const response = await axios.post( `${API_BASE_URL}/auth/refresh`, {}, { withCredentials: true } ); const { accessToken } = response.data; // Update store useAuthStore.getState().updateToken(accessToken); // Notify subscribers onTokenRefreshed(accessToken); // Retry original request if (originalRequest.headers) { originalRequest.headers.Authorization = `Bearer ${accessToken}`; } return apiClient(originalRequest); } catch (refreshError) { // Refresh failed - logout useAuthStore.getState().logout(); window.location.href = '/login'; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); } ); ``` ### 6.3 Auth API Functions ```typescript // colaflow-web/src/services/auth.service.ts import { apiClient } from '@/lib/api-client'; export interface RegisterRequest { email: string; password: string; firstName: string; lastName: string; } export interface LoginRequest { email: string; password: string; } export interface LoginResponse { accessToken: string; refreshToken: string; expiresAt: string; user: { id: string; email: string; firstName: string; lastName: string; role: string; isActive: boolean; }; } export const authService = { register: (data: RegisterRequest) => apiClient.post('/auth/register', data), login: async (data: LoginRequest): Promise => { const response = await apiClient.post('/auth/login', data); return response.data; }, logout: () => apiClient.post('/auth/logout'), getCurrentUser: () => apiClient.get('/auth/me'), refreshToken: () => apiClient.post('/auth/refresh'), }; ``` ### 6.4 Login Page ```typescript // colaflow-web/src/app/(auth)/login/page.tsx 'use client'; import { useState } from 'react'; import { useRouter } from 'next/navigation'; import { useAuthStore } from '@/stores/authStore'; import { authService } from '@/services/auth.service'; export default function LoginPage() { const router = useRouter(); const login = useAuthStore((state) => state.login); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); setLoading(true); try { const response = await authService.login({ email, password }); login(response.accessToken, response.user); router.push('/dashboard'); } catch (err: any) { setError(err.response?.data?.message || 'Login failed'); } finally { setLoading(false); } }; return ( Login to ColaFlow {error && ( {error} )} Email setEmail(e.target.value)} required className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" /> Password setPassword(e.target.value)} required className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" /> {loading ? 'Logging in...' : 'Login'} Don't have an account?{' '} Register ); } ``` ### 6.5 Protected Route Middleware ```typescript // colaflow-web/src/middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; const publicPaths = ['/login', '/register']; const authPaths = ['/login', '/register']; export function middleware(request: NextRequest) { const { pathname } = request.nextUrl; // Check if user has auth token (stored in cookie or localStorage) // Note: In production, validate token server-side const isAuthenticated = request.cookies.has('refreshToken'); // Redirect authenticated users away from auth pages if (isAuthenticated && authPaths.includes(pathname)) { return NextResponse.redirect(new URL('/dashboard', request.url)); } // Redirect unauthenticated users to login if (!isAuthenticated && !publicPaths.includes(pathname)) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } export const config = { matcher: [ /* * Match all request paths except: * - _next/static (static files) * - _next/image (image optimization files) * - favicon.ico (favicon file) * - public files (public directory) */ '/((?!_next/static|_next/image|favicon.ico|public).*)', ], }; ``` --- ## 7. Security Considerations ### 7.1 OWASP Top 10 Compliance | Risk | Mitigation | |------|------------| | **A01: Broken Access Control** | - JWT-based authentication- `[Authorize]` attribute on all protected endpoints- Role-based authorization | | **A02: Cryptographic Failures** | - BCrypt password hashing (work factor 12)- HTTPS enforced- httpOnly cookies for refresh tokens- Minimum 256-bit JWT signing key | | **A03: Injection** | - EF Core parameterized queries- Input validation with FluentValidation- Email regex validation | | **A04: Insecure Design** | - Refresh Token Rotation- Token expiration (60 min access, 7 day refresh)- Password strength requirements- Account lockout (future) | | **A05: Security Misconfiguration** | - Environment-specific config- Secrets in environment variables- CORS configured for specific origin- Disable unnecessary features | | **A06: Vulnerable Components** | - Latest .NET 9 and packages- Regular dependency updates- Security scanning in CI/CD | | **A07: Authentication Failures** | - Strong password policy- Refresh token rotation- Revoke tokens on password change- Limit active refresh tokens per user | | **A08: Software/Data Integrity** | - Audit logs (domain events)- Signed JWT tokens- Immutable domain events | | **A09: Logging Failures** | - UserLoggedInEvent- UserRegisteredEvent- Failed login attempts (future)- Audit all authentication events | | **A10: SSRF** | - No external requests based on user input in auth flow | ### 7.2 Token Security **Access Token**: - Short-lived (60 minutes) - Stored in memory (Zustand store, not persisted) - Sent in `Authorization: Bearer ` header - Cannot be revoked (design trade-off for performance) **Refresh Token**: - Long-lived (7 days) - Stored in httpOnly cookie (XSS protection) - Secure + SameSite=Strict (CSRF protection) - Can be revoked (stored in database) - Rotation on each use (prevents replay attacks) - Max 5 active tokens per user ### 7.3 Password Security - **Algorithm**: BCrypt with work factor 12 - **Requirements**: - Minimum 8 characters - Maximum 128 characters - At least one uppercase letter - At least one lowercase letter - At least one digit - At least one special character - **Storage**: Hashed with salt (BCrypt handles this) - **Transmission**: HTTPS only ### 7.4 Additional Security Measures **Recommended (Future)**: 1. **Rate Limiting**: Limit login attempts (e.g., 5 per 15 minutes) 2. **Account Lockout**: Lock account after N failed attempts 3. **2FA**: Two-factor authentication support 4. **Email Verification**: Verify email on registration 5. **Password Reset**: Secure password reset flow 6. **Audit Logging**: Log all authentication events to database 7. **Token Blacklist**: Blacklist access tokens on logout (requires Redis) 8. **IP Whitelisting**: Optional IP-based restrictions for admin users 9. **Session Management**: Track active sessions, allow user to revoke --- ## 8. Testing Strategy ### 8.1 Unit Tests (Domain Layer) ```csharp // ColaFlow.Domain.Tests/UserTests.cs using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate; using ColaFlow.Modules.Identity.Domain.ValueObjects; using ColaFlow.Modules.Identity.Domain.Exceptions; using Xunit; namespace ColaFlow.Domain.Tests; public class UserTests { [Fact] public void Create_ValidUser_Success() { // Arrange var email = Email.Create("test@example.com"); var password = "SecureP@ss123"; var firstName = "John"; var lastName = "Doe"; var role = Role.User; // Act var user = User.Create(email, password, firstName, lastName, role); // Assert Assert.NotNull(user); Assert.Equal(email, user.Email); Assert.True(user.IsActive); Assert.Single(user.DomainEvents); // UserRegisteredEvent } [Fact] public void Login_InvalidPassword_ThrowsException() { // Arrange var user = User.Create( Email.Create("test@example.com"), "SecureP@ss123", "John", "Doe", Role.User ); // Act & Assert Assert.Throws(() => user.Login("WrongPassword")); } [Fact] public void CreateRefreshToken_Success() { // Arrange var user = User.Create( Email.Create("test@example.com"), "SecureP@ss123", "John", "Doe", Role.User ); // Act var refreshToken = user.CreateRefreshToken(7); // Assert Assert.NotNull(refreshToken); Assert.Equal(user.Id, refreshToken.UserId); Assert.False(refreshToken.IsRevoked); Assert.True(refreshToken.ExpiresAt > DateTime.UtcNow); } [Theory] [InlineData("short")] // Too short [InlineData("nouppercase123!")] // No uppercase [InlineData("NOLOWERCASE123!")] // No lowercase [InlineData("NoDigits!")] // No digits [InlineData("NoSpecialChar123")] // No special char public void Create_WeakPassword_ThrowsException(string weakPassword) { // Arrange & Act & Assert Assert.Throws(() => User.Create( Email.Create("test@example.com"), weakPassword, "John", "Doe", Role.User ) ); } } ``` ### 8.2 Integration Tests (Application + Infrastructure) ```csharp // ColaFlow.IntegrationTests/AuthenticationTests.cs using ColaFlow.Modules.Identity.Application.Commands.RegisterUser; using ColaFlow.Modules.Identity.Application.Commands.Login; using ColaFlow.Modules.Identity.Domain.Exceptions; using MediatR; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace ColaFlow.IntegrationTests; public class AuthenticationTests : IClassFixture { private readonly IMediator _mediator; public AuthenticationTests(TestWebApplicationFactory factory) { _mediator = factory.Services.GetRequiredService(); } [Fact] public async Task RegisterAndLogin_ValidCredentials_Success() { // Arrange var registerCommand = new RegisterUserCommand( Email: "integration@test.com", Password: "TestPass123!", FirstName: "Test", LastName: "User" ); // Act - Register var user = await _mediator.Send(registerCommand); // Assert - Register Assert.NotNull(user); Assert.Equal("integration@test.com", user.Email); // Act - Login var loginCommand = new LoginCommand("integration@test.com", "TestPass123!"); var loginResponse = await _mediator.Send(loginCommand); // Assert - Login Assert.NotNull(loginResponse); Assert.NotEmpty(loginResponse.AccessToken); Assert.NotEmpty(loginResponse.RefreshToken); Assert.Equal(user.Id, loginResponse.User.Id); } [Fact] public async Task Login_InvalidCredentials_ThrowsException() { // Arrange var loginCommand = new LoginCommand("nonexistent@test.com", "WrongPassword"); // Act & Assert await Assert.ThrowsAsync( async () => await _mediator.Send(loginCommand) ); } } ``` ### 8.3 API Tests (End-to-End) ```csharp // ColaFlow.IntegrationTests/AuthControllerTests.cs using System.Net; using System.Net.Http.Json; using ColaFlow.Modules.Identity.Application.DTOs; using Xunit; namespace ColaFlow.IntegrationTests; public class AuthControllerTests : IClassFixture { private readonly HttpClient _client; public AuthControllerTests(TestWebApplicationFactory factory) { _client = factory.CreateClient(); } [Fact] public async Task POST_Register_ReturnsCreated() { // Arrange var request = new { Email = "apitest@example.com", Password = "ApiTest123!", FirstName = "API", LastName = "Test" }; // Act var response = await _client.PostAsJsonAsync("/api/v1/auth/register", request); // Assert Assert.Equal(HttpStatusCode.Created, response.StatusCode); var user = await response.Content.ReadFromJsonAsync(); Assert.NotNull(user); Assert.Equal(request.Email, user.Email); } [Fact] public async Task POST_Login_ReturnsToken() { // Arrange - First register var registerRequest = new { Email = "login@example.com", Password = "LoginTest123!", FirstName = "Login", LastName = "Test" }; await _client.PostAsJsonAsync("/api/v1/auth/register", registerRequest); // Act - Login var loginRequest = new { Email = "login@example.com", Password = "LoginTest123!" }; var response = await _client.PostAsJsonAsync("/api/v1/auth/login", loginRequest); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var loginResponse = await response.Content.ReadFromJsonAsync(); Assert.NotNull(loginResponse); Assert.NotEmpty(loginResponse.AccessToken); Assert.True(response.Headers.Contains("Set-Cookie")); // Refresh token cookie } [Fact] public async Task GET_Me_WithoutToken_ReturnsUnauthorized() { // Act var response = await _client.GetAsync("/api/v1/auth/me"); // Assert Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); } [Fact] public async Task GET_Me_WithValidToken_ReturnsUser() { // Arrange - Register and login var email = "me@example.com"; await RegisterAndLogin(email, "MeTest123!"); // Act var response = await _client.GetAsync("/api/v1/auth/me"); // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); var user = await response.Content.ReadFromJsonAsync(); Assert.NotNull(user); Assert.Equal(email, user.Email); } private async Task RegisterAndLogin(string email, string password) { // Register await _client.PostAsJsonAsync("/api/v1/auth/register", new { Email = email, Password = password, FirstName = "Test", LastName = "User" }); // Login var loginResponse = await _client.PostAsJsonAsync("/api/v1/auth/login", new { Email = email, Password = password }); var loginData = await loginResponse.Content.ReadFromJsonAsync(); _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", loginData!.AccessToken); } } ``` --- ## 9. Implementation Roadmap (7 Days) ### Day 1: Architecture & Domain Layer (Today) **Deliverables**: - ✅ Complete architecture document (this document) - ✅ Domain models (User, RefreshToken, Value Objects) - ✅ Domain events - ✅ Repository interfaces **Tasks**: 1. Review and approve this architecture document 2. Create Identity module structure 3. Implement User aggregate root 4. Implement value objects (Email, PasswordHash, Role) 5. Implement RefreshToken entity 6. Write unit tests for domain logic --- ### Day 2-3: Application & Infrastructure Layers **Deliverables**: - Commands (Register, Login, RefreshToken, Logout, ChangePassword) - Queries (GetCurrentUser, ValidateToken) - Command/Query handlers - FluentValidation validators - JwtService implementation - UserRepository implementation - EF Core configurations - Database migrations **Tasks**: 1. Implement all Commands and Handlers 2. Implement all Queries and Handlers 3. Add FluentValidation for input validation 4. Implement JwtService (token generation/validation) 5. Implement UserRepository with EF Core 6. Create EF Core entity configurations 7. Generate and test database migrations 8. Write integration tests --- ### Day 4: API Layer & Backend Integration **Deliverables**: - AuthController with all endpoints - JWT authentication middleware configuration - Protected controllers (`[Authorize]` attributes) - Global exception handling updates - API documentation (Scalar/OpenAPI) **Tasks**: 1. Create AuthController 2. Configure JWT authentication in Program.cs 3. Add `[Authorize]` to all protected controllers 4. Update GlobalExceptionHandler for auth exceptions 5. Configure CORS for frontend 6. Test all API endpoints with Postman/Scalar 7. Write API integration tests --- ### Day 5-6: Frontend Implementation **Deliverables**: - Zustand auth store - API client with token refresh interceptor - Login/Register pages - Protected route middleware - Auth API service functions - Dashboard with current user display **Tasks**: 1. Create Zustand auth store 2. Implement API client with Axios interceptors 3. Create auth service functions 4. Build Login page 5. Build Register page 6. Implement Next.js middleware for protected routes 7. Update existing pages to use auth 8. Add logout functionality 9. Handle token expiration gracefully --- ### Day 7: Testing, Integration & Documentation **Deliverables**: - Complete test suite (unit + integration + E2E) - CI/CD pipeline updates - Deployment guide - Security audit checklist - User documentation **Tasks**: 1. Run full test suite and fix any issues 2. Perform security audit (OWASP checklist) 3. Test token refresh flow end-to-end 4. Test all protected routes 5. Update CI/CD to run auth tests 6. Document deployment process 7. Create user guide for authentication 8. Final review and demo --- ## 10. Risk Assessment & Mitigation | Risk | Impact | Probability | Mitigation | |------|--------|-------------|------------| | **JWT Secret Key Exposure** | Critical | Low | - Store in environment variables- Never commit to git- Rotate keys periodically- Use strong random keys (256+ bits) | | **Password Database Breach** | High | Low | - BCrypt with work factor 12- Salted hashes- Rate limit login attempts- Monitor for breach patterns | | **Token Theft (XSS)** | High | Medium | - httpOnly cookies for refresh tokens- Short-lived access tokens- Content Security Policy (CSP)- Input sanitization | | **Token Theft (CSRF)** | Medium | Medium | - SameSite=Strict cookies- CORS configuration- Double-submit cookie pattern (future) | | **Refresh Token Replay** | Medium | Low | - Token rotation on each use- Revoke old tokens- Detect multiple concurrent uses | | **Performance Issues** | Medium | Low | - Index on email and token columns- Cache user lookups (Redis, future)- Connection pooling- Token validation is fast (stateless) | | **Breaking Existing APIs** | High | Low | - Add `[Authorize]` incrementally- Test all endpoints- Frontend updates synchronized- Allow grace period for migration | --- ## 11. Future Enhancements ### Phase 2 (Post-M1) 1. **Rate Limiting**: Protect against brute force attacks 2. **Account Lockout**: Lock after N failed login attempts 3. **Email Verification**: Confirm email on registration 4. **Password Reset**: Secure password reset flow via email 5. **Audit Logging**: Persist authentication events to database 6. **Admin User Management**: CRUD operations for admin users ### Phase 3 (M2-M3) 1. **Two-Factor Authentication (2FA)**: TOTP or SMS-based 2. **OAuth2/OpenID Connect**: Integration with Google, GitHub, etc. 3. **API Key Authentication**: For MCP client integrations 4. **Session Management UI**: View/revoke active sessions 5. **Token Blacklist**: Redis-based access token revocation 6. **Advanced RBAC**: Fine-grained permissions system ### Phase 4 (M4+) 1. **Single Sign-On (SSO)**: Enterprise SSO via SAML 2. **Passwordless Authentication**: Magic links, WebAuthn 3. **Security Dashboard**: Monitor suspicious activities 4. **Compliance Features**: GDPR data export, right to be forgotten --- ## 12. References & Resources ### .NET 9 & JWT - [Microsoft: ASP.NET Core Authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/) - [JWT.io - Official JWT Resource](https://jwt.io/) - [Microsoft: JWT Bearer Authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt-bearer) ### Security Best Practices - [OWASP Top 10](https://owasp.org/www-project-top-ten/) - [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html) - [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) - [OWASP JWT Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_for_Java_Cheat_Sheet.html) ### Clean Architecture & DDD - [Clean Architecture by Robert C. Martin](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) - [Domain-Driven Design by Eric Evans](https://www.domainlanguage.com/ddd/) - [CQRS Pattern](https://martinfowler.com/bliki/CQRS.html) ### Libraries & Tools - [BCrypt.Net](https://github.com/BcryptNet/bcrypt.net) - [FluentValidation](https://docs.fluentvalidation.net/) - [MediatR](https://github.com/jbogard/MediatR) - [EF Core](https://learn.microsoft.com/en-us/ef/core/) --- ## 13. Approval & Sign-off | Role | Name | Status | Date | |------|------|--------|------| | **Architect** | Architecture Team | ✅ Approved | 2025-11-03 | | **Product Manager** | - | Pending | - | | **Backend Lead** | - | Pending | - | | **Frontend Lead** | - | Pending | - | | **Security Review** | - | Pending | - | --- ## Appendix A: Quick Start Commands ### Backend Setup ```bash # Navigate to backend cd c:\Users\yaoji\git\ColaCoder\product-master\colaflow-api # Create Identity module structure mkdir -p src/Modules/Identity/ColaFlow.Modules.Identity.Domain mkdir -p src/Modules/Identity/ColaFlow.Modules.Identity.Application mkdir -p src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure # Install BCrypt package cd src/Modules/Identity/ColaFlow.Modules.Identity.Domain dotnet add package BCrypt.Net-Next # Generate migration cd ../ColaFlow.Modules.Identity.Infrastructure dotnet ef migrations add InitialIdentitySchema --context IdentityDbContext # Apply migration dotnet ef database update --context IdentityDbContext # Run backend cd ../../../ColaFlow.API dotnet run ``` ### Frontend Setup ```bash # Navigate to frontend cd c:\Users\yaoji\git\ColaCoder\product-master\colaflow-web # Install dependencies npm install axios zustand # Create auth store and services (copy code from sections 6.1-6.3) # Run frontend npm run dev ``` ### Testing ```bash # Run all tests dotnet test # Run with coverage dotnet test --collect:"XPlat Code Coverage" # Run specific test project dotnet test tests/ColaFlow.Domain.Tests ``` --- **END OF ARCHITECTURE DOCUMENT** This architecture design is comprehensive, production-ready, and aligned with Clean Architecture, DDD, and CQRS principles. It addresses all security requirements per OWASP standards and provides a clear 7-day implementation roadmap. For questions or clarifications, please contact the Architecture Team.
Don't have an account?{' '} Register