Files
ColaFlow/colaflow-api/DAY5-ARCHITECTURE-DESIGN.md
Yaojia Wang 9e2edb2965 feat(backend): Implement Refresh Token mechanism (Day 5 Phase 1)
Implemented secure refresh token rotation with the following features:
- RefreshToken domain entity with IsExpired(), IsRevoked(), IsActive(), Revoke() methods
- IRefreshTokenService with token generation, rotation, and revocation
- RefreshTokenService with SHA-256 hashing and token family tracking
- RefreshTokenRepository for database operations
- Database migration for refresh_tokens table with proper indexes
- Updated LoginCommandHandler and RegisterTenantCommandHandler to return refresh tokens
- Added POST /api/auth/refresh endpoint (token rotation)
- Added POST /api/auth/logout endpoint (revoke single token)
- Added POST /api/auth/logout-all endpoint (revoke all user tokens)
- Updated JWT access token expiration to 15 minutes (from 60)
- Refresh token expiration set to 7 days
- Security features: token reuse detection, IP address tracking, user-agent logging

Changes:
- Domain: RefreshToken.cs, IRefreshTokenRepository.cs
- Application: IRefreshTokenService.cs, updated LoginResponseDto and RegisterTenantResult
- Infrastructure: RefreshTokenService.cs, RefreshTokenRepository.cs, RefreshTokenConfiguration.cs
- API: AuthController.cs (3 new endpoints), RefreshTokenRequest.cs, LogoutRequest.cs
- Configuration: appsettings.Development.json (updated JWT settings)
- DI: DependencyInjection.cs (registered new services)
- Migration: AddRefreshTokens migration

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:44:36 +01:00

58 KiB

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.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

-- 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):

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

public interface IRefreshTokenService
{
    Task<RefreshToken> 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

public class RefreshTokenService : IRefreshTokenService
{
    private readonly IUserRepository _userRepository;
    private readonly IRefreshTokenRepository _refreshTokenRepository;
    private readonly IJwtService _jwtService;
    private readonly IConfiguration _configuration;
    private readonly ILogger<RefreshTokenService> _logger;

    public async Task<RefreshToken> 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<int>("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<RefreshToken> 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<int>("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

[HttpPost("refresh")]
[AllowAnonymous]
public async Task<ActionResult<LoginResponseDto>> 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<IActionResult> 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<IActionResult> 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:

{
  "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:

{
  "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

-- 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):

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):

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):

// 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):

// 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):

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<Claim>
    {
        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):

[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:

// 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<IActionResult> DeleteUser(Guid userId)
{
    // Only TenantOwner can delete users
}

// Fine-grained authorization
[HttpPost("projects")]
[Authorize]  // Any authenticated user
public async Task<IActionResult> 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):

public interface IUserTenantRoleRepository
{
    Task<UserTenantRole?> GetByUserAndTenantAsync(
        Guid userId,
        Guid tenantId,
        CancellationToken cancellationToken = default);

    Task<IReadOnlyList<UserTenantRole>> GetByTenantAsync(
        Guid tenantId,
        CancellationToken cancellationToken = default);

    Task<IReadOnlyList<UserTenantRole>> 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:

public async Task<TenantDto> 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:

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:

public DateTime? EmailVerifiedAt { get; private set; }
public string? EmailVerificationToken { get; private set; }

Add expiration field:

ALTER TABLE identity.users
ADD COLUMN email_verification_token_expires_at TIMESTAMP NULL;

Update User.cs:

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

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

public class SendGridEmailService : IEmailService
{
    private readonly IConfiguration _configuration;
    private readonly ILogger<SendGridEmailService> _logger;
    private readonly SendGridClient _client;

    public SendGridEmailService(IConfiguration configuration, ILogger<SendGridEmailService> 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 = $@"
            <h2>Welcome to ColaFlow!</h2>
            <p>Please verify your email address by clicking the button below:</p>
            <p><a href=""{verificationUrl}"" style=""background-color: #4CAF50; color: white; padding: 14px 20px; text-decoration: none; border-radius: 4px;"">Verify Email</a></p>
            <p>Or copy and paste this link into your browser:</p>
            <p>{verificationUrl}</p>
            <p>This link expires in 24 hours.</p>
        ";

        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

public record VerifyEmailCommand(string Token) : IRequest<bool>;

public class VerifyEmailCommandHandler : IRequestHandler<VerifyEmailCommand, bool>
{
    private readonly IUserRepository _userRepository;
    private readonly ILogger<VerifyEmailCommandHandler> _logger;

    public async Task<bool> 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

public record ResendVerificationEmailCommand(string Email, string TenantSlug) : IRequest<bool>;

public class ResendVerificationEmailCommandHandler : IRequestHandler<ResendVerificationEmailCommand, bool>
{
    private readonly IUserRepository _userRepository;
    private readonly ITenantRepository _tenantRepository;
    private readonly IEmailService _emailService;
    private readonly ILogger<ResendVerificationEmailCommandHandler> _logger;

    public async Task<bool> 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

[HttpGet("verify-email")]
[AllowAnonymous]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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:

public async Task<TenantDto> 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:

{
  "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:

{
  "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):

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):

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:

# JWT Configuration
JWT_SECRET_KEY=<64-character-random-string>

# SendGrid (Email)
SENDGRID_API_KEY=<sendgrid-api-key>

# Database
DATABASE_CONNECTION_STRING=<postgresql-connection-string>

# Application URLs
APP_BASE_URL=https://api.colaflow.com
APP_FRONTEND_URL=https://app.colaflow.com

7.2 NuGet Packages Required

<!-- Identity.Infrastructure -->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.14.0" />
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />

<!-- For Email (choose one) -->
<PackageReference Include="SendGrid" Version="9.28.1" />
<!-- OR -->
<PackageReference Include="MailKit" Version="4.3.0" />

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:

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:

{
  "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