Files
ColaFlow/docs/architecture/jwt-authentication-architecture.md
Yaojia Wang fe8ad1c1f9
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
In progress
2025-11-03 11:51:02 +01:00

87 KiB

JWT Authentication System Architecture Design

Version: 1.0 Author: ColaFlow Architecture Team Date: 2025-11-03 Status: Design Phase (M1 Sprint 2 - Task 1.1)


1. Executive Summary

This document defines the complete JWT authentication system architecture for ColaFlow, addressing the Critical security risk of unprotected API endpoints. The design follows Clean Architecture, Domain-Driven Design (DDD), and CQRS principles, ensuring separation of concerns, maintainability, and security.

Key Objectives:

  • Protect all 23+ API endpoints with JWT authentication
  • Implement secure user registration and login
  • Support Access Token + Refresh Token pattern
  • Integrate with existing Clean Architecture layers
  • Enable frontend (Next.js 16) seamless authentication
  • Maintain OWASP security standards

2. Architecture Overview

2.1 System Context

┌─────────────────────────────────────────────────────────────┐
│                    Frontend Layer                            │
│  Next.js 16 App Router + React 19 + Zustand                 │
│  - Login/Register Pages                                      │
│  - Protected Routes (Middleware)                             │
│  - Token Management (httpOnly cookies)                       │
│  - API Interceptor (TanStack Query)                          │
└──────────────────────┬──────────────────────────────────────┘
                       │ HTTPS + JWT Bearer Token
                       │
┌──────────────────────┴──────────────────────────────────────┐
│                    API Layer                                 │
│  ColaFlow.API (ASP.NET Core 9)                              │
│  - Authentication Middleware [JwtBearer]                     │
│  - Authorization Policies [Authorize]                        │
│  - AuthController (Register/Login/Refresh/Logout)           │
│  - Protected Controllers (Projects, Epics, etc.)            │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────┴──────────────────────────────────────┐
│              Application Layer (CQRS)                        │
│  ColaFlow.Application + Identity.Application Module         │
│  - Commands: RegisterUserCommand, LoginCommand, etc.        │
│  - Queries: GetCurrentUserQuery, ValidateTokenQuery         │
│  - Command/Query Handlers (MediatR)                         │
│  - DTOs: UserDto, LoginResponseDto, etc.                    │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────┴──────────────────────────────────────┐
│                  Domain Layer (DDD)                          │
│  ColaFlow.Domain + Identity.Domain Module                   │
│  - User (AggregateRoot)                                     │
│  - RefreshToken (Entity)                                    │
│  - Value Objects: Email, PasswordHash, Role                 │
│  - Domain Events: UserRegisteredEvent, UserLoggedInEvent    │
│  - Interfaces: IUserRepository, IJwtService                 │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────┴──────────────────────────────────────┐
│              Infrastructure Layer                            │
│  ColaFlow.Infrastructure + Identity.Infrastructure          │
│  - UserRepository (EF Core)                                 │
│  - JwtService (Token generation/validation)                 │
│  - PasswordHasher (BCrypt/Argon2)                           │
│  - IdentityDbContext (EF Core)                              │
│  - Migrations                                               │
└──────────────────────┬──────────────────────────────────────┘
                       │
┌──────────────────────┴──────────────────────────────────────┐
│                  Data Layer                                  │
│  PostgreSQL 16                                              │
│  - users (id, email, password_hash, role, created_at, ...)  │
│  - refresh_tokens (id, user_id, token, expires_at, ...)     │
└─────────────────────────────────────────────────────────────┘

2.2 Module Structure

Following ColaFlow's modular architecture pattern, authentication will be implemented as an Identity Module:

ColaFlow.Modules.Identity/
├── ColaFlow.Modules.Identity.Domain/
│   ├── Aggregates/
│   │   ├── UserAggregate/
│   │   │   ├── User.cs (AggregateRoot)
│   │   │   └── RefreshToken.cs (Entity)
│   ├── ValueObjects/
│   │   ├── UserId.cs
│   │   ├── Email.cs
│   │   ├── PasswordHash.cs
│   │   └── Role.cs
│   ├── Events/
│   │   ├── UserRegisteredEvent.cs
│   │   ├── UserLoggedInEvent.cs
│   │   └── UserPasswordChangedEvent.cs
│   ├── Repositories/
│   │   ├── IUserRepository.cs
│   │   └── IUnitOfWork.cs
│   └── Exceptions/
│       ├── InvalidCredentialsException.cs
│       ├── UserAlreadyExistsException.cs
│       └── RefreshTokenExpiredException.cs
│
├── ColaFlow.Modules.Identity.Application/
│   ├── Commands/
│   │   ├── RegisterUser/
│   │   │   ├── RegisterUserCommand.cs
│   │   │   ├── RegisterUserCommandHandler.cs
│   │   │   └── RegisterUserCommandValidator.cs
│   │   ├── Login/
│   │   │   ├── LoginCommand.cs
│   │   │   ├── LoginCommandHandler.cs
│   │   │   └── LoginCommandValidator.cs
│   │   ├── RefreshToken/
│   │   │   ├── RefreshTokenCommand.cs
│   │   │   └── RefreshTokenCommandHandler.cs
│   │   ├── Logout/
│   │   │   ├── LogoutCommand.cs
│   │   │   └── LogoutCommandHandler.cs
│   │   └── ChangePassword/
│   │       ├── ChangePasswordCommand.cs
│   │       ├── ChangePasswordCommandHandler.cs
│   │       └── ChangePasswordCommandValidator.cs
│   ├── Queries/
│   │   ├── GetCurrentUser/
│   │   │   ├── GetCurrentUserQuery.cs
│   │   │   └── GetCurrentUserQueryHandler.cs
│   │   └── ValidateToken/
│   │       ├── ValidateTokenQuery.cs
│   │       └── ValidateTokenQueryHandler.cs
│   ├── DTOs/
│   │   ├── UserDto.cs
│   │   ├── LoginResponseDto.cs
│   │   └── RefreshTokenResponseDto.cs
│   └── Services/
│       └── IJwtService.cs (Interface only)
│
├── ColaFlow.Modules.Identity.Infrastructure/
│   ├── Persistence/
│   │   ├── IdentityDbContext.cs
│   │   ├── Configurations/
│   │   │   ├── UserConfiguration.cs
│   │   │   └── RefreshTokenConfiguration.cs
│   │   ├── Repositories/
│   │   │   └── UserRepository.cs
│   │   ├── Migrations/
│   │   └── UnitOfWork.cs
│   └── Services/
│       ├── JwtService.cs (Implementation)
│       └── PasswordHasher.cs
│
└── IdentityModule.cs (Module registration)

3. Core Components Design

3.1 Domain Layer - User Aggregate

User Entity (Aggregate Root)

// ColaFlow.Modules.Identity.Domain/Aggregates/UserAggregate/User.cs

using ColaFlow.Shared.Kernel.Common;
using ColaFlow.Modules.Identity.Domain.ValueObjects;
using ColaFlow.Modules.Identity.Domain.Events;
using ColaFlow.Modules.Identity.Domain.Exceptions;

namespace ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;

