# Day 5 Architecture Design: Advanced Authentication & Authorization **Date**: 2025-11-03 **Author**: System Architect **Status**: Ready for Implementation --- ## Executive Summary This document provides comprehensive technical architecture for Day 5 development, focusing on three core security features: 1. **Refresh Token Mechanism** (Priority 1) 2. **Role-Based Authorization (RBAC)** (Priority 1) 3. **Email Verification Flow** (Priority 2) All designs are tailored for the existing .NET 9 + Clean Architecture + Multi-tenant system, with forward compatibility for future MCP Server integration. --- ## Table of Contents - [1. Refresh Token Mechanism](#1-refresh-token-mechanism) - [2. Role-Based Authorization (RBAC)](#2-role-based-authorization-rbac) - [3. Email Verification Flow](#3-email-verification-flow) - [4. Risk Assessment](#4-risk-assessment) - [5. Implementation Roadmap](#5-implementation-roadmap) - [6. MCP Integration Considerations](#6-mcp-integration-considerations) --- ## 1. Refresh Token Mechanism ### 1.1 Background & Goals **Problem**: Current JWT access tokens expire after 60 minutes, requiring users to re-login frequently. This degrades user experience and security. **Goals**: - Implement secure refresh token rotation - Support long-lived sessions (7-30 days) - Enable token revocation for security incidents - Prepare for distributed session management ### 1.2 Architecture Design #### 1.2.1 Token Flow Diagram ``` ┌─────────────┐ ┌─────────────┐ │ Client │ │ API Server │ └──────┬──────┘ └──────┬──────┘ │ │ │ 1. Login (credentials) │ ├────────────────────────────────>│ │ │ │ 2. Access Token (60 min) │ │ Refresh Token (7 days) │ │<────────────────────────────────┤ │ │ │ 3. API Request + Access Token │ ├────────────────────────────────>│ │ │ │ 4. Response (200 OK) │ │<────────────────────────────────┤ │ │ │ [After 60 minutes] │ │ │ │ 5. API Request + Expired Token │ ├────────────────────────────────>│ │ │ │ 6. 401 Unauthorized │ │<────────────────────────────────┤ │ │ │ 7. Refresh Token Request │ ├────────────────────────────────>│ │ │ │ 8. New Access Token (60 min) │ │ New Refresh Token (7 days) │ │<────────────────────────────────┤ │ │ ``` #### 1.2.2 Technology Decision: Database vs Redis **Comparison**: | Criteria | PostgreSQL | Redis | |----------|-----------|-------| | **Performance** | Good (indexed queries) | Excellent (in-memory) | | **Persistence** | Native (ACID) | Optional (AOF/RDB) | | **Complexity** | Low (existing stack) | Medium (new dependency) | | **Scalability** | Vertical + Read Replicas | Horizontal + Clustering | | **Query Capability** | Rich (SQL) | Limited (Key-Value) | | **Cost** | Included | Additional infrastructure | **Recommendation**: **PostgreSQL for MVP, Redis for Scale** **Rationale**: - Day 5 MVP: Use PostgreSQL to minimize new dependencies - PostgreSQL can handle 10K-100K users easily with proper indexing - Redis migration path is straightforward when scaling is needed - Reduces Day 5 complexity and deployment overhead #### 1.2.3 Database Schema Design ```sql -- New table for refresh tokens CREATE TABLE identity.refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), token_hash VARCHAR(128) NOT NULL UNIQUE, -- SHA-256 hash of token user_id UUID NOT NULL, tenant_id UUID NOT NULL, -- Token metadata expires_at TIMESTAMP NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT NOW(), revoked_at TIMESTAMP NULL, revoked_reason VARCHAR(500) NULL, -- Security tracking ip_address VARCHAR(45) NULL, -- IPv6 compatible user_agent VARCHAR(500) NULL, last_used_at TIMESTAMP NULL, -- Token family for rotation token_family UUID NOT NULL, -- Group rotated tokens together replaced_by_token_id UUID NULL, -- Link to next token in chain -- Indexes CONSTRAINT fk_refresh_tokens_user FOREIGN KEY (user_id) REFERENCES identity.users(id) ON DELETE CASCADE, CONSTRAINT fk_refresh_tokens_tenant FOREIGN KEY (tenant_id) REFERENCES identity.tenants(id) ON DELETE CASCADE ); -- Indexes for performance CREATE INDEX idx_refresh_tokens_user_id ON identity.refresh_tokens(user_id); CREATE INDEX idx_refresh_tokens_tenant_id ON identity.refresh_tokens(tenant_id); CREATE INDEX idx_refresh_tokens_token_hash ON identity.refresh_tokens(token_hash); CREATE INDEX idx_refresh_tokens_expires_at ON identity.refresh_tokens(expires_at); CREATE INDEX idx_refresh_tokens_token_family ON identity.refresh_tokens(token_family); -- Cleanup expired tokens (scheduled job) CREATE INDEX idx_refresh_tokens_cleanup ON identity.refresh_tokens(expires_at, revoked_at) WHERE revoked_at IS NULL; ``` #### 1.2.4 Domain Model **RefreshToken Entity** (`Domain/Aggregates/Users/RefreshToken.cs`): ```csharp public sealed class RefreshToken : Entity { public string TokenHash { get; private set; } = null!; public UserId UserId { get; private set; } = null!; public TenantId TenantId { get; private set; } = null!; // Token lifecycle public DateTime ExpiresAt { get; private set; } public DateTime CreatedAt { get; private set; } public DateTime? RevokedAt { get; private set; } public string? RevokedReason { get; private set; } // Security tracking public string? IpAddress { get; private set; } public string? UserAgent { get; private set; } public DateTime? LastUsedAt { get; private set; } // Token rotation public Guid TokenFamily { get; private set; } public Guid? ReplacedByTokenId { get; private set; } // Factory method public static RefreshToken Create( UserId userId, TenantId tenantId, string tokenHash, DateTime expiresAt, Guid tokenFamily, string? ipAddress = null, string? userAgent = null) { return new RefreshToken { Id = Guid.NewGuid(), TokenHash = tokenHash, UserId = userId, TenantId = tenantId, ExpiresAt = expiresAt, CreatedAt = DateTime.UtcNow, TokenFamily = tokenFamily, IpAddress = ipAddress, UserAgent = userAgent }; } // Business methods public void MarkAsUsed() { LastUsedAt = DateTime.UtcNow; } public void Revoke(string reason) { if (RevokedAt.HasValue) throw new InvalidOperationException("Token already revoked"); RevokedAt = DateTime.UtcNow; RevokedReason = reason; } public void MarkAsReplaced(Guid newTokenId) { ReplacedByTokenId = newTokenId; RevokedAt = DateTime.UtcNow; RevokedReason = "Rotated"; } public bool IsValid() { return !RevokedAt.HasValue && DateTime.UtcNow < ExpiresAt; } public bool IsExpired() { return DateTime.UtcNow >= ExpiresAt; } } ``` #### 1.2.5 Application Layer Design **Interface**: `Application/Services/IRefreshTokenService.cs` ```csharp public interface IRefreshTokenService { Task GenerateRefreshTokenAsync( User user, string? ipAddress = null, string? userAgent = null, CancellationToken cancellationToken = default); Task<(string AccessToken, RefreshToken NewRefreshToken)> RotateRefreshTokenAsync( string refreshToken, string? ipAddress = null, string? userAgent = null, CancellationToken cancellationToken = default); Task RevokeTokenAsync( string refreshToken, string reason, CancellationToken cancellationToken = default); Task RevokeAllUserTokensAsync( Guid userId, string reason, CancellationToken cancellationToken = default); } ``` **Implementation**: `Infrastructure/Services/RefreshTokenService.cs` ```csharp public class RefreshTokenService : IRefreshTokenService { private readonly IUserRepository _userRepository; private readonly IRefreshTokenRepository _refreshTokenRepository; private readonly IJwtService _jwtService; private readonly IConfiguration _configuration; private readonly ILogger _logger; public async Task GenerateRefreshTokenAsync( User user, string? ipAddress, string? userAgent, CancellationToken cancellationToken) { // Generate cryptographically secure token var tokenBytes = new byte[64]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(tokenBytes); var token = Convert.ToBase64String(tokenBytes); // Hash token before storage (never store plain text) var tokenHash = ComputeSha256Hash(token); // Create refresh token var expirationDays = _configuration.GetValue("Jwt:RefreshTokenExpirationDays", 7); var tokenFamily = Guid.NewGuid(); // New token family var refreshToken = RefreshToken.Create( userId: UserId.From(user.Id), tenantId: user.TenantId, tokenHash: tokenHash, expiresAt: DateTime.UtcNow.AddDays(expirationDays), tokenFamily: tokenFamily, ipAddress: ipAddress, userAgent: userAgent ); await _refreshTokenRepository.AddAsync(refreshToken, cancellationToken); _logger.LogInformation( "Generated refresh token for user {UserId}, expires at {ExpiresAt}", user.Id, refreshToken.ExpiresAt); // Return token with plain text (only time we return plain text) refreshToken.PlainTextToken = token; // Add transient property return refreshToken; } public async Task<(string AccessToken, RefreshToken NewRefreshToken)> RotateRefreshTokenAsync( string refreshToken, string? ipAddress, string? userAgent, CancellationToken cancellationToken) { var tokenHash = ComputeSha256Hash(refreshToken); // Find existing token var existingToken = await _refreshTokenRepository .GetByTokenHashAsync(tokenHash, cancellationToken); if (existingToken == null) { _logger.LogWarning("Refresh token not found: {TokenHash}", tokenHash); throw new UnauthorizedAccessException("Invalid refresh token"); } // Check if token is valid if (!existingToken.IsValid()) { _logger.LogWarning( "Invalid refresh token used by user {UserId}, token family {TokenFamily}", existingToken.UserId, existingToken.TokenFamily); // SECURITY: Revoke entire token family (possible token theft) await RevokeTokenFamilyAsync(existingToken.TokenFamily, "Security: Reuse detected", cancellationToken); throw new UnauthorizedAccessException("Token invalid or revoked"); } // Get user and tenant var user = await _userRepository.GetByIdAsync(existingToken.UserId.Value, cancellationToken); if (user == null || user.Status != UserStatus.Active) { throw new UnauthorizedAccessException("User not found or inactive"); } var tenant = await _tenantRepository.GetByIdAsync(existingToken.TenantId.Value, cancellationToken); if (tenant == null || tenant.Status != TenantStatus.Active) { throw new UnauthorizedAccessException("Tenant not found or inactive"); } // Generate new tokens var newAccessToken = _jwtService.GenerateToken(user, tenant); var newRefreshToken = await GenerateRefreshTokenForRotationAsync( user, existingToken.TokenFamily, ipAddress, userAgent, cancellationToken); // Mark old token as replaced existingToken.MarkAsReplaced(newRefreshToken.Id); await _refreshTokenRepository.UpdateAsync(existingToken, cancellationToken); _logger.LogInformation( "Rotated refresh token for user {UserId}, old token: {OldTokenId}, new token: {NewTokenId}", user.Id, existingToken.Id, newRefreshToken.Id); return (newAccessToken, newRefreshToken); } private async Task GenerateRefreshTokenForRotationAsync( User user, Guid tokenFamily, string? ipAddress, string? userAgent, CancellationToken cancellationToken) { // Same as GenerateRefreshTokenAsync but reuses token family var tokenBytes = new byte[64]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(tokenBytes); var token = Convert.ToBase64String(tokenBytes); var tokenHash = ComputeSha256Hash(token); var expirationDays = _configuration.GetValue("Jwt:RefreshTokenExpirationDays", 7); var refreshToken = RefreshToken.Create( userId: UserId.From(user.Id), tenantId: user.TenantId, tokenHash: tokenHash, expiresAt: DateTime.UtcNow.AddDays(expirationDays), tokenFamily: tokenFamily, // Reuse token family ipAddress: ipAddress, userAgent: userAgent ); await _refreshTokenRepository.AddAsync(refreshToken, cancellationToken); refreshToken.PlainTextToken = token; return refreshToken; } private async Task RevokeTokenFamilyAsync( Guid tokenFamily, string reason, CancellationToken cancellationToken) { var tokens = await _refreshTokenRepository .GetByTokenFamilyAsync(tokenFamily, cancellationToken); foreach (var token in tokens.Where(t => !t.RevokedAt.HasValue)) { token.Revoke(reason); } await _refreshTokenRepository.UpdateRangeAsync(tokens, cancellationToken); _logger.LogWarning( "Revoked entire token family {TokenFamily}, reason: {Reason}", tokenFamily, reason); } private static string ComputeSha256Hash(string input) { using var sha256 = SHA256.Create(); var bytes = Encoding.UTF8.GetBytes(input); var hash = sha256.ComputeHash(bytes); return Convert.ToBase64String(hash); } } ``` #### 1.2.6 API Endpoints **New endpoint**: `POST /api/auth/refresh` ```csharp [HttpPost("refresh")] [AllowAnonymous] public async Task> RefreshToken( [FromBody] RefreshTokenRequest request) { try { var ipAddress = HttpContext.Connection.RemoteIpAddress?.ToString(); var userAgent = HttpContext.Request.Headers["User-Agent"].ToString(); var (accessToken, newRefreshToken) = await _refreshTokenService .RotateRefreshTokenAsync( request.RefreshToken, ipAddress, userAgent, HttpContext.RequestAborted); return Ok(new LoginResponseDto { AccessToken = accessToken, RefreshToken = newRefreshToken.PlainTextToken, ExpiresIn = 3600, // 60 minutes TokenType = "Bearer" }); } catch (UnauthorizedAccessException ex) { _logger.LogWarning(ex, "Refresh token failed"); return Unauthorized(new { message = "Invalid or expired refresh token" }); } } [HttpPost("logout")] [Authorize] public async Task Logout([FromBody] LogoutRequest request) { try { await _refreshTokenService.RevokeTokenAsync( request.RefreshToken, "User logout", HttpContext.RequestAborted); return Ok(new { message = "Logged out successfully" }); } catch (Exception ex) { _logger.LogError(ex, "Logout failed"); return BadRequest(new { message = "Logout failed" }); } } [HttpPost("logout-all")] [Authorize] public async Task LogoutAllDevices() { var userId = Guid.Parse(User.FindFirstValue("user_id")!); await _refreshTokenService.RevokeAllUserTokensAsync( userId, "User requested logout from all devices", HttpContext.RequestAborted); return Ok(new { message = "Logged out from all devices" }); } ``` #### 1.2.7 Security Mechanisms **1. Token Rotation Strategy**: - Each refresh token can only be used once - Using a refresh token generates a new access token AND a new refresh token - Old refresh token is immediately invalidated **2. Token Family Tracking**: - All rotated tokens belong to the same "family" - If any token in a family is reused, entire family is revoked - Detects token theft and replay attacks **3. Token Storage Security**: - Never store plain text tokens in database - Store SHA-256 hash of tokens - Plain text tokens only returned to client once **4. Additional Security**: - IP address and User-Agent tracking - Last used timestamp tracking - Automatic cleanup of expired tokens (scheduled job) #### 1.2.8 Configuration **appsettings.Development.json**: ```json { "Jwt": { "SecretKey": "your-super-secret-key-min-32-characters-long-12345", "Issuer": "ColaFlow.API", "Audience": "ColaFlow.Web", "ExpirationMinutes": "60", "RefreshTokenExpirationDays": "7", "RefreshTokenCleanupDays": "30" } } ``` **appsettings.Production.json**: ```json { "Jwt": { "SecretKey": "${JWT_SECRET_KEY}", // Environment variable "Issuer": "ColaFlow.API", "Audience": "ColaFlow.Web", "ExpirationMinutes": "30", // Shorter for production "RefreshTokenExpirationDays": "7", "RefreshTokenCleanupDays": "30" } } ``` --- ## 2. Role-Based Authorization (RBAC) ### 2.1 Background & Goals **Problem**: Current system has no role differentiation. All authenticated users have same permissions. **Goals**: - Implement hierarchical role system - Support tenant-level and project-level permissions - Prepare for future MCP Server permission integration - Enable fine-grained access control ### 2.2 Architecture Design #### 2.2.1 Role Hierarchy ``` Enterprise Architecture: ┌─────────────────────────────────────────────────────┐ │ System Admin │ │ (Internal ColaFlow admin - not tenant-specific) │ └─────────────────────────────────────────────────────┘ │ ┌───────────────┴───────────────┐ │ │ ┌───────▼──────────┐ ┌────────▼─────────┐ │ Tenant Owner │ │ Tenant Admin │ │ (Full control) │ │ (Manage users) │ └───────┬──────────┘ └────────┬─────────┘ │ │ └───────────────┬───────────────┘ │ ┌───────────────┴───────────────┐ │ │ ┌───────▼──────────┐ ┌────────▼─────────┐ │ Project Manager │ │ Project Member │ │ (Manage project)│ │ (View/Edit) │ └───────┬──────────┘ └────────┬─────────┘ │ │ └───────────────┬───────────────┘ │ ┌───────▼────────┐ │ Project Guest │ │ (View only) │ └────────────────┘ ``` #### 2.2.2 Permission Model **Two-Level Permission System**: 1. **Tenant-Level Roles** (applies to entire tenant): - TenantOwner - TenantAdmin - TenantMember (default) - TenantGuest (read-only) 2. **Project-Level Roles** (applies to specific projects): - ProjectOwner - ProjectManager - ProjectMember - ProjectGuest **Permission Matrix**: | Action | Tenant Owner | Tenant Admin | Tenant Member | Tenant Guest | |--------|-------------|--------------|---------------|--------------| | Manage Tenant Settings | ✅ | ❌ | ❌ | ❌ | | Manage Billing | ✅ | ❌ | ❌ | ❌ | | Invite/Remove Users | ✅ | ✅ | ❌ | ❌ | | Create Projects | ✅ | ✅ | ✅ | ❌ | | View All Projects | ✅ | ✅ | Assigned Only | Assigned Only | | Delete Projects | ✅ | ✅ | ❌ | ❌ | #### 2.2.3 Database Schema Design ```sql -- Tenant roles (user's role within a tenant) CREATE TABLE identity.user_tenant_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, tenant_id UUID NOT NULL, role VARCHAR(50) NOT NULL, -- TenantOwner, TenantAdmin, TenantMember, TenantGuest assigned_at TIMESTAMP NOT NULL DEFAULT NOW(), assigned_by_user_id UUID NULL, CONSTRAINT fk_user_tenant_roles_user FOREIGN KEY (user_id) REFERENCES identity.users(id) ON DELETE CASCADE, CONSTRAINT fk_user_tenant_roles_tenant FOREIGN KEY (tenant_id) REFERENCES identity.tenants(id) ON DELETE CASCADE, CONSTRAINT fk_user_tenant_roles_assigned_by FOREIGN KEY (assigned_by_user_id) REFERENCES identity.users(id) ON DELETE SET NULL, -- One role per user per tenant CONSTRAINT uq_user_tenant_role UNIQUE (user_id, tenant_id) ); CREATE INDEX idx_user_tenant_roles_user_id ON identity.user_tenant_roles(user_id); CREATE INDEX idx_user_tenant_roles_tenant_id ON identity.user_tenant_roles(tenant_id); -- Project roles (will be in Projects module, shown here for reference) -- This table will be created when Projects module is implemented -- CREATE TABLE projects.user_project_roles ( -- id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- user_id UUID NOT NULL, -- project_id UUID NOT NULL, -- role VARCHAR(50) NOT NULL, -- ProjectOwner, ProjectManager, ProjectMember, ProjectGuest -- assigned_at TIMESTAMP NOT NULL DEFAULT NOW(), -- assigned_by_user_id UUID NULL -- ); ``` #### 2.2.4 Domain Model **TenantRole Enum** (`Domain/Aggregates/Users/TenantRole.cs`): ```csharp public enum TenantRole { TenantOwner = 1, // Full control TenantAdmin = 2, // User management TenantMember = 3, // Default role TenantGuest = 4 // Read-only } ``` **UserTenantRole Entity** (`Domain/Aggregates/Users/UserTenantRole.cs`): ```csharp public sealed class UserTenantRole : Entity { public UserId UserId { get; private set; } = null!; public TenantId TenantId { get; private set; } = null!; public TenantRole Role { get; private set; } public DateTime AssignedAt { get; private set; } public Guid? AssignedByUserId { get; private set; } private UserTenantRole() : base() { } public static UserTenantRole Create( UserId userId, TenantId tenantId, TenantRole role, Guid? assignedByUserId = null) { return new UserTenantRole { Id = Guid.NewGuid(), UserId = userId, TenantId = tenantId, Role = role, AssignedAt = DateTime.UtcNow, AssignedByUserId = assignedByUserId }; } public void UpdateRole(TenantRole newRole, Guid updatedByUserId) { if (Role == newRole) return; Role = newRole; AssignedByUserId = updatedByUserId; // Note: AssignedAt intentionally not updated to preserve original assignment date } } ``` **Update User Entity** (`Domain/Aggregates/Users/User.cs`): ```csharp // Add to User entity public TenantRole GetTenantRole() { // This will be loaded from UserTenantRole entity // For now, return default return TenantRole.TenantMember; } ``` #### 2.2.5 Authorization Implementation **Policy-Based Authorization** (`Program.cs`): ```csharp // Add authorization policies builder.Services.AddAuthorization(options => { // Tenant-level policies options.AddPolicy("RequireTenantOwner", policy => policy.RequireClaim("tenant_role", "TenantOwner")); options.AddPolicy("RequireTenantAdmin", policy => policy.RequireAssertion(context => context.User.HasClaim(c => c.Type == "tenant_role" && (c.Value == "TenantOwner" || c.Value == "TenantAdmin")))); options.AddPolicy("RequireTenantMember", policy => policy.RequireAssertion(context => context.User.HasClaim(c => c.Type == "tenant_role" && (c.Value == "TenantOwner" || c.Value == "TenantAdmin" || c.Value == "TenantMember")))); // Future: Project-level policies options.AddPolicy("RequireProjectOwner", policy => policy.RequireClaim("project_role", "ProjectOwner")); }); ``` **Update JWT Claims** (`Infrastructure/Services/JwtService.cs`): ```csharp public string GenerateToken(User user, Tenant tenant, TenantRole tenantRole) { var securityKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(_configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey not configured"))); var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); var claims = new List { new(JwtRegisteredClaimNames.Sub, user.Id.ToString()), new(JwtRegisteredClaimNames.Email, user.Email.Value), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new("user_id", user.Id.ToString()), new("tenant_id", tenant.Id.ToString()), new("tenant_slug", tenant.Slug.Value), new("tenant_plan", tenant.Plan.ToString()), new("full_name", user.FullName.Value), new("auth_provider", user.AuthProvider.ToString()), // NEW: Tenant-level role new("tenant_role", tenantRole.ToString()), new(ClaimTypes.Role, tenantRole.ToString()) // Standard claim for [Authorize(Roles = "...")] }; var token = new JwtSecurityToken( issuer: _configuration["Jwt:Issuer"], audience: _configuration["Jwt:Audience"], claims: claims, expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(_configuration["Jwt:ExpirationMinutes"] ?? "60")), signingCredentials: credentials ); return new JwtSecurityTokenHandler().WriteToken(token); } ``` #### 2.2.6 Authorization Attributes **Custom Authorization Attribute** (`API/Authorization/RequireTenantRoleAttribute.cs`): ```csharp [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public class RequireTenantRoleAttribute : AuthorizeAttribute { public RequireTenantRoleAttribute(params TenantRole[] roles) { Roles = string.Join(",", roles.Select(r => r.ToString())); } } ``` **Usage Examples**: ```csharp // Controller-level authorization [ApiController] [Route("api/tenants")] [RequireTenantRole(TenantRole.TenantAdmin, TenantRole.TenantOwner)] public class TenantManagementController : ControllerBase { // All actions require TenantAdmin or TenantOwner } // Action-level authorization [HttpDelete("{userId}")] [RequireTenantRole(TenantRole.TenantOwner)] public async Task DeleteUser(Guid userId) { // Only TenantOwner can delete users } // Fine-grained authorization [HttpPost("projects")] [Authorize] // Any authenticated user public async Task CreateProject([FromBody] CreateProjectCommand command) { // Check role in code for complex logic var tenantRole = User.FindFirstValue("tenant_role"); if (tenantRole == "TenantGuest") { return Forbid("Guests cannot create projects"); } // Continue with project creation } ``` #### 2.2.7 Repository Pattern **IUserTenantRoleRepository** (`Domain/Repositories/IUserTenantRoleRepository.cs`): ```csharp public interface IUserTenantRoleRepository { Task GetByUserAndTenantAsync( Guid userId, Guid tenantId, CancellationToken cancellationToken = default); Task> GetByTenantAsync( Guid tenantId, CancellationToken cancellationToken = default); Task> GetByUserAsync( Guid userId, CancellationToken cancellationToken = default); Task AddAsync(UserTenantRole role, CancellationToken cancellationToken = default); Task UpdateAsync(UserTenantRole role, CancellationToken cancellationToken = default); Task DeleteAsync(UserTenantRole role, CancellationToken cancellationToken = default); } ``` #### 2.2.8 Command Handlers Update **Update RegisterTenantCommandHandler** to assign TenantOwner role: ```csharp public async Task Handle(RegisterTenantCommand request, CancellationToken cancellationToken) { // ... existing validation ... // Create tenant var tenant = Tenant.Create(tenantName, tenantSlug, subscriptionPlan); await _tenantRepository.AddAsync(tenant, cancellationToken); // Create admin user var hashedPassword = _passwordHasher.HashPassword(request.AdminPassword); var adminUser = User.CreateLocal( TenantId.From(tenant.Id), email, hashedPassword, fullName); await _userRepository.AddAsync(adminUser, cancellationToken); // NEW: Assign TenantOwner role to admin var tenantRole = UserTenantRole.Create( UserId.From(adminUser.Id), TenantId.From(tenant.Id), TenantRole.TenantOwner); await _userTenantRoleRepository.AddAsync(tenantRole, cancellationToken); // Generate JWT with role var token = _jwtService.GenerateToken(adminUser, tenant, TenantRole.TenantOwner); // ... rest of handler ... } ``` --- ## 3. Email Verification Flow ### 3.1 Background & Goals **Problem**: Users can register with any email without verification, leading to: - Invalid email addresses in system - Security risk (account takeover) - Compliance issues (GDPR) **Goals**: - Verify email ownership during registration - Support re-sending verification emails - Block unverified users from critical actions - Prepare for password reset flow ### 3.2 Architecture Design #### 3.2.1 Verification Flow Diagram ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Client │ │ API Server │ │Email Service│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ 1. Register (email) │ │ ├────────────────────────────────>│ │ │ │ │ │ │ 2. Generate token │ │ │ Save to DB │ │ │ │ │ │ 3. Send verification email│ │ ├───────────────────────────>│ │ │ │ │ 4. Success (please check email)│ │ │<────────────────────────────────┤ │ │ │ │ │ │ 5. Email delivered │ │ │<───────────────────────────┤ │ │ │ │ 6. Click verification link │ │ │ (GET /verify-email?token=XX)│ │ ├────────────────────────────────>│ │ │ │ │ │ │ 7. Validate token │ │ │ Update EmailVerifiedAt │ │ │ │ │ 8. Email verified (redirect) │ │ │<────────────────────────────────┤ │ │ │ │ ``` #### 3.2.2 Token Design **Token Structure**: - Base64-encoded GUID (URL-safe) - Expiration: 24 hours (configurable) - One-time use only - Stored as SHA-256 hash in database **Token Generation**: ```csharp public string GenerateEmailVerificationToken() { var tokenBytes = new byte[32]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(tokenBytes); return Convert.ToBase64String(tokenBytes) .Replace("+", "-") .Replace("/", "_") .TrimEnd('='); // URL-safe base64 } ``` #### 3.2.3 Database Schema (Already Exists) The `User` entity already has email verification fields: ```csharp public DateTime? EmailVerifiedAt { get; private set; } public string? EmailVerificationToken { get; private set; } ``` **Add expiration field**: ```sql ALTER TABLE identity.users ADD COLUMN email_verification_token_expires_at TIMESTAMP NULL; ``` Update `User.cs`: ```csharp public DateTime? EmailVerificationTokenExpiresAt { get; private set; } public void SetEmailVerificationToken(string token, DateTime expiresAt) { EmailVerificationToken = ComputeSha256Hash(token); // Store hash EmailVerificationTokenExpiresAt = expiresAt; UpdatedAt = DateTime.UtcNow; } public bool IsEmailVerificationTokenValid(string token) { if (EmailVerificationToken == null || EmailVerificationTokenExpiresAt == null) return false; if (DateTime.UtcNow > EmailVerificationTokenExpiresAt) return false; var tokenHash = ComputeSha256Hash(token); return EmailVerificationToken == tokenHash; } ``` #### 3.2.4 Email Service Design **Interface**: `Application/Services/IEmailService.cs` ```csharp public interface IEmailService { Task SendEmailVerificationAsync( string recipientEmail, string recipientName, string verificationToken, CancellationToken cancellationToken = default); Task SendPasswordResetAsync( string recipientEmail, string recipientName, string resetToken, CancellationToken cancellationToken = default); Task SendWelcomeEmailAsync( string recipientEmail, string recipientName, CancellationToken cancellationToken = default); } ``` **Implementation Options**: | Provider | Pros | Cons | Cost | |----------|------|------|------| | **SendGrid** | Easy setup, 100 emails/day free | Rate limits | Free/Paid | | **AWS SES** | Scalable, cheap (0.10/1000) | Complex setup | Pay-as-you-go | | **MailKit (SMTP)** | No external dependency | Requires SMTP server | Self-hosted | | **Mailgun** | Developer-friendly API | Limited free tier | Free/Paid | **Recommendation**: **SendGrid for MVP** (easy setup, generous free tier) **Implementation**: `Infrastructure/Services/SendGridEmailService.cs` ```csharp public class SendGridEmailService : IEmailService { private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly SendGridClient _client; public SendGridEmailService(IConfiguration configuration, ILogger logger) { _configuration = configuration; _logger = logger; var apiKey = _configuration["SendGrid:ApiKey"]; if (string.IsNullOrEmpty(apiKey)) throw new InvalidOperationException("SendGrid API key not configured"); _client = new SendGridClient(apiKey); } public async Task SendEmailVerificationAsync( string recipientEmail, string recipientName, string verificationToken, CancellationToken cancellationToken) { var from = new EmailAddress( _configuration["SendGrid:FromEmail"] ?? "noreply@colaflow.com", "ColaFlow"); var to = new EmailAddress(recipientEmail, recipientName); var verificationUrl = $"{_configuration["App:BaseUrl"]}/verify-email?token={verificationToken}"; var subject = "Verify your ColaFlow email address"; var plainTextContent = $"Please verify your email by clicking: {verificationUrl}"; var htmlContent = $@"