/// <summary>
/// User Aggregate Root
/// Enforces all business rules related to user authentication and identity
/// </summary>
public class User : AggregateRoot
{
    public new UserId Id { get; private set; }
    public Email Email { get; private set; }
    public PasswordHash PasswordHash { get; private set; }
    public string FirstName { get; private set; }
    public string LastName { get; private set; }
    public Role Role { get; private set; }
    public bool IsActive { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime? UpdatedAt { get; private set; }
    public DateTime? LastLoginAt { get; private set; }

    private readonly List<RefreshToken> _refreshTokens = new();
    public IReadOnlyCollection<RefreshToken> RefreshTokens => _refreshTokens.AsReadOnly();

    // EF Core constructor
    private User()
    {
        Id = null!;
        Email = null!;
        PasswordHash = null!;
        FirstName = null!;
        LastName = null!;
        Role = null!;
    }

    /// <summary>
    /// Factory method to create a new user
    /// </summary>
    public static User Create(
        Email email,
        string plainPassword,
        string firstName,
        string lastName,
        Role role)
    {
        // Validate password strength
        ValidatePasswordStrength(plainPassword);

        var user = new User
        {
            Id = UserId.Create(),
            Email = email,
            PasswordHash = PasswordHash.Create(plainPassword),
            FirstName = firstName,
            LastName = lastName,
            Role = role,
            IsActive = true,
            CreatedAt = DateTime.UtcNow
        };

        // Raise domain event
        user.AddDomainEvent(new UserRegisteredEvent(user.Id, user.Email.Value));

        return user;
    }

    /// <summary>
    /// Verify password and record login
    /// </summary>
    public void Login(string plainPassword)
    {
        if (!IsActive)
            throw new InvalidCredentialsException("User account is deactivated");

        if (!PasswordHash.Verify(plainPassword))
            throw new InvalidCredentialsException("Invalid email or password");

        LastLoginAt = DateTime.UtcNow;
        UpdatedAt = DateTime.UtcNow;

        AddDomainEvent(new UserLoggedInEvent(Id, Email.Value, DateTime.UtcNow));
    }

    /// <summary>
    /// Create a new refresh token for this user
    /// </summary>
    public RefreshToken CreateRefreshToken(int expiryDays = 7)
    {
        if (!IsActive)
            throw new InvalidCredentialsException("User account is deactivated");

        // Revoke old tokens (keep only latest N)
        var oldTokens = _refreshTokens
            .Where(t => !t.IsRevoked)
            .OrderByDescending(t => t.CreatedAt)
            .Skip(4) // Keep max 5 active tokens per user
            .ToList();

        foreach (var token in oldTokens)
        {
            token.Revoke();
        }

        var refreshToken = RefreshToken.Create(Id, expiryDays);
        _refreshTokens.Add(refreshToken);

        return refreshToken;
    }

    /// <summary>
    /// Change user password
    /// </summary>
    public void ChangePassword(string currentPassword, string newPassword)
    {
        if (!PasswordHash.Verify(currentPassword))
            throw new InvalidCredentialsException("Current password is incorrect");

        ValidatePasswordStrength(newPassword);

        PasswordHash = PasswordHash.Create(newPassword);
        UpdatedAt = DateTime.UtcNow;

        // Revoke all refresh tokens (force re-login)
        foreach (var token in _refreshTokens.Where(t => !t.IsRevoked))
        {
            token.Revoke();
        }

        AddDomainEvent(new UserPasswordChangedEvent(Id));
    }

    /// <summary>
    /// Deactivate user account
    /// </summary>
    public void Deactivate()
    {
        if (!IsActive)
            throw new DomainException("User is already deactivated");

        IsActive = false;
        UpdatedAt = DateTime.UtcNow;

        // Revoke all refresh tokens
        foreach (var token in _refreshTokens.Where(t => !t.IsRevoked))
        {
            token.Revoke();
        }
    }

    /// <summary>
    /// Validate password strength (OWASP guidelines)
    /// </summary>
    private static void ValidatePasswordStrength(string password)
    {
        if (string.IsNullOrWhiteSpace(password))
            throw new DomainException("Password cannot be empty");

        if (password.Length < 8)
            throw new DomainException("Password must be at least 8 characters long");

        if (password.Length > 128)
            throw new DomainException("Password cannot exceed 128 characters");

        // Require at least one uppercase, one lowercase, one digit, one special char
        if (!password.Any(char.IsUpper))
            throw new DomainException("Password must contain at least one uppercase letter");

        if (!password.Any(char.IsLower))
            throw new DomainException("Password must contain at least one lowercase letter");

        if (!password.Any(char.IsDigit))
            throw new DomainException("Password must contain at least one digit");

        if (!password.Any(ch => !char.IsLetterOrDigit(ch)))
            throw new DomainException("Password must contain at least one special character");
    }
}

RefreshToken Entity

// ColaFlow.Modules.Identity.Domain/Aggregates/UserAggregate/RefreshToken.cs

using ColaFlow.Shared.Kernel.Common;
using ColaFlow.Modules.Identity.Domain.ValueObjects;
using ColaFlow.Modules.Identity.Domain.Exceptions;
using System.Security.Cryptography;

namespace ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;

/// <summary>
/// RefreshToken entity - part of User aggregate
/// Implements Refresh Token Rotation for security
/// </summary>
public class RefreshToken : Entity
{
    public new Guid Id { get; private set; }
    public UserId UserId { get; private set; }
    public string Token { get; private set; }
    public DateTime ExpiresAt { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public bool IsRevoked { get; private set; }
    public DateTime? RevokedAt { get; private set; }

    // Navigation property
    public User User { get; private set; } = null!;

    // EF Core constructor
    private RefreshToken()
    {
        Token = null!;
        UserId = null!;
    }

    /// <summary>
    /// Create a new refresh token
    /// </summary>
    public static RefreshToken Create(UserId userId, int expiryDays = 7)
    {
        return new RefreshToken
        {
            Id = Guid.NewGuid(),
            UserId = userId,
            Token = GenerateSecureToken(),
            ExpiresAt = DateTime.UtcNow.AddDays(expiryDays),
            CreatedAt = DateTime.UtcNow,
            IsRevoked = false
        };
    }

    /// <summary>
    /// Validate if token is still usable
    /// </summary>
    public void Validate()
    {
        if (IsRevoked)
            throw new RefreshTokenExpiredException("Token has been revoked");

        if (DateTime.UtcNow > ExpiresAt)
            throw new RefreshTokenExpiredException("Token has expired");
    }

    /// <summary>
    /// Revoke this token (e.g., on logout or password change)
    /// </summary>
    public void Revoke()
    {
        if (IsRevoked)
            return;

        IsRevoked = true;
        RevokedAt = DateTime.UtcNow;
    }

    /// <summary>
    /// Generate cryptographically secure random token
    /// </summary>
    private static string GenerateSecureToken()
    {
        var randomBytes = new byte[64];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomBytes);
        return Convert.ToBase64String(randomBytes);
    }
}

Value Objects

// ColaFlow.Modules.Identity.Domain/ValueObjects/Email.cs

using ColaFlow.Shared.Kernel.Common;
using System.Text.RegularExpressions;

namespace ColaFlow.Modules.Identity.Domain.ValueObjects;

public class Email : ValueObject
{
    public string Value { get; }

    private Email(string value)
    {
        Value = value;
    }

    public static Email Create(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            throw new DomainException("Email cannot be empty");

        email = email.Trim().ToLowerInvariant();

        if (!IsValidEmail(email))
            throw new DomainException("Invalid email format");

        if (email.Length > 254) // RFC 5321
            throw new DomainException("Email cannot exceed 254 characters");

        return new Email(email);
    }

    private static bool IsValidEmail(string email)
    {
        // RFC 5322 simplified pattern
        var pattern = @"^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$";
        return Regex.IsMatch(email, pattern, RegexOptions.IgnoreCase);
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }
}
// ColaFlow.Modules.Identity.Domain/ValueObjects/PasswordHash.cs

using ColaFlow.Shared.Kernel.Common;

namespace ColaFlow.Modules.Identity.Domain.ValueObjects;

/// <summary>
/// Password hash value object
/// Uses BCrypt for hashing (will be implemented in Infrastructure layer)
/// </summary>
public class PasswordHash : ValueObject
{
    public string Value { get; }

    private PasswordHash(string value)
    {
        Value = value;
    }

    /// <summary>
    /// Create password hash from plain text password
    /// </summary>
    public static PasswordHash Create(string plainPassword)
    {
        if (string.IsNullOrWhiteSpace(plainPassword))
            throw new DomainException("Password cannot be empty");

        // Hash will be generated in Infrastructure layer
        // This is just a placeholder - actual hashing happens in PasswordHasher service
        var hash = HashPassword(plainPassword);
        return new PasswordHash(hash);
    }

    /// <summary>
    /// Verify plain password against this hash
    /// </summary>
    public bool Verify(string plainPassword)
    {
        return VerifyPassword(plainPassword, Value);
    }

    // These will delegate to Infrastructure layer's PasswordHasher
    // Placeholder implementations here
    private static string HashPassword(string plainPassword)
    {
        // This will be replaced with actual BCrypt hashing in Infrastructure
        return BCrypt.Net.BCrypt.HashPassword(plainPassword, workFactor: 12);
    }

    private static bool VerifyPassword(string plainPassword, string hash)
    {
        return BCrypt.Net.BCrypt.Verify(plainPassword, hash);
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Value;
    }
}
// ColaFlow.Modules.Identity.Domain/ValueObjects/Role.cs

using ColaFlow.Shared.Kernel.Common;

namespace ColaFlow.Modules.Identity.Domain.ValueObjects;

/// <summary>
/// User role enumeration
/// </summary>
public class Role : Enumeration
{
    public static readonly Role Admin = new(1, "Admin");
    public static readonly Role User = new(2, "User");
    public static readonly Role Guest = new(3, "Guest");

    private Role(int id, string name) : base(id, name)
    {
    }

    public static Role FromName(string name)
    {
        var role = GetAll<Role>().FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
        if (role == null)
            throw new DomainException($"Invalid role: {name}");
        return role;
    }

    public static Role FromId(int id)
    {
        var role = GetAll<Role>().FirstOrDefault(r => r.Id == id);
        if (role == null)
            throw new DomainException($"Invalid role id: {id}");
        return role;
    }
}

3.2 Application Layer - Commands & Queries

RegisterUserCommand

// ColaFlow.Modules.Identity.Application/Commands/RegisterUser/RegisterUserCommand.cs

using MediatR;
using ColaFlow.Modules.Identity.Application.DTOs;

namespace ColaFlow.Modules.Identity.Application.Commands.RegisterUser;

public record RegisterUserCommand(
    string Email,
    string Password,
    string FirstName,
    string LastName
) : IRequest<UserDto>;
// ColaFlow.Modules.Identity.Application/Commands/RegisterUser/RegisterUserCommandValidator.cs

using FluentValidation;

namespace ColaFlow.Modules.Identity.Application.Commands.RegisterUser;

public class RegisterUserCommandValidator : AbstractValidator<RegisterUserCommand>
{
    public RegisterUserCommandValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty()
            .EmailAddress()
            .MaximumLength(254);

        RuleFor(x => x.Password)
            .NotEmpty()
            .MinimumLength(8)
            .MaximumLength(128)
            .Matches(@"[A-Z]").WithMessage("Password must contain at least one uppercase letter")
            .Matches(@"[a-z]").WithMessage("Password must contain at least one lowercase letter")
            .Matches(@"\d").WithMessage("Password must contain at least one digit")
            .Matches(@"[^a-zA-Z0-9]").WithMessage("Password must contain at least one special character");

        RuleFor(x => x.FirstName)
            .NotEmpty()
            .MaximumLength(100);

        RuleFor(x => x.LastName)
            .NotEmpty()
            .MaximumLength(100);
    }
}
// ColaFlow.Modules.Identity.Application/Commands/RegisterUser/RegisterUserCommandHandler.cs

using MediatR;
using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;
using ColaFlow.Modules.Identity.Domain.Repositories;
using ColaFlow.Modules.Identity.Domain.ValueObjects;
using ColaFlow.Modules.Identity.Domain.Exceptions;
using ColaFlow.Modules.Identity.Application.DTOs;

namespace ColaFlow.Modules.Identity.Application.Commands.RegisterUser;

public class RegisterUserCommandHandler : IRequestHandler<RegisterUserCommand, UserDto>
{
    private readonly IUserRepository _userRepository;
    private readonly IUnitOfWork _unitOfWork;

    public RegisterUserCommandHandler(IUserRepository userRepository, IUnitOfWork unitOfWork)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
        _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
    }

    public async Task<UserDto> Handle(RegisterUserCommand request, CancellationToken cancellationToken)
    {
        var email = Email.Create(request.Email);

        // Check if user already exists
        var existingUser = await _userRepository.GetByEmailAsync(email, cancellationToken);
        if (existingUser != null)
        {
            throw new UserAlreadyExistsException($"User with email {request.Email} already exists");
        }

        // Create new user (default role: User)
        var user = User.Create(
            email,
            request.Password,
            request.FirstName,
            request.LastName,
            Role.User
        );

        // Save to database
        await _userRepository.AddAsync(user, cancellationToken);
        await _unitOfWork.CommitAsync(cancellationToken);

        // Map to DTO
        return new UserDto
        {
            Id = user.Id.Value,
            Email = user.Email.Value,
            FirstName = user.FirstName,
            LastName = user.LastName,
            Role = user.Role.Name,
            IsActive = user.IsActive,
            CreatedAt = user.CreatedAt
        };
    }
}

LoginCommand

// ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommand.cs

using MediatR;
using ColaFlow.Modules.Identity.Application.DTOs;

namespace ColaFlow.Modules.Identity.Application.Commands.Login;

public record LoginCommand(
    string Email,
    string Password
) : IRequest<LoginResponseDto>;
// ColaFlow.Modules.Identity.Application/Commands/Login/LoginCommandHandler.cs

using MediatR;
using ColaFlow.Modules.Identity.Domain.Repositories;
using ColaFlow.Modules.Identity.Domain.ValueObjects;
using ColaFlow.Modules.Identity.Domain.Exceptions;
using ColaFlow.Modules.Identity.Application.DTOs;
using ColaFlow.Modules.Identity.Application.Services;

namespace ColaFlow.Modules.Identity.Application.Commands.Login;

public class LoginCommandHandler : IRequestHandler<LoginCommand, LoginResponseDto>
{
    private readonly IUserRepository _userRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IJwtService _jwtService;

    public LoginCommandHandler(
        IUserRepository userRepository,
        IUnitOfWork unitOfWork,
        IJwtService jwtService)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
        _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
        _jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService));
    }

    public async Task<LoginResponseDto> Handle(LoginCommand request, CancellationToken cancellationToken)
    {
        var email = Email.Create(request.Email);

        // Get user by email
        var user = await _userRepository.GetByEmailAsync(email, cancellationToken);
        if (user == null)
        {
            throw new InvalidCredentialsException("Invalid email or password");
        }

        // Verify password and record login (will throw if invalid)
        user.Login(request.Password);

        // Create refresh token
        var refreshToken = user.CreateRefreshToken(expiryDays: 7);

        // Generate JWT access token
        var accessToken = _jwtService.GenerateAccessToken(user);

        // Save changes (LastLoginAt, new refresh token)
        await _unitOfWork.CommitAsync(cancellationToken);

        return new LoginResponseDto
        {
            AccessToken = accessToken,
            RefreshToken = refreshToken.Token,
            ExpiresAt = _jwtService.GetTokenExpiration(),
            User = new UserDto
            {
                Id = user.Id.Value,
                Email = user.Email.Value,
                FirstName = user.FirstName,
                LastName = user.LastName,
                Role = user.Role.Name,
                IsActive = user.IsActive,
                CreatedAt = user.CreatedAt
            }
        };
    }
}

RefreshTokenCommand

// ColaFlow.Modules.Identity.Application/Commands/RefreshToken/RefreshTokenCommand.cs

using MediatR;
using ColaFlow.Modules.Identity.Application.DTOs;

namespace ColaFlow.Modules.Identity.Application.Commands.RefreshToken;

public record RefreshTokenCommand(string RefreshToken) : IRequest<LoginResponseDto>;
// ColaFlow.Modules.Identity.Application/Commands/RefreshToken/RefreshTokenCommandHandler.cs

using MediatR;
using ColaFlow.Modules.Identity.Domain.Repositories;
using ColaFlow.Modules.Identity.Domain.Exceptions;
using ColaFlow.Modules.Identity.Application.DTOs;
using ColaFlow.Modules.Identity.Application.Services;

namespace ColaFlow.Modules.Identity.Application.Commands.RefreshToken;

public class RefreshTokenCommandHandler : IRequestHandler<RefreshTokenCommand, LoginResponseDto>
{
    private readonly IUserRepository _userRepository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IJwtService _jwtService;

    public RefreshTokenCommandHandler(
        IUserRepository userRepository,
        IUnitOfWork unitOfWork,
        IJwtService jwtService)
    {
        _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
        _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
        _jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService));
    }

    public async Task<LoginResponseDto> Handle(RefreshTokenCommand request, CancellationToken cancellationToken)
    {
        // Find user by refresh token
        var user = await _userRepository.GetByRefreshTokenAsync(request.RefreshToken, cancellationToken);
        if (user == null)
        {
            throw new RefreshTokenExpiredException("Invalid refresh token");
        }

        // Find the specific refresh token
        var refreshTokenEntity = user.RefreshTokens
            .FirstOrDefault(rt => rt.Token == request.RefreshToken);

        if (refreshTokenEntity == null)
        {
            throw new RefreshTokenExpiredException("Invalid refresh token");
        }

        // Validate token (will throw if expired or revoked)
        refreshTokenEntity.Validate();

        // Revoke old token (Refresh Token Rotation)
        refreshTokenEntity.Revoke();

        // Create new refresh token
        var newRefreshToken = user.CreateRefreshToken(expiryDays: 7);

        // Generate new JWT access token
        var accessToken = _jwtService.GenerateAccessToken(user);

        // Save changes
        await _unitOfWork.CommitAsync(cancellationToken);

        return new LoginResponseDto
        {
            AccessToken = accessToken,
            RefreshToken = newRefreshToken.Token,
            ExpiresAt = _jwtService.GetTokenExpiration(),
            User = new UserDto
            {
                Id = user.Id.Value,
                Email = user.Email.Value,
                FirstName = user.FirstName,
                LastName = user.LastName,
                Role = user.Role.Name,
                IsActive = user.IsActive,
                CreatedAt = user.CreatedAt
            }
        };
    }
}

DTOs

// ColaFlow.Modules.Identity.Application/DTOs/UserDto.cs

namespace ColaFlow.Modules.Identity.Application.DTOs;

public class UserDto
{
    public Guid Id { get; set; }
    public string Email { get; set; } = string.Empty;
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public string Role { get; set; } = string.Empty;
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }
}
// ColaFlow.Modules.Identity.Application/DTOs/LoginResponseDto.cs

namespace ColaFlow.Modules.Identity.Application.DTOs;

public class LoginResponseDto
{
    public string AccessToken { get; set; } = string.Empty;
    public string RefreshToken { get; set; } = string.Empty;
    public DateTime ExpiresAt { get; set; }
    public UserDto User { get; set; } = null!;
}

IJwtService Interface

// ColaFlow.Modules.Identity.Application/Services/IJwtService.cs

using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;

namespace ColaFlow.Modules.Identity.Application.Services;

/// <summary>
/// JWT token generation and validation service
/// Implemented in Infrastructure layer
/// </summary>
public interface IJwtService
{
    /// <summary>
    /// Generate JWT access token for user (includes tenant context)
    /// </summary>
    string GenerateAccessToken(User user, Tenant tenant);

    /// <summary>
    /// Get token expiration time
    /// </summary>
    DateTime GetTokenExpiration();

    /// <summary>
    /// Validate JWT token and extract user ID
    /// </summary>
    Guid? ValidateToken(string token);
}