Welcome to ColaFlow!

Please verify your email address by clicking the button below:

Verify Email

Or copy and paste this link into your browser:

{verificationUrl}

This link expires in 24 hours.

"; var msg = MailHelper.CreateSingleEmail(from, to, subject, plainTextContent, htmlContent); var response = await _client.SendEmailAsync(msg, cancellationToken); if (response.StatusCode != System.Net.HttpStatusCode.OK && response.StatusCode != System.Net.HttpStatusCode.Accepted) { _logger.LogError("Failed to send verification email to {Email}, status: {Status}", recipientEmail, response.StatusCode); throw new InvalidOperationException("Failed to send verification email"); } _logger.LogInformation("Sent verification email to {Email}", recipientEmail); } } ``` #### 3.2.5 Command Handlers **New Command**: `Application/Commands/VerifyEmail/VerifyEmailCommand.cs` ```csharp public record VerifyEmailCommand(string Token) : IRequest; public class VerifyEmailCommandHandler : IRequestHandler { private readonly IUserRepository _userRepository; private readonly ILogger _logger; public async Task Handle(VerifyEmailCommand request, CancellationToken cancellationToken) { // Find user by token hash var tokenHash = ComputeSha256Hash(request.Token); var user = await _userRepository.GetByEmailVerificationTokenAsync(tokenHash, cancellationToken); if (user == null) { _logger.LogWarning("Email verification failed: token not found"); return false; } // Validate token if (!user.IsEmailVerificationTokenValid(request.Token)) { _logger.LogWarning("Email verification failed for user {UserId}: token invalid or expired", user.Id); return false; } // Verify email user.VerifyEmail(); await _userRepository.UpdateAsync(user, cancellationToken); _logger.LogInformation("Email verified for user {UserId}", user.Id); return true; } private static string ComputeSha256Hash(string input) { using var sha256 = SHA256.Create(); var bytes = Encoding.UTF8.GetBytes(input); var hash = sha256.ComputeHash(bytes); return Convert.ToBase64String(hash); } } ``` **New Command**: `Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommand.cs` ```csharp public record ResendVerificationEmailCommand(string Email, string TenantSlug) : IRequest; public class ResendVerificationEmailCommandHandler : IRequestHandler { private readonly IUserRepository _userRepository; private readonly ITenantRepository _tenantRepository; private readonly IEmailService _emailService; private readonly ILogger _logger; public async Task Handle(ResendVerificationEmailCommand request, CancellationToken cancellationToken) { // Find user var tenant = await _tenantRepository.GetBySlugAsync(request.TenantSlug, cancellationToken); if (tenant == null) return false; var user = await _userRepository.GetByEmailAsync(request.Email, tenant.Id, cancellationToken); if (user == null) return false; // Check if already verified if (user.EmailVerifiedAt.HasValue) { _logger.LogInformation("User {UserId} already verified", user.Id); return true; // Already verified, consider success } // Generate new token var token = GenerateEmailVerificationToken(); var expiresAt = DateTime.UtcNow.AddHours(24); user.SetEmailVerificationToken(token, expiresAt); await _userRepository.UpdateAsync(user, cancellationToken); // Send email await _emailService.SendEmailVerificationAsync( user.Email.Value, user.FullName.Value, token, cancellationToken); _logger.LogInformation("Resent verification email to user {UserId}", user.Id); return true; } } ``` #### 3.2.6 API Endpoints ```csharp [HttpGet("verify-email")] [AllowAnonymous] public async Task VerifyEmail([FromQuery] string token) { if (string.IsNullOrEmpty(token)) return BadRequest(new { message = "Token is required" }); var command = new VerifyEmailCommand(token); var result = await _mediator.Send(command); if (result) { // Redirect to success page return Redirect($"{_configuration["App:FrontendUrl"]}/email-verified"); } else { // Redirect to error page return Redirect($"{_configuration["App:FrontendUrl"]}/email-verification-failed"); } } [HttpPost("resend-verification")] [AllowAnonymous] public async Task ResendVerification([FromBody] ResendVerificationRequest request) { var command = new ResendVerificationEmailCommand(request.Email, request.TenantSlug); var result = await _mediator.Send(command); // Always return success to prevent email enumeration return Ok(new { message = "If the email exists, a verification link has been sent" }); } [HttpGet("me")] [Authorize] public async Task GetCurrentUser() { var userId = Guid.Parse(User.FindFirstValue("user_id")!); var user = await _userRepository.GetByIdAsync(userId); return Ok(new { userId = user.Id, email = user.Email.Value, fullName = user.FullName.Value, emailVerified = user.EmailVerifiedAt.HasValue, emailVerifiedAt = user.EmailVerifiedAt }); } ``` #### 3.2.7 Update RegisterTenant Flow **Update `RegisterTenantCommandHandler.cs`**: ```csharp public async Task Handle(RegisterTenantCommand request, CancellationToken cancellationToken) { // ... existing validation and creation ... // Create admin user var hashedPassword = _passwordHasher.HashPassword(request.AdminPassword); var adminUser = User.CreateLocal(tenantId, email, hashedPassword, fullName); // Generate email verification token var verificationToken = GenerateEmailVerificationToken(); var tokenExpiresAt = DateTime.UtcNow.AddHours(24); adminUser.SetEmailVerificationToken(verificationToken, tokenExpiresAt); await _userRepository.AddAsync(adminUser, cancellationToken); // Send verification email await _emailService.SendEmailVerificationAsync( adminUser.Email.Value, adminUser.FullName.Value, verificationToken, cancellationToken); // Generate JWT (user can login even if email not verified) var token = _jwtService.GenerateToken(adminUser, tenant, TenantRole.TenantOwner); _logger.LogInformation( "Tenant {TenantId} registered, verification email sent to {Email}", tenant.Id, adminUser.Email.Value); // ... return response ... } ``` #### 3.2.8 Configuration **appsettings.Development.json**: ```json { "SendGrid": { "ApiKey": "${SENDGRID_API_KEY}", "FromEmail": "noreply@colaflow.com", "FromName": "ColaFlow" }, "App": { "BaseUrl": "http://localhost:5167", "FrontendUrl": "http://localhost:3000" }, "EmailVerification": { "TokenExpirationHours": "24", "RequireVerification": "false" } } ``` **appsettings.Production.json**: ```json { "SendGrid": { "ApiKey": "${SENDGRID_API_KEY}", "FromEmail": "noreply@colaflow.com", "FromName": "ColaFlow" }, "App": { "BaseUrl": "https://api.colaflow.com", "FrontendUrl": "https://app.colaflow.com" }, "EmailVerification": { "TokenExpirationHours": "24", "RequireVerification": "true" } } ``` --- ## 4. Risk Assessment ### 4.1 Technical Risks | Risk | Impact | Probability | Mitigation | |------|--------|-------------|------------| | **Refresh token database performance** | Medium | Low | Add proper indexes, implement cleanup job, plan Redis migration | | **Token family revocation complexity** | Medium | Medium | Thorough testing, clear logging, transaction safety | | **Email delivery failures** | High | Medium | Implement retry mechanism, queue system (future), fallback SMTP | | **Role permission escalation** | High | Low | Comprehensive testing, audit logging, code review | | **Migration data corruption** | High | Low | Test migrations thoroughly, backup database, use transactions | ### 4.2 Security Risks | Risk | Impact | Mitigation | |------|--------|------------| | **Token theft** | High | Token rotation, family revocation, HTTPS-only, IP tracking | | **Privilege escalation** | High | Policy-based authorization, audit logs, principle of least privilege | | **Email enumeration** | Medium | Generic error messages, rate limiting | | **Token replay attacks** | High | One-time use tokens, token family tracking | | **Brute force token guessing** | Medium | Cryptographically secure tokens (64 bytes), short expiration | ### 4.3 Complexity Assessment | Feature | Complexity | Development Time | Testing Time | |---------|-----------|------------------|--------------| | **Refresh Token** | Medium | 4-6 hours | 2-3 hours | | **RBAC** | Medium-High | 6-8 hours | 3-4 hours | | **Email Verification** | Low-Medium | 3-4 hours | 2 hours | | **Total** | - | **13-18 hours** | **7-9 hours** | **Total Estimated Time**: 20-27 hours (2.5-3.5 days) --- ## 5. Implementation Roadmap ### 5.1 Phase 1: Refresh Token (Priority 1) - Day 5 Morning **Tasks**: 1. Create database migration for `refresh_tokens` table 2. Implement `RefreshToken` domain entity 3. Implement `IRefreshTokenRepository` and repository 4. Implement `IRefreshTokenService` and service 5. Update `JwtService` to support refresh token generation 6. Add `/api/auth/refresh`, `/api/auth/logout`, `/api/auth/logout-all` endpoints 7. Update `LoginCommandHandler` to return refresh token 8. Test token rotation and revocation **Files to Create**: - `Domain/Aggregates/Users/RefreshToken.cs` - `Domain/Repositories/IRefreshTokenRepository.cs` - `Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs` - `Infrastructure/Persistence/Repositories/RefreshTokenRepository.cs` - `Application/Services/IRefreshTokenService.cs` - `Infrastructure/Services/RefreshTokenService.cs` - `Infrastructure/Persistence/Migrations/XXXXXX_AddRefreshTokens.cs` **Files to Modify**: - `Application/Commands/Login/LoginCommandHandler.cs` - `API/Controllers/AuthController.cs` - `appsettings.Development.json` ### 5.2 Phase 2: RBAC (Priority 1) - Day 5 Afternoon **Tasks**: 1. Create database migration for `user_tenant_roles` table 2. Implement `TenantRole` enum and `UserTenantRole` entity 3. Implement `IUserTenantRoleRepository` and repository 4. Update `JwtService` to include role claims 5. Configure authorization policies in `Program.cs` 6. Update `RegisterTenantCommandHandler` to assign TenantOwner role 7. Update `LoginCommandHandler` to load user role 8. Test role-based authorization **Files to Create**: - `Domain/Aggregates/Users/TenantRole.cs` - `Domain/Aggregates/Users/UserTenantRole.cs` - `Domain/Repositories/IUserTenantRoleRepository.cs` - `Infrastructure/Persistence/Configurations/UserTenantRoleConfiguration.cs` - `Infrastructure/Persistence/Repositories/UserTenantRoleRepository.cs` - `API/Authorization/RequireTenantRoleAttribute.cs` - `Infrastructure/Persistence/Migrations/XXXXXX_AddUserTenantRoles.cs` **Files to Modify**: - `Infrastructure/Services/JwtService.cs` - `Application/Commands/RegisterTenant/RegisterTenantCommandHandler.cs` - `Application/Commands/Login/LoginCommandHandler.cs` - `API/Program.cs` - `API/Controllers/AuthController.cs` (add role info to `/me` endpoint) ### 5.3 Phase 3: Email Verification (Priority 2) - Day 6 Morning (Optional) **Tasks**: 1. Create database migration to add `EmailVerificationTokenExpiresAt` column 2. Update `User` entity with token validation methods 3. Implement `IEmailService` interface 4. Implement `SendGridEmailService` (or SMTP fallback) 5. Create `VerifyEmailCommand` and handler 6. Create `ResendVerificationEmailCommand` and handler 7. Update `RegisterTenantCommandHandler` to send verification email 8. Add `/api/auth/verify-email` and `/api/auth/resend-verification` endpoints 9. Test email flow end-to-end **Files to Create**: - `Application/Services/IEmailService.cs` - `Infrastructure/Services/SendGridEmailService.cs` - `Application/Commands/VerifyEmail/VerifyEmailCommand.cs` - `Application/Commands/VerifyEmail/VerifyEmailCommandHandler.cs` - `Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommand.cs` - `Application/Commands/ResendVerificationEmail/ResendVerificationEmailCommandHandler.cs` - `Infrastructure/Persistence/Migrations/XXXXXX_AddEmailVerificationExpiration.cs` **Files to Modify**: - `Domain/Aggregates/Users/User.cs` - `Application/Commands/RegisterTenant/RegisterTenantCommandHandler.cs` - `API/Controllers/AuthController.cs` - `Infrastructure/DependencyInjection.cs` - `appsettings.Development.json` ### 5.4 Testing Strategy **Unit Tests**: - `RefreshToken` entity business logic - `UserTenantRole` entity business logic - `User.VerifyEmail()` and token validation methods - `RefreshTokenService` token generation and rotation - JWT claims generation with roles **Integration Tests**: - Full refresh token flow (generate → use → rotate → revoke) - Role-based authorization (correct roles allowed, others denied) - Email verification flow (send → verify → check status) - Token family revocation on suspicious activity **Security Tests**: - Token reuse detection - Expired token rejection - Invalid role access denial - Email enumeration prevention --- ## 6. MCP Integration Considerations ### 6.1 Authentication for MCP Server When implementing MCP Server (future), the authentication system needs to support: 1. **API Key Authentication** (for AI tools): - Generate long-lived API keys per tenant - API keys inherit user's tenant role - Scoped permissions (read-only, write with approval) 2. **OAuth 2.0 for Third-Party MCP Clients**: - Authorization code flow - Scope-based permissions - Refresh token support ### 6.2 Permission Model for MCP **MCP-specific permissions** (future expansion): ```csharp public enum McpPermission { // Resource permissions ReadProjects, ReadIssues, ReadDocuments, // Tool permissions (with human approval) CreateIssue, UpdateIssueStatus, CreateDocument, LogDecision, // Admin permissions ManageIntegrations, ViewAuditLogs } ``` **RBAC → MCP Permission Mapping**: | Tenant Role | MCP Read | MCP Write | MCP Admin | |-------------|----------|-----------|-----------| | TenantOwner | ✅ | ✅ (with approval) | ✅ | | TenantAdmin | ✅ | ✅ (with approval) | ✅ | | TenantMember | ✅ | ✅ (with approval) | ❌ | | TenantGuest | ✅ | ❌ | ❌ | ### 6.3 Audit Logging for MCP Operations All MCP operations should be logged with: - User/API key identifier - Action performed - Timestamp - IP address - Approval status (if required) **Schema** (future): ```sql CREATE TABLE audit.mcp_operations ( id UUID PRIMARY KEY, user_id UUID NOT NULL, tenant_id UUID NOT NULL, operation VARCHAR(100) NOT NULL, resource_type VARCHAR(50) NOT NULL, resource_id UUID NULL, approved_by_user_id UUID NULL, approved_at TIMESTAMP NULL, created_at TIMESTAMP NOT NULL, ip_address VARCHAR(45) NULL ); ``` --- ## 7. Configuration Summary ### 7.1 Required Environment Variables **Production**: ```bash # JWT Configuration JWT_SECRET_KEY=<64-character-random-string> # SendGrid (Email) SENDGRID_API_KEY= # Database DATABASE_CONNECTION_STRING= # Application URLs APP_BASE_URL=https://api.colaflow.com APP_FRONTEND_URL=https://app.colaflow.com ``` ### 7.2 NuGet Packages Required ```xml ``` --- ## 8. Success Criteria ### 8.1 Refresh Token - [ ] Users can obtain refresh token on login - [ ] Refresh token can be used to get new access token - [ ] Refresh token rotation works correctly - [ ] Token reuse is detected and entire family is revoked - [ ] Users can logout from current device - [ ] Users can logout from all devices - [ ] Expired tokens are rejected ### 8.2 RBAC - [ ] New tenants have TenantOwner role assigned - [ ] JWT tokens contain role claims - [ ] Role-based authorization works at endpoint level - [ ] Different roles have different permissions - [ ] Unauthorized access returns 403 Forbidden - [ ] Role information visible in `/me` endpoint ### 8.3 Email Verification - [ ] Verification email sent on registration - [ ] Verification link works and marks email as verified - [ ] Expired verification links are rejected - [ ] Users can resend verification email - [ ] Email verification status visible in user profile --- ## 9. Performance Considerations ### 9.1 Database Optimization **Indexes**: - All foreign keys indexed - Token hash columns indexed (for fast lookup) - Composite index on (expires_at, revoked_at) for cleanup queries **Query Performance**: - Refresh token lookup: < 10ms (indexed) - Role lookup: < 5ms (indexed) - User verification: < 15ms (indexed) ### 9.2 Caching Strategy (Future) **Redis caching candidates**: - User roles (cache for 5 minutes) - Refresh token validity (cache for token lifetime) - Email verification status (cache for 1 hour) --- ## 10. Rollback Plan ### 10.1 Database Rollback All migrations must have `Down()` methods: ```csharp protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "refresh_tokens", schema: "identity"); migrationBuilder.DropTable( name: "user_tenant_roles", schema: "identity"); migrationBuilder.DropColumn( name: "email_verification_token_expires_at", schema: "identity", table: "users"); } ``` ### 10.2 Feature Flags Consider adding feature flags for gradual rollout: ```json { "Features": { "RefreshToken": true, "RoleBasedAuthorization": true, "EmailVerification": false } } ``` --- ## 11. Documentation Requirements **API Documentation** (Swagger/OpenAPI): - Document all new endpoints - Include request/response examples - Document error codes **Developer Documentation**: - How to configure SendGrid - How to test authentication flow locally - How to add new roles **Security Documentation**: - Token rotation mechanism - Role hierarchy - Permission model --- ## Conclusion This architecture design provides a comprehensive, secure, and scalable foundation for Day 5 development. The design prioritizes: 1. **Security**: Token rotation, hash storage, audit logging 2. **Scalability**: PostgreSQL for MVP with clear Redis migration path 3. **Extensibility**: RBAC system ready for MCP integration 4. **Maintainability**: Clean architecture, clear separation of concerns **Recommended Implementation Order**: 1. Refresh Token (4-6 hours) - Critical for user experience 2. RBAC (6-8 hours) - Foundation for all future authorization 3. Email Verification (3-4 hours) - Important for security and compliance **Total Estimated Time**: 20-27 hours (2.5-3.5 days of focused development) The architecture is production-ready with appropriate configuration changes and aligns with the ColaFlow vision of secure, AI-powered project management. --- **Next Steps**: 1. Review and approve architecture design 2. Set up development environment (SendGrid account, test database) 3. Begin implementation starting with Refresh Token 4. Execute comprehensive testing after each phase 5. Update Day 5 documentation with actual implementation details --- **Document Version**: 1.0 **Last Updated**: 2025-11-03 **Status**: Ready for Implementation