3.3 Infrastructure Layer - JWT Service Implementation

// ColaFlow.Modules.Identity.Infrastructure/Services/JwtService.cs

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
using ColaFlow.Modules.Identity.Application.Services;
using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;

namespace ColaFlow.Modules.Identity.Infrastructure.Services;

public class JwtService : IJwtService
{
    private readonly string _secretKey;
    private readonly string _issuer;
    private readonly string _audience;
    private readonly int _expiryMinutes;

    public JwtService(IConfiguration configuration)
    {
        var jwtSection = configuration.GetSection("Jwt");
        _secretKey = jwtSection["SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey not configured");
        _issuer = jwtSection["Issuer"] ?? "ColaFlow";
        _audience = jwtSection["Audience"] ?? "ColaFlow-API";
        _expiryMinutes = int.Parse(jwtSection["ExpiryMinutes"] ?? "60");

        // Validate key length (minimum 256 bits for HS256)
        if (_secretKey.Length < 32)
        {
            throw new InvalidOperationException("JWT SecretKey must be at least 32 characters (256 bits)");
        }
    }

    public string GenerateAccessToken(User user, Tenant tenant)
    {
        var claims = new[]
        {
            // Standard JWT claims
            new Claim(JwtRegisteredClaimNames.Sub, user.Id.Value.ToString()),
            new Claim(JwtRegisteredClaimNames.Email, user.Email.Value),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), // Unique token ID
            new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()), // Issued at

            // User claims
            new Claim(ClaimTypes.Role, user.Role.Name),
            new Claim("firstName", user.FirstName),
            new Claim("lastName", user.LastName),

            // Multi-tenant claims (NEW)
            new Claim("tenant_id", user.TenantId.Value.ToString()),
            new Claim("tenant_slug", tenant.Slug.Value),
            new Claim("tenant_plan", tenant.Plan.ToString()),

            // SSO claims (if applicable)
            new Claim("auth_provider", user.AuthProvider.ToString()),
            new Claim("auth_provider_id", user.ExternalUserId ?? string.Empty)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_secretKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _issuer,
            audience: _audience,
            claims: claims,
            expires: DateTime.UtcNow.AddMinutes(_expiryMinutes),
            signingCredentials: credentials
        );

        return new JwtSecurityTokenHandler().WriteToken(token);
    }

    public DateTime GetTokenExpiration()
    {
        return DateTime.UtcNow.AddMinutes(_expiryMinutes);
    }

    public Guid? ValidateToken(string token)
    {
        try
        {
            var tokenHandler = new JwtSecurityTokenHandler();
            var key = Encoding.UTF8.GetBytes(_secretKey);

            var validationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = _issuer,
                ValidAudience = _audience,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ClockSkew = TimeSpan.Zero // No tolerance for expiration
            };

            var principal = tokenHandler.ValidateToken(token, validationParameters, out _);
            var userIdClaim = principal.FindFirst(JwtRegisteredClaimNames.Sub);

            if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
            {
                return userId;
            }

            return null;
        }
        catch
        {
            return null;
        }
    }
}

3.4 Multi-Tenant JWT Claims Structure (UPDATED FOR MULTI-TENANCY)

JWT Token Payload Example

{
  // Standard JWT claims
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",  // User ID
  "email": "john.doe@acme.com",
  "jti": "f9e8d7c6-b5a4-3210-9876-543210fedcba",  // Token ID
  "iat": 1704067200,  // Issued at (Unix timestamp)
  "exp": 1704070800,  // Expires at (Unix timestamp)

  // User claims
  "role": "User",
  "firstName": "John",
  "lastName": "Doe",

  // Multi-tenant claims (NEW)
  "tenant_id": "tenant-uuid-1234-5678-9abc-def0",
  "tenant_slug": "acme",
  "tenant_plan": "Enterprise",

  // SSO claims (if applicable)
  "auth_provider": "AzureAD",
  "auth_provider_id": "azure-user-id-123",

  // JWT standard claims
  "iss": "ColaFlow",
  "aud": "ColaFlow-API"
}

Updated Login Flow with Tenant Context

// LoginCommandHandler - Updated to include tenant
public async Task<LoginResponseDto> Handle(LoginCommand request, CancellationToken cancellationToken)
{
    // 1. Resolve tenant from subdomain (via TenantResolutionMiddleware)
    var tenant = await _context.Tenants
        .IgnoreQueryFilters()
        .FirstAsync(t => t.Id == _tenantContext.CurrentTenantId, cancellationToken);

    // 2. Get user by email (within tenant scope)
    var email = Email.Create(request.Email);
    var user = await _userRepository.GetByEmailAsync(email, cancellationToken);

    if (user == null || user.TenantId != tenant.Id)
    {
        throw new InvalidCredentialsException("Invalid email or password");
    }

    // 3. Verify password and record login
    user.Login(request.Password);

    // 4. Create refresh token
    var refreshToken = user.CreateRefreshToken(expiryDays: 7);

    // 5. Generate JWT with tenant context (UPDATED)
    var accessToken = _jwtService.GenerateAccessToken(user, tenant);

    // 6. Save changes
    await _unitOfWork.CommitAsync(cancellationToken);

    return new LoginResponseDto
    {
        AccessToken = accessToken,
        RefreshToken = refreshToken.Token,
        ExpiresAt = _jwtService.GetTokenExpiration(),
        User = new UserDto
        {
            Id = user.Id.Value,
            Email = user.Email.Value,
            FirstName = user.FirstName,
            LastName = user.LastName,
            Role = user.Role.Name,
            IsActive = user.IsActive,
            CreatedAt = user.CreatedAt,
            // Multi-tenant fields (NEW)
            TenantId = user.TenantId.Value,
            TenantName = tenant.Name.Value,
            TenantSlug = tenant.Slug.Value
        }
    };
}

Updated UserDto

// ColaFlow.Modules.Identity.Application/DTOs/UserDto.cs (Updated)

public class UserDto
{
    public Guid Id { get; set; }
    public string Email { get; set; } = string.Empty;
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public string Role { get; set; } = string.Empty;
    public bool IsActive { get; set; }
    public DateTime CreatedAt { get; set; }

    // Multi-tenant fields (NEW)
    public Guid TenantId { get; set; }
    public string TenantName { get; set; } = string.Empty;
    public string TenantSlug { get; set; } = string.Empty;

    // SSO fields (NEW)
    public string AuthProvider { get; set; } = "Local";
    public string? ExternalUserId { get; set; }
}

How Tenant Context is Injected

  1. Request arrives at acme.colaflow.com/api/v1/auth/login
  2. TenantResolutionMiddleware extracts subdomain "acme"
  3. Middleware queries tenants table to find tenant with slug = "acme"
  4. Middleware injects TenantContext into HTTP context items
  5. LoginCommandHandler retrieves tenant from TenantContext
  6. JWT is generated with tenant claims embedded
  7. All subsequent requests use tenant claims from JWT for filtering

Security Benefits

  • Single JWT contains all context: No need for additional database lookups
  • Tenant isolation enforced: Every API call validates tenant from JWT
  • Cross-tenant attacks prevented: User cannot access other tenant's data
  • SSO integration ready: Auth provider claims already included
  • Audit-friendly: Token contains complete identity context

3.5 Infrastructure Layer - Database Configuration

EF Core Entity Configurations

// ColaFlow.Modules.Identity.Infrastructure/Persistence/Configurations/UserConfiguration.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;
using ColaFlow.Modules.Identity.Domain.ValueObjects;

namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Configurations;

public class UserConfiguration : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        builder.ToTable("users");

        builder.HasKey(u => u.Id);

        builder.Property(u => u.Id)
            .HasConversion(
                id => id.Value,
                value => UserId.Create(value))
            .HasColumnName("id");

        builder.Property(u => u.Email)
            .HasConversion(
                email => email.Value,
                value => Email.Create(value))
            .HasColumnName("email")
            .HasMaxLength(254)
            .IsRequired();

        builder.HasIndex(u => u.Email)
            .IsUnique()
            .HasDatabaseName("ix_users_email");

        builder.Property(u => u.PasswordHash)
            .HasConversion(
                hash => hash.Value,
                value => PasswordHash.Create(value)) // Note: This won't rehash, just wraps existing hash
            .HasColumnName("password_hash")
            .HasMaxLength(255)
            .IsRequired();

        builder.Property(u => u.FirstName)
            .HasColumnName("first_name")
            .HasMaxLength(100)
            .IsRequired();

        builder.Property(u => u.LastName)
            .HasColumnName("last_name")
            .HasMaxLength(100)
            .IsRequired();

        builder.Property(u => u.Role)
            .HasConversion(
                role => role.Name,
                name => Role.FromName(name))
            .HasColumnName("role")
            .HasMaxLength(50)
            .IsRequired();

        builder.Property(u => u.IsActive)
            .HasColumnName("is_active")
            .IsRequired();

        builder.Property(u => u.CreatedAt)
            .HasColumnName("created_at")
            .IsRequired();

        builder.Property(u => u.UpdatedAt)
            .HasColumnName("updated_at");

        builder.Property(u => u.LastLoginAt)
            .HasColumnName("last_login_at");

        // One-to-many relationship with RefreshTokens
        builder.HasMany(u => u.RefreshTokens)
            .WithOne(rt => rt.User)
            .HasForeignKey(rt => rt.UserId)
            .OnDelete(DeleteBehavior.Cascade);

        // Ignore domain events (not persisted)
        builder.Ignore(u => u.DomainEvents);
    }
}
// ColaFlow.Modules.Identity.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;
using ColaFlow.Modules.Identity.Domain.ValueObjects;

namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Configurations;

public class RefreshTokenConfiguration : IEntityTypeConfiguration<RefreshToken>
{
    public void Configure(EntityTypeBuilder<RefreshToken> builder)
    {
        builder.ToTable("refresh_tokens");

        builder.HasKey(rt => rt.Id);

        builder.Property(rt => rt.Id)
            .HasColumnName("id");

        builder.Property(rt => rt.UserId)
            .HasConversion(
                id => id.Value,
                value => UserId.Create(value))
            .HasColumnName("user_id")
            .IsRequired();

        builder.Property(rt => rt.Token)
            .HasColumnName("token")
            .HasMaxLength(500)
            .IsRequired();

        builder.HasIndex(rt => rt.Token)
            .IsUnique()
            .HasDatabaseName("ix_refresh_tokens_token");

        builder.Property(rt => rt.ExpiresAt)
            .HasColumnName("expires_at")
            .IsRequired();

        builder.Property(rt => rt.CreatedAt)
            .HasColumnName("created_at")
            .IsRequired();

        builder.Property(rt => rt.IsRevoked)
            .HasColumnName("is_revoked")
            .IsRequired();

        builder.Property(rt => rt.RevokedAt)
            .HasColumnName("revoked_at");

        // Index for cleanup queries (find expired/revoked tokens)
        builder.HasIndex(rt => new { rt.IsRevoked, rt.ExpiresAt })
            .HasDatabaseName("ix_refresh_tokens_cleanup");
    }
}

Database Context

// ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.cs

using Microsoft.EntityFrameworkCore;
using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;
using ColaFlow.Modules.Identity.Infrastructure.Persistence.Configurations;

namespace ColaFlow.Modules.Identity.Infrastructure.Persistence;

public class IdentityDbContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;
    public DbSet<RefreshToken> RefreshTokens { get; set; } = null!;

    public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // Apply all configurations
        modelBuilder.ApplyConfiguration(new UserConfiguration());
        modelBuilder.ApplyConfiguration(new RefreshTokenConfiguration());
    }
}

UserRepository Implementation

// ColaFlow.Modules.Identity.Infrastructure/Repositories/UserRepository.cs

using Microsoft.EntityFrameworkCore;
using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;
using ColaFlow.Modules.Identity.Domain.Repositories;
using ColaFlow.Modules.Identity.Domain.ValueObjects;
using ColaFlow.Modules.Identity.Infrastructure.Persistence;

namespace ColaFlow.Modules.Identity.Infrastructure.Repositories;

public class UserRepository : IUserRepository
{
    private readonly IdentityDbContext _context;

    public UserRepository(IdentityDbContext context)
    {
        _context = context ?? throw new ArgumentNullException(nameof(context));
    }

    public async Task<User?> GetByIdAsync(UserId id, CancellationToken cancellationToken = default)
    {
        return await _context.Users
            .Include(u => u.RefreshTokens)
            .FirstOrDefaultAsync(u => u.Id == id, cancellationToken);
    }

    public async Task<User?> GetByEmailAsync(Email email, CancellationToken cancellationToken = default)
    {
        return await _context.Users
            .Include(u => u.RefreshTokens)
            .FirstOrDefaultAsync(u => u.Email == email, cancellationToken);
    }

    public async Task<User?> GetByRefreshTokenAsync(string refreshToken, CancellationToken cancellationToken = default)
    {
        return await _context.Users
            .Include(u => u.RefreshTokens)
            .FirstOrDefaultAsync(u => u.RefreshTokens.Any(rt => rt.Token == refreshToken), cancellationToken);
    }

    public async Task AddAsync(User user, CancellationToken cancellationToken = default)
    {
        await _context.Users.AddAsync(user, cancellationToken);
    }

    public void Update(User user)
    {
        _context.Users.Update(user);
    }

    public void Remove(User user)
    {
        _context.Users.Remove(user);
    }
}

3.5 API Layer - Controllers & Middleware

AuthController

// ColaFlow.API/Controllers/AuthController.cs

using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using ColaFlow.Modules.Identity.Application.Commands.RegisterUser;
using ColaFlow.Modules.Identity.Application.Commands.Login;
using ColaFlow.Modules.Identity.Application.Commands.RefreshToken;
using ColaFlow.Modules.Identity.Application.Commands.Logout;
using ColaFlow.Modules.Identity.Application.Queries.GetCurrentUser;
using ColaFlow.Modules.Identity.Application.DTOs;

namespace ColaFlow.API.Controllers;

/// <summary>
/// Authentication API Controller
/// </summary>
[ApiController]
[Route("api/v1/[controller]")]
public class AuthController : ControllerBase
{
    private readonly IMediator _mediator;

    public AuthController(IMediator mediator)
    {
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    /// <summary>
    /// Register a new user
    /// </summary>
    [HttpPost("register")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Register(
        [FromBody] RegisterUserCommand command,
        CancellationToken cancellationToken = default)
    {
        var result = await _mediator.Send(command, cancellationToken);
        return CreatedAtAction(nameof(GetCurrentUser), result);
    }

    /// <summary>
    /// Login with email and password
    /// </summary>
    [HttpPost("login")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> Login(
        [FromBody] LoginCommand command,
        CancellationToken cancellationToken = default)
    {
        var result = await _mediator.Send(command, cancellationToken);

        // Set refresh token in httpOnly cookie
        SetRefreshTokenCookie(result.RefreshToken);

        return Ok(result);
    }

    /// <summary>
    /// Refresh access token using refresh token
    /// </summary>
    [HttpPost("refresh")]
    [AllowAnonymous]
    [ProducesResponseType(typeof(LoginResponseDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> RefreshToken(CancellationToken cancellationToken = default)
    {
        // Get refresh token from cookie
        var refreshToken = Request.Cookies["refreshToken"];
        if (string.IsNullOrEmpty(refreshToken))
        {
            return Unauthorized("Refresh token not found");
        }

        var command = new RefreshTokenCommand(refreshToken);
        var result = await _mediator.Send(command, cancellationToken);

        // Update refresh token cookie
        SetRefreshTokenCookie(result.RefreshToken);

        return Ok(result);
    }

    /// <summary>
    /// Logout and revoke refresh token
    /// </summary>
    [HttpPost("logout")]
    [Authorize]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> Logout(CancellationToken cancellationToken = default)
    {
        var refreshToken = Request.Cookies["refreshToken"];
        if (!string.IsNullOrEmpty(refreshToken))
        {
            var command = new LogoutCommand(refreshToken);
            await _mediator.Send(command, cancellationToken);
        }

        // Clear cookie
        Response.Cookies.Delete("refreshToken");

        return NoContent();
    }

    /// <summary>
    /// Get current authenticated user
    /// </summary>
    [HttpGet("me")]
    [Authorize]
    [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> GetCurrentUser(CancellationToken cancellationToken = default)
    {
        var userId = GetUserIdFromClaims();
        var query = new GetCurrentUserQuery(userId);
        var result = await _mediator.Send(query, cancellationToken);
        return Ok(result);
    }

    private void SetRefreshTokenCookie(string refreshToken)
    {
        var cookieOptions = new CookieOptions
        {
            HttpOnly = true, // Cannot be accessed by JavaScript
            Secure = true,   // HTTPS only
            SameSite = SameSiteMode.Strict, // CSRF protection
            Expires = DateTimeOffset.UtcNow.AddDays(7)
        };

        Response.Cookies.Append("refreshToken", refreshToken, cookieOptions);
    }

    private Guid GetUserIdFromClaims()
    {
        var userIdClaim = User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
                       ?? User.FindFirst("sub")?.Value;

        if (userIdClaim != null && Guid.TryParse(userIdClaim, out var userId))
        {
            return userId;
        }

        throw new UnauthorizedAccessException("User ID not found in token");
    }
}

Program.cs - JWT Configuration

// ColaFlow.API/Program.cs (updated)

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using ColaFlow.API.Extensions;
using ColaFlow.API.Handlers;
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Register ProjectManagement Module
builder.Services.AddProjectManagementModule(builder.Configuration);

// Register Identity Module (NEW)
builder.Services.AddIdentityModule(builder.Configuration);

// Add controllers
builder.Services.AddControllers();

// Configure JWT Authentication (NEW)
var jwtSection = builder.Configuration.GetSection("Jwt");
var secretKey = jwtSection["SecretKey"] ?? throw new InvalidOperationException("JWT SecretKey not configured");

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = jwtSection["Issuer"],
        ValidAudience = jwtSection["Audience"],
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
        ClockSkew = TimeSpan.Zero
    };

    options.Events = new JwtBearerEvents
    {
        OnAuthenticationFailed = context =>
        {
            if (context.Exception is SecurityTokenExpiredException)
            {
                context.Response.Headers.Add("Token-Expired", "true");
            }
            return Task.CompletedTask;
        }
    };
});

builder.Services.AddAuthorization();

// Configure exception handling (IExceptionHandler - .NET 8+)
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();

// Configure CORS for frontend
builder.Services.AddCors(options =>
{
    options.AddPolicy("AllowFrontend", policy =>
    {
        policy.WithOrigins("http://localhost:3000")
              .AllowAnyHeader()
              .AllowAnyMethod()
              .AllowCredentials(); // Required for cookies
    });
});

// Configure OpenAPI/Scalar
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
    app.MapScalarApiReference();
}

// Global exception handler (should be first in pipeline)
app.UseExceptionHandler();

// Enable CORS
app.UseCors("AllowFrontend");

app.UseHttpsRedirection();

// Authentication & Authorization (NEW)
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Protecting Existing Controllers

// ColaFlow.API/Controllers/ProjectsController.cs (updated)

using MediatR;
using Microsoft.AspNetCore.Authorization; // NEW
using Microsoft.AspNetCore.Mvc;
using ColaFlow.Modules.ProjectManagement.Application.DTOs;
using ColaFlow.Modules.ProjectManagement.Application.Commands.CreateProject;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjectById;
using ColaFlow.Modules.ProjectManagement.Application.Queries.GetProjects;

namespace ColaFlow.API.Controllers;

/// <summary>
/// Projects API Controller
/// </summary>
[ApiController]
[Route("api/v1/[controller]")]
[Authorize] // NEW - Protect all endpoints
public class ProjectsController : ControllerBase
{
    private readonly IMediator _mediator;

    public ProjectsController(IMediator mediator)
    {
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
    }

    // ... rest of the controller code remains the same
}

4. Database Schema

SQL DDL

-- Users table
CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(254) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    first_name VARCHAR(100) NOT NULL,
    last_name VARCHAR(100) NOT NULL,
    role VARCHAR(50) NOT NULL,
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NULL,
    last_login_at TIMESTAMP NULL
);

CREATE INDEX ix_users_email ON users(email);
CREATE INDEX ix_users_role ON users(role);
CREATE INDEX ix_users_is_active ON users(is_active);

-- Refresh tokens table
CREATE TABLE refresh_tokens (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    token VARCHAR(500) NOT NULL UNIQUE,
    expires_at TIMESTAMP NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    is_revoked BOOLEAN NOT NULL DEFAULT FALSE,
    revoked_at TIMESTAMP NULL
);

CREATE INDEX ix_refresh_tokens_token ON refresh_tokens(token);
CREATE INDEX ix_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX ix_refresh_tokens_cleanup ON refresh_tokens(is_revoked, expires_at);

EF Core Migration Command

# In colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure
dotnet ef migrations add InitialIdentitySchema --context IdentityDbContext --output-dir Persistence/Migrations

# Apply migration
dotnet ef database update --context IdentityDbContext

5. Configuration

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=colaflow;Username=postgres;Password=postgres",
    "IdentityConnection": "Host=localhost;Port=5432;Database=colaflow;Username=postgres;Password=postgres"
  },
  "Jwt": {
    "SecretKey": "YOUR-256-BIT-SECRET-KEY-MINIMUM-32-CHARACTERS-LONG-REPLACE-IN-PRODUCTION",
    "Issuer": "ColaFlow",
    "Audience": "ColaFlow-API",
    "ExpiryMinutes": 60
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

appsettings.Development.json

{
  "Jwt": {
    "SecretKey": "development-secret-key-32-chars-minimum-do-not-use-in-production",
    "ExpiryMinutes": 60
  }
}

Environment Variables (Production)

# NEVER commit secrets to git
export JWT__SECRETKEY="<strong-random-key-from-key-generator>"
export JWT__ISSUER="ColaFlow"
export JWT__AUDIENCE="ColaFlow-API"
export JWT__EXPIRYMINUTES="60"

Generate Secret Key:

# PowerShell
-join ((48..57) + (65..90) + (97..122) | Get-Random -Count 64 | % {[char]$_})

# Linux/Mac
openssl rand -base64 48

6. Frontend Integration (Next.js 16)

6.1 Authentication Store (Zustand)

// colaflow-web/src/stores/authStore.ts

import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface User {
  id: string;
  email: string;
  firstName: string;
  lastName: string;
  role: string;
  isActive: boolean;
}

interface AuthState {
  user: User | null;
  accessToken: string | null;
  isAuthenticated: boolean;
  login: (accessToken: string, user: User) => void;
  logout: () => void;
  updateToken: (accessToken: string) => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      accessToken: null,
      isAuthenticated: false,

      login: (accessToken, user) =>
        set({
          accessToken,
          user,
          isAuthenticated: true,
        }),

      logout: () =>
        set({
          accessToken: null,
          user: null,
          isAuthenticated: false,
        }),

      updateToken: (accessToken) =>
        set({ accessToken }),
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({
        user: state.user,
        // Don't persist accessToken (security)
      }),
    }
  )
);

6.2 API Client with Token Refresh

// colaflow-web/src/lib/api-client.ts

import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import { useAuthStore } from '@/stores/authStore';

const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://localhost:7001/api/v1';

export const apiClient = axios.create({
  baseURL: API_BASE_URL,
  withCredentials: true, // Include cookies (refresh token)
  headers: {
    'Content-Type': 'application/json',
  },
});

// Request interceptor - Add access token
apiClient.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const { accessToken } = useAuthStore.getState();
    if (accessToken && config.headers) {
      config.headers.Authorization = `Bearer ${accessToken}`;
    }
    return config;
  },
  (error) => Promise.reject(error)
);

// Response interceptor - Handle token refresh
let isRefreshing = false;
let refreshSubscribers: ((token: string) => void)[] = [];

function subscribeTokenRefresh(cb: (token: string) => void) {
  refreshSubscribers.push(cb);
}

function onTokenRefreshed(token: string) {
  refreshSubscribers.forEach((cb) => cb(token));
  refreshSubscribers = [];
}

apiClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };

    // If 401 and not already retrying
    if (error.response?.status === 401 && !originalRequest._retry) {
      if (isRefreshing) {
        // Wait for token refresh
        return new Promise((resolve) => {
          subscribeTokenRefresh((token: string) => {
            if (originalRequest.headers) {
              originalRequest.headers.Authorization = `Bearer ${token}`;
            }
            resolve(apiClient(originalRequest));
          });
        });
      }

      originalRequest._retry = true;
      isRefreshing = true;

      try {
        // Call refresh endpoint
        const response = await axios.post(
          `${API_BASE_URL}/auth/refresh`,
          {},
          { withCredentials: true }
        );

        const { accessToken } = response.data;

        // Update store
        useAuthStore.getState().updateToken(accessToken);

        // Notify subscribers
        onTokenRefreshed(accessToken);

        // Retry original request
        if (originalRequest.headers) {
          originalRequest.headers.Authorization = `Bearer ${accessToken}`;
        }
        return apiClient(originalRequest);
      } catch (refreshError) {
        // Refresh failed - logout
        useAuthStore.getState().logout();
        window.location.href = '/login';
        return Promise.reject(refreshError);
      } finally {
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

6.3 Auth API Functions

// colaflow-web/src/services/auth.service.ts

import { apiClient } from '@/lib/api-client';

export interface RegisterRequest {
  email: string;
  password: string;
  firstName: string;
  lastName: string;
}

export interface LoginRequest {
  email: string;
  password: string;
}

export interface LoginResponse {
  accessToken: string;
  refreshToken: string;
  expiresAt: string;
  user: {
    id: string;
    email: string;
    firstName: string;
    lastName: string;
    role: string;
    isActive: boolean;
  };
}

export const authService = {
  register: (data: RegisterRequest) =>
    apiClient.post('/auth/register', data),

  login: async (data: LoginRequest): Promise<LoginResponse> => {
    const response = await apiClient.post<LoginResponse>('/auth/login', data);
    return response.data;
  },

  logout: () =>
    apiClient.post('/auth/logout'),

  getCurrentUser: () =>
    apiClient.get('/auth/me'),

  refreshToken: () =>
    apiClient.post('/auth/refresh'),
};

6.4 Login Page

// colaflow-web/src/app/(auth)/login/page.tsx

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuthStore } from '@/stores/authStore';
import { authService } from '@/services/auth.service';

export default function LoginPage() {
  const router = useRouter();
  const login = useAuthStore((state) => state.login);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError('');
    setLoading(true);

    try {
      const response = await authService.login({ email, password });
      login(response.accessToken, response.user);
      router.push('/dashboard');
    } catch (err: any) {
      setError(err.response?.data?.message || 'Login failed');
    } finally {
      setLoading(false);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
        <h1 className="text-2xl font-bold text-center mb-6">Login to ColaFlow</h1>

        {error && (
          <div className="mb-4 p-3 bg-red-100 text-red-700 rounded">
            {error}
          </div>
        )}

        <form onSubmit={handleSubmit} className="space-y-4">
          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              Email
            </label>
            <input
              type="email"
              id="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            />
          </div>

          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              Password
            </label>
            <input
              type="password"
              id="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
              className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
            />
          </div>

          <button
            type="submit"
            disabled={loading}
            className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50"
          >
            {loading ? 'Logging in...' : 'Login'}
          </button>
        </form>

        <p className="mt-4 text-center text-sm text-gray-600">
          Don't have an account?{' '}
          <a href="/register" className="text-blue-600 hover:text-blue-500">
            Register
          </a>
        </p>
      </div>
    </div>
  );
}

6.5 Protected Route Middleware

// colaflow-web/src/middleware.ts

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const publicPaths = ['/login', '/register'];
const authPaths = ['/login', '/register'];

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Check if user has auth token (stored in cookie or localStorage)
  // Note: In production, validate token server-side
  const isAuthenticated = request.cookies.has('refreshToken');

  // Redirect authenticated users away from auth pages
  if (isAuthenticated && authPaths.includes(pathname)) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  // Redirect unauthenticated users to login
  if (!isAuthenticated && !publicPaths.includes(pathname)) {
    return NextResponse.redirect(new URL('/login', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    /*
     * Match all request paths except:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * - public files (public directory)
     */
    '/((?!_next/static|_next/image|favicon.ico|public).*)',
  ],
};

7. Security Considerations

7.1 OWASP Top 10 Compliance

Risk Mitigation
A01: Broken Access Control - JWT-based authentication
- [Authorize] attribute on all protected endpoints
- Role-based authorization
A02: Cryptographic Failures - BCrypt password hashing (work factor 12)
- HTTPS enforced
- httpOnly cookies for refresh tokens
- Minimum 256-bit JWT signing key
A03: Injection - EF Core parameterized queries
- Input validation with FluentValidation
- Email regex validation
A04: Insecure Design - Refresh Token Rotation
- Token expiration (60 min access, 7 day refresh)
- Password strength requirements
- Account lockout (future)
A05: Security Misconfiguration - Environment-specific config
- Secrets in environment variables
- CORS configured for specific origin
- Disable unnecessary features
A06: Vulnerable Components - Latest .NET 9 and packages
- Regular dependency updates
- Security scanning in CI/CD
A07: Authentication Failures - Strong password policy
- Refresh token rotation
- Revoke tokens on password change
- Limit active refresh tokens per user
A08: Software/Data Integrity - Audit logs (domain events)
- Signed JWT tokens
- Immutable domain events
A09: Logging Failures - UserLoggedInEvent
- UserRegisteredEvent
- Failed login attempts (future)
- Audit all authentication events
A10: SSRF - No external requests based on user input in auth flow

7.2 Token Security

Access Token:

  • Short-lived (60 minutes)
  • Stored in memory (Zustand store, not persisted)
  • Sent in Authorization: Bearer <token> header
  • Cannot be revoked (design trade-off for performance)

Refresh Token:

  • Long-lived (7 days)
  • Stored in httpOnly cookie (XSS protection)
  • Secure + SameSite=Strict (CSRF protection)
  • Can be revoked (stored in database)
  • Rotation on each use (prevents replay attacks)
  • Max 5 active tokens per user

7.3 Password Security

  • Algorithm: BCrypt with work factor 12
  • Requirements:
    • Minimum 8 characters
    • Maximum 128 characters
    • At least one uppercase letter
    • At least one lowercase letter
    • At least one digit
    • At least one special character
  • Storage: Hashed with salt (BCrypt handles this)
  • Transmission: HTTPS only

7.4 Additional Security Measures

Recommended (Future):

  1. Rate Limiting: Limit login attempts (e.g., 5 per 15 minutes)
  2. Account Lockout: Lock account after N failed attempts
  3. 2FA: Two-factor authentication support
  4. Email Verification: Verify email on registration
  5. Password Reset: Secure password reset flow
  6. Audit Logging: Log all authentication events to database
  7. Token Blacklist: Blacklist access tokens on logout (requires Redis)
  8. IP Whitelisting: Optional IP-based restrictions for admin users
  9. Session Management: Track active sessions, allow user to revoke

8. Testing Strategy

8.1 Unit Tests (Domain Layer)

// ColaFlow.Domain.Tests/UserTests.cs

using ColaFlow.Modules.Identity.Domain.Aggregates.UserAggregate;
using ColaFlow.Modules.Identity.Domain.ValueObjects;
using ColaFlow.Modules.Identity.Domain.Exceptions;
using Xunit;

namespace ColaFlow.Domain.Tests;

public class UserTests
{
    [Fact]
    public void Create_ValidUser_Success()
    {
        // Arrange
        var email = Email.Create("test@example.com");
        var password = "SecureP@ss123";
        var firstName = "John";
        var lastName = "Doe";
        var role = Role.User;

        // Act
        var user = User.Create(email, password, firstName, lastName, role);

        // Assert
        Assert.NotNull(user);
        Assert.Equal(email, user.Email);
        Assert.True(user.IsActive);
        Assert.Single(user.DomainEvents); // UserRegisteredEvent
    }

    [Fact]
    public void Login_InvalidPassword_ThrowsException()
    {
        // Arrange
        var user = User.Create(
            Email.Create("test@example.com"),
            "SecureP@ss123",
            "John",
            "Doe",
            Role.User
        );

        // Act & Assert
        Assert.Throws<InvalidCredentialsException>(() => user.Login("WrongPassword"));
    }

    [Fact]
    public void CreateRefreshToken_Success()
    {
        // Arrange
        var user = User.Create(
            Email.Create("test@example.com"),
            "SecureP@ss123",
            "John",
            "Doe",
            Role.User
        );

        // Act
        var refreshToken = user.CreateRefreshToken(7);

        // Assert
        Assert.NotNull(refreshToken);
        Assert.Equal(user.Id, refreshToken.UserId);
        Assert.False(refreshToken.IsRevoked);
        Assert.True(refreshToken.ExpiresAt > DateTime.UtcNow);
    }

    [Theory]
    [InlineData("short")] // Too short
    [InlineData("nouppercase123!")] // No uppercase
    [InlineData("NOLOWERCASE123!")] // No lowercase
    [InlineData("NoDigits!")] // No digits
    [InlineData("NoSpecialChar123")] // No special char
    public void Create_WeakPassword_ThrowsException(string weakPassword)
    {
        // Arrange & Act & Assert
        Assert.Throws<DomainException>(() =>
            User.Create(
                Email.Create("test@example.com"),
                weakPassword,
                "John",
                "Doe",
                Role.User
            )
        );
    }
}

8.2 Integration Tests (Application + Infrastructure)

// ColaFlow.IntegrationTests/AuthenticationTests.cs

using ColaFlow.Modules.Identity.Application.Commands.RegisterUser;
using ColaFlow.Modules.Identity.Application.Commands.Login;
using ColaFlow.Modules.Identity.Domain.Exceptions;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace ColaFlow.IntegrationTests;

public class AuthenticationTests : IClassFixture<TestWebApplicationFactory>
{
    private readonly IMediator _mediator;

    public AuthenticationTests(TestWebApplicationFactory factory)
    {
        _mediator = factory.Services.GetRequiredService<IMediator>();
    }

    [Fact]
    public async Task RegisterAndLogin_ValidCredentials_Success()
    {
        // Arrange
        var registerCommand = new RegisterUserCommand(
            Email: "integration@test.com",
            Password: "TestPass123!",
            FirstName: "Test",
            LastName: "User"
        );

        // Act - Register
        var user = await _mediator.Send(registerCommand);

        // Assert - Register
        Assert.NotNull(user);
        Assert.Equal("integration@test.com", user.Email);

        // Act - Login
        var loginCommand = new LoginCommand("integration@test.com", "TestPass123!");
        var loginResponse = await _mediator.Send(loginCommand);

        // Assert - Login
        Assert.NotNull(loginResponse);
        Assert.NotEmpty(loginResponse.AccessToken);
        Assert.NotEmpty(loginResponse.RefreshToken);
        Assert.Equal(user.Id, loginResponse.User.Id);
    }

    [Fact]
    public async Task Login_InvalidCredentials_ThrowsException()
    {
        // Arrange
        var loginCommand = new LoginCommand("nonexistent@test.com", "WrongPassword");

        // Act & Assert
        await Assert.ThrowsAsync<InvalidCredentialsException>(
            async () => await _mediator.Send(loginCommand)
        );
    }
}

8.3 API Tests (End-to-End)

// ColaFlow.IntegrationTests/AuthControllerTests.cs

using System.Net;
using System.Net.Http.Json;
using ColaFlow.Modules.Identity.Application.DTOs;
using Xunit;

namespace ColaFlow.IntegrationTests;

public class AuthControllerTests : IClassFixture<TestWebApplicationFactory>
{
    private readonly HttpClient _client;

    public AuthControllerTests(TestWebApplicationFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task POST_Register_ReturnsCreated()
    {
        // Arrange
        var request = new
        {
            Email = "apitest@example.com",
            Password = "ApiTest123!",
            FirstName = "API",
            LastName = "Test"
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/v1/auth/register", request);

        // Assert
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
        var user = await response.Content.ReadFromJsonAsync<UserDto>();
        Assert.NotNull(user);
        Assert.Equal(request.Email, user.Email);
    }

    [Fact]
    public async Task POST_Login_ReturnsToken()
    {
        // Arrange - First register
        var registerRequest = new
        {
            Email = "login@example.com",
            Password = "LoginTest123!",
            FirstName = "Login",
            LastName = "Test"
        };
        await _client.PostAsJsonAsync("/api/v1/auth/register", registerRequest);

        // Act - Login
        var loginRequest = new
        {
            Email = "login@example.com",
            Password = "LoginTest123!"
        };
        var response = await _client.PostAsJsonAsync("/api/v1/auth/login", loginRequest);

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var loginResponse = await response.Content.ReadFromJsonAsync<LoginResponseDto>();
        Assert.NotNull(loginResponse);
        Assert.NotEmpty(loginResponse.AccessToken);
        Assert.True(response.Headers.Contains("Set-Cookie")); // Refresh token cookie
    }

    [Fact]
    public async Task GET_Me_WithoutToken_ReturnsUnauthorized()
    {
        // Act
        var response = await _client.GetAsync("/api/v1/auth/me");

        // Assert
        Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
    }

    [Fact]
    public async Task GET_Me_WithValidToken_ReturnsUser()
    {
        // Arrange - Register and login
        var email = "me@example.com";
        await RegisterAndLogin(email, "MeTest123!");

        // Act
        var response = await _client.GetAsync("/api/v1/auth/me");

        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
        var user = await response.Content.ReadFromJsonAsync<UserDto>();
        Assert.NotNull(user);
        Assert.Equal(email, user.Email);
    }

    private async Task RegisterAndLogin(string email, string password)
    {
        // Register
        await _client.PostAsJsonAsync("/api/v1/auth/register", new
        {
            Email = email,
            Password = password,
            FirstName = "Test",
            LastName = "User"
        });

        // Login
        var loginResponse = await _client.PostAsJsonAsync("/api/v1/auth/login", new
        {
            Email = email,
            Password = password
        });

        var loginData = await loginResponse.Content.ReadFromJsonAsync<LoginResponseDto>();
        _client.DefaultRequestHeaders.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", loginData!.AccessToken);
    }
}

9. Implementation Roadmap (7 Days)

Day 1: Architecture & Domain Layer (Today)

Deliverables:

  • Complete architecture document (this document)
  • Domain models (User, RefreshToken, Value Objects)
  • Domain events
  • Repository interfaces

Tasks:

  1. Review and approve this architecture document
  2. Create Identity module structure
  3. Implement User aggregate root
  4. Implement value objects (Email, PasswordHash, Role)
  5. Implement RefreshToken entity
  6. Write unit tests for domain logic

Day 2-3: Application & Infrastructure Layers

Deliverables:

  • Commands (Register, Login, RefreshToken, Logout, ChangePassword)
  • Queries (GetCurrentUser, ValidateToken)
  • Command/Query handlers
  • FluentValidation validators
  • JwtService implementation
  • UserRepository implementation
  • EF Core configurations
  • Database migrations

Tasks:

  1. Implement all Commands and Handlers
  2. Implement all Queries and Handlers
  3. Add FluentValidation for input validation
  4. Implement JwtService (token generation/validation)
  5. Implement UserRepository with EF Core
  6. Create EF Core entity configurations
  7. Generate and test database migrations
  8. Write integration tests

Day 4: API Layer & Backend Integration

Deliverables:

  • AuthController with all endpoints
  • JWT authentication middleware configuration
  • Protected controllers ([Authorize] attributes)
  • Global exception handling updates
  • API documentation (Scalar/OpenAPI)

Tasks:

  1. Create AuthController
  2. Configure JWT authentication in Program.cs
  3. Add [Authorize] to all protected controllers
  4. Update GlobalExceptionHandler for auth exceptions
  5. Configure CORS for frontend
  6. Test all API endpoints with Postman/Scalar
  7. Write API integration tests

Day 5-6: Frontend Implementation

Deliverables:

  • Zustand auth store
  • API client with token refresh interceptor
  • Login/Register pages
  • Protected route middleware
  • Auth API service functions
  • Dashboard with current user display

Tasks:

  1. Create Zustand auth store
  2. Implement API client with Axios interceptors
  3. Create auth service functions
  4. Build Login page
  5. Build Register page
  6. Implement Next.js middleware for protected routes
  7. Update existing pages to use auth
  8. Add logout functionality
  9. Handle token expiration gracefully

Day 7: Testing, Integration & Documentation

Deliverables:

  • Complete test suite (unit + integration + E2E)
  • CI/CD pipeline updates
  • Deployment guide
  • Security audit checklist
  • User documentation

Tasks:

  1. Run full test suite and fix any issues
  2. Perform security audit (OWASP checklist)
  3. Test token refresh flow end-to-end
  4. Test all protected routes
  5. Update CI/CD to run auth tests
  6. Document deployment process
  7. Create user guide for authentication
  8. Final review and demo

10. Risk Assessment & Mitigation

Risk Impact Probability Mitigation
JWT Secret Key Exposure Critical Low - Store in environment variables
- Never commit to git
- Rotate keys periodically
- Use strong random keys (256+ bits)
Password Database Breach High Low - BCrypt with work factor 12
- Salted hashes
- Rate limit login attempts
- Monitor for breach patterns
Token Theft (XSS) High Medium - httpOnly cookies for refresh tokens
- Short-lived access tokens
- Content Security Policy (CSP)
- Input sanitization
Token Theft (CSRF) Medium Medium - SameSite=Strict cookies
- CORS configuration
- Double-submit cookie pattern (future)
Refresh Token Replay Medium Low - Token rotation on each use
- Revoke old tokens
- Detect multiple concurrent uses
Performance Issues Medium Low - Index on email and token columns
- Cache user lookups (Redis, future)
- Connection pooling
- Token validation is fast (stateless)
Breaking Existing APIs High Low - Add [Authorize] incrementally
- Test all endpoints
- Frontend updates synchronized
- Allow grace period for migration

11. Future Enhancements

Phase 2 (Post-M1)

  1. Rate Limiting: Protect against brute force attacks
  2. Account Lockout: Lock after N failed login attempts
  3. Email Verification: Confirm email on registration
  4. Password Reset: Secure password reset flow via email
  5. Audit Logging: Persist authentication events to database
  6. Admin User Management: CRUD operations for admin users

Phase 3 (M2-M3)

  1. Two-Factor Authentication (2FA): TOTP or SMS-based
  2. OAuth2/OpenID Connect: Integration with Google, GitHub, etc.
  3. API Key Authentication: For MCP client integrations
  4. Session Management UI: View/revoke active sessions
  5. Token Blacklist: Redis-based access token revocation
  6. Advanced RBAC: Fine-grained permissions system

Phase 4 (M4+)

  1. Single Sign-On (SSO): Enterprise SSO via SAML
  2. Passwordless Authentication: Magic links, WebAuthn
  3. Security Dashboard: Monitor suspicious activities
  4. Compliance Features: GDPR data export, right to be forgotten

12. References & Resources

.NET 9 & JWT

Security Best Practices

Clean Architecture & DDD

Libraries & Tools


13. Approval & Sign-off

Role Name Status Date
Architect Architecture Team Approved 2025-11-03
Product Manager - Pending -
Backend Lead - Pending -
Frontend Lead - Pending -
Security Review - Pending -

Appendix A: Quick Start Commands

Backend Setup

# Navigate to backend
cd c:\Users\yaoji\git\ColaCoder\product-master\colaflow-api

# Create Identity module structure
mkdir -p src/Modules/Identity/ColaFlow.Modules.Identity.Domain
mkdir -p src/Modules/Identity/ColaFlow.Modules.Identity.Application
mkdir -p src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure

# Install BCrypt package
cd src/Modules/Identity/ColaFlow.Modules.Identity.Domain
dotnet add package BCrypt.Net-Next

# Generate migration
cd ../ColaFlow.Modules.Identity.Infrastructure
dotnet ef migrations add InitialIdentitySchema --context IdentityDbContext

# Apply migration
dotnet ef database update --context IdentityDbContext

# Run backend
cd ../../../ColaFlow.API
dotnet run

Frontend Setup

# Navigate to frontend
cd c:\Users\yaoji\git\ColaCoder\product-master\colaflow-web

# Install dependencies
npm install axios zustand

# Create auth store and services (copy code from sections 6.1-6.3)

# Run frontend
npm run dev

Testing

# Run all tests
dotnet test

# Run with coverage
dotnet test --collect:"XPlat Code Coverage"

# Run specific test project
dotnet test tests/ColaFlow.Domain.Tests

END OF ARCHITECTURE DOCUMENT

This architecture design is comprehensive, production-ready, and aligned with Clean Architecture, DDD, and CQRS principles. It addresses all security requirements per OWASP standards and provides a clear 7-day implementation roadmap.

For questions or clarifications, please contact the Architecture Team.