NetAuth is an educational ASP.NET Core authentication service built with .NET 10, designed to learn and apply modern software architecture patterns including Domain-Driven Design (DDD), CQRS, Clean Architecture, RBAC with permission-based authorization, and the Transactional Outbox Pattern.
- I. Architecture
- II. Features
- III. Technology Stack
- IV. Getting Started
- V. Project Structure
- VI. Design Patterns & Principles
- VII. Security Features
- VIII. Domain Errors Best Practice
- IX. Testing
- X. Configuration
- XI. API Documentation
- XII. Performance Considerations
- XIII. Observability
- XIV. Roadmap
- XV. License
NetAuth follows Clean Architecture principles with clear separation of concerns:
- Domain Layer - Core business logic and entities (framework-agnostic)
- Application Layer - Use cases, commands, queries, and business workflows
- Infrastructure Layer - Technical implementations and external dependencies
- Web.Api Layer - HTTP endpoints and API contracts
- β User Registration & Authentication
- β JWT-based Authentication with access tokens
- β Refresh Token Rotation with automatic revocation on reuse detection
- β Device Binding for enhanced security
- β Permission-Based Authorization (RBAC with fine-grained permissions)
- β Audit Logging via domain events
- β Outbox Pattern for reliable event processing (batching, SKIP LOCKED, retry with max attempts)
- β Rate Limiting on authentication endpoints
- β Health Checks for database and Redis
- β OpenAPI/Swagger documentation
- β Hybrid Cache for permission lookups (memory + Redis)
- β API Versioning (v1, v2) with grouped endpoints
- .NET 10 with C# 14
- ASP.NET Core Minimal APIs
- Entity Framework Core 10 with PostgreSQL
- MediatR 12 for CQRS
- FluentValidation 12 for validation
- LanguageExt 4 for functional programming (Either, Option)
- Ardalis.GuardClauses for defensive programming
- Humanizer for string transformations
- JWT Bearer Authentication (Microsoft.AspNetCore.Authentication.JwtBearer)
- PBKDF2 for password hashing
- Permission-based authorization
- Npgsql for PostgreSQL
- Dapper for raw SQL queries (Outbox)
- EFCore.NamingConventions for snake_case naming
- HybridCache for distributed caching with local cache fallback
- StackExchange.Redis for Redis connectivity
- Quartz.NET 3 for scheduled jobs (Outbox processing)
- Asp.Versioning for API versioning (v1, v2)
- Swashbuckle for Swagger/OpenAPI documentation
- Microsoft.AspNetCore.OpenApi for OpenAPI support
- Serilog for structured logging (Console/File/Seq)
- AspNetCore.HealthChecks for health monitoring (PostgreSQL, Redis, UI)
- xUnit for test framework
- NSubstitute for mocking
- Testcontainers for integration tests (PostgreSQL, Redis)
- NetArchTest for architecture tests
- LanguageExt.UnitTesting for Either/Option assertions
- Coverlet for code coverage
- .NET 10 SDK
- PostgreSQL 16+
- Redis 7+
- Docker & Docker Compose (optional)
# Start PostgreSQL and Redis (compose.yaml)
docker compose up -d
# Apply database migrations
dotnet ef database update --project src/NetAuth/NetAuth.csproj --startup-project src/NetAuth
# Run the application
dotnet run --project src/NetAuth/NetAuth.csproj# Update connection strings in appsettings.Development.json
# Or copy .env.example to .env and fill Jwt__SecretKey, connection strings, Seq URL, etc.
# Then run:
dotnet run --project src/NetAuth/NetAuth.csprojThe API will be available at:
- HTTPS:
https://localhost:7169 - HTTP:
http://localhost:5215 - Swagger UI:
https://localhost:7169/swagger
NetAuth/
βββ Domain/ # Core business logic
β βββ Core/ # Base classes and abstractions
β β βββ Abstractions/ # Interfaces (IAuditableEntity, ISoftDeletableEntity)
β β βββ Events/ # Domain event base classes
β β βββ Primitives/ # Entity, AggregateRoot, ValueObject, DomainError
β βββ Users/ # User bounded context
β β βββ User.cs # User aggregate root
β β βββ Email.cs # Email value object
β β βββ Username.cs # Username value object
β β βββ Password.cs # Password value object
β β βββ RefreshToken.cs # Refresh token entity
β β βββ Role.cs # Role entity with permissions
β β βββ UsersDomainErrors.cs # Domain errors (static readonly fields)
β βββ TodoItems/ # TodoItem bounded context
β βββ TodoItem.cs # TodoItem aggregate root
β βββ TodoTitle.cs # TodoTitle value object
β βββ TodoDescription.cs # TodoDescription value object
β βββ TodoItemDomainErrors.cs # Domain errors
βββ Application/ # Use cases and workflows
β βββ Abstractions/ # Application interfaces
β β βββ Authentication/ # Auth abstractions (IJwtProvider, IUserContext)
β β βββ Common/ # Common abstractions (IClock)
β β βββ Cryptography/ # Password hashing
β β βββ Data/ # Repository, UnitOfWork
β β βββ Messaging/ # CQRS abstractions (ICommand, IQuery)
β βββ Core/
β β βββ Behaviors/ # MediatR pipeline behaviors (Validation, Logging)
β β βββ Exceptions/ # Application exceptions
β β βββ Extensions/ # Extension methods
β βββ Users/ # User feature slices
β β βββ Login/ # Login command, handler, validator
β β βββ LoginWithRefreshToken/
β β βββ Register/ # Registration command, handler, validator
β β βββ SetUserRoles/ # Role management
β β βββ GetRoles/ # Query all roles
β β βββ GetUserRoles/ # Query user's roles
β βββ TodoItems/ # TodoItem feature slices
β βββ Create/ # Create todo item
β βββ Update/ # Update todo item
β βββ Complete/ # Mark as completed
β βββ MarkAsIncomplete/ # Undo completion
β βββ Get/ # Query todo items
βββ Infrastructure/ # Technical implementations
β βββ Authentication/ # JWT provider, refresh token generator
β βββ Authorization/ # Permission service, policies
β βββ Configurations/ # EF Core entity configurations
β βββ Cryptography/ # Password hasher
β βββ Interceptors/ # EF Core interceptors (audit, soft delete)
β βββ Migrations/ # EF Core migrations
β βββ Outbox/ # Outbox pattern implementation
β βββ Repositories/ # Repository implementations
βββ Web.Api/ # HTTP layer
βββ Endpoints/ # Minimal API endpoints
βββ ExceptionHandlers/ # Global exception handling
βββ Extensions/ # API extensions
βββ OpenApi/ # OpenAPI configuration
- Aggregates: User is the aggregate root managing RefreshTokens
- Value Objects: Email, Username, Password with validation
- Domain Events: UserCreatedDomainEvent, UserRolesChangedDomainEvent, RefreshTokenCreated/Rotated/ReuseDetected/DeviceMismatchDetected/ExpiredUsage/ChainCompromised
- Domain Errors: Immutable error types using
static readonlyfields for performance
- Commands: Operations that change state (Login, Register)
- Queries: Read operations (future: GetUserProfile)
- Handlers: Separate handler per command/query
- Validation: FluentValidation in pipeline behavior
- Dependency Rule: Dependencies point inward (Infrastructure β Application β Domain)
- Framework Independence: Domain layer has no external dependencies
- Testability: Clear boundaries enable easy unit testing
- Railway-Oriented Programming: Using
Either<DomainError, T>for operations that can fail - Option Type: Using
Option<T>for nullable values - Monadic Composition: Chaining operations with
Bind,Map,MapAsync
Ensures reliable event processing:
- Domain events saved as
OutboxMessagein same transaction - Quartz job processes messages on an interval (
Outbox:Interval) with batch size and max attempts - Uses
FOR UPDATE SKIP LOCKEDto avoid double processing - Parallel publish with a capped degree of parallelism and bulk update of processed rows
- PBKDF2 algorithm with 80,000 iterations (v1, salted, constant-time verify)
- Unique random salt per password
- Versioned storage format:
v1.{iterations}.{salt}.{hash}
- Token Rotation: New token issued on every refresh
- Reuse Detection: Automatic chain revocation on suspicious activity
- Device Binding: Tokens bound to specific devices
- Expiration: Configurable token lifetime (default config 7 days; development config shorter)
- Audit Trail: Complete history via domain events
- Permission-Based: Fine-grained permissions (
permission:resource:action) - Claims Transformation: Role permissions loaded and cached
- Policy-Based: Custom authorization policies
All domain and validation errors use static readonly fields for optimal performance:
public static class UsersDomainErrors
{
public static class Email
{
// β
CORRECT - static readonly field (single allocation)
public static readonly DomainError InvalidFormat = new(
code: "User.Email.InvalidFormat",
message: "The email format is invalid.",
type: DomainError.ErrorType.Validation);
// β WRONG - property (new allocation on every access)
// public static DomainError InvalidFormat => new(...);
}
}Benefits:
- Single allocation per error, no per-call allocations
- Thread-safe by CLR static initialization guarantee
- Clear, centralized error catalog
tests/
βββ UnitTests/ # 459 tests
β βββ Domain/
β β βββ Core/Primitives/ # ValueObject, Entity, AggregateRoot, DomainError tests
β β βββ Users/ # Email, Username, Password, User, RefreshToken tests
β β βββ TodoItems/ # TodoItem, TodoTitle, TodoDescription tests
β βββ Application/
β βββ Core/ # ValidationError, DateTimeExtensions tests
β βββ Users/ # Login, Register, RefreshToken handlers & validators
β βββ TodoItems/ # Create, Update, Complete, MarkAsIncomplete handlers & validators
β
βββ IntegrationTests/ # 24 tests
β βββ Users/ # Register, Login, RefreshToken, SetUserRoles
β βββ TodoItems/ # Create, Complete, MarkAsIncomplete, Update, GetTodoItems
β
βββ ArchitectureTests/ # 6 tests
βββ LayerTest.cs # Domain, Application, Infrastructure, WebApi layer rules
- Domain logic (value objects, entities, aggregates)
- Command/query handlers with mocked dependencies (NSubstitute)
- Validators with FluentValidation test helpers
- Uses xUnit and LanguageExt.UnitTesting
- Full application pipeline with real database (PostgreSQL via Testcontainers)
- Tests DI wiring, EF Core mappings, transaction behavior
- Verifies outbox pattern: domain events stored in same transaction
- Tests authorization and ownership validation
- Uses WebApplicationFactory for realistic HTTP pipeline
- Dependency rules enforcement (NetArchTest)
- Layer isolation verification
- Naming conventions
# Run all tests
dotnet test
# Run with coverage
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
# Run specific test category
dotnet test --filter "FullyQualifiedName~UnitTests.Domain"Before running the application, you must configure the JWT Secret Key. This key is required for generating and validating JWT tokens.
# Navigate to the project directory
cd src/NetAuth
# Initialize user secrets (if not already done)
dotnet user-secrets init
# Set the JWT Secret Key (minimum 32 characters)
dotnet user-secrets set "Jwt:SecretKey" "your-super-secret-key-here-minimum-32-characters-long"# Linux/macOS
export Jwt__SecretKey="your-super-secret-key-here-minimum-32-characters-long"
# Windows (PowerShell)
$env:Jwt__SecretKey="your-super-secret-key-here-minimum-32-characters-long"
# Windows (Command Prompt)
set Jwt__SecretKey=your-super-secret-key-here-minimum-32-characters-longAdd to your compose file (e.g., compose.yaml):
services:
netauth:
environment:
- Jwt__SecretKey=${JWT_SECRET_KEY}Then set the environment variable before running Docker Compose:
export JWT_SECRET_KEY="your-super-secret-key-here-minimum-32-characters-long"
docker compose up -d
β οΈ Security Notes:
- Never commit the actual secret key to source control
- Use a cryptographically secure random key (minimum 32 bytes / 256 bits for HS256)
- Rotate keys periodically in production
- Use different keys for each environment (dev, staging, production)
Key configuration sections in appsettings.json:
{
"Jwt": {
"SecretKey": "",
"Issuer": "hoc081098",
"Audience": "MyAppClients",
"Expiration": "00:10:00",
"RefreshTokenExpiration": "7.00:00:00"
},
"Outbox": {
"Interval": "00:00:10",
"BatchSize": 500,
"MaxAttempts": 3,
"CleanupRetention": "30.00:00:00",
"CleanupBatchSize": 5000
}
}Development settings override JWT expirations (access: 1 hour, refresh: 2 hours) and include localhost connection strings for PostgreSQL and Redis.
Visit /swagger for interactive API documentation.
Available in Development environment (enabled when
ASPNETCORE_ENVIRONMENT=Development).
POST /v1/auth/register- Register new userPOST /v1/auth/login- Login with email/passwordPOST /v1/auth/refresh- Refresh access token
Replace
v1withv2for the alternate API version.
Authentication endpoints are protected with rate limiting:
- /auth/login: Sliding window 5 requests per 20s per IP
- /auth/register: Sliding window 3 requests per minute per IP
- /auth/refresh: Sliding window 20 requests per minute per IP
- Global: Sliding window 100 requests per minute per IP for all other endpoints
- Static readonly domain errors (zero allocation per access)
- Outbox processor uses SKIP LOCKED + bulk updates + limited parallel publish
- Permission caching via HybridCache (memory + Redis)
- Rate limiting on auth endpoints and global limiter
- PostgreSQL database connectivity
- Redis connectivity
- DbContext health
- Outbox backlog/processing health
- Structured logging with Serilog
- Correlation ID tracking for request tracing (X-Correlation-Id header)
- Audit logging via domain events
- Request/response logging with timing
- Unit tests and architecture tests (465 tests: 459 Unit + 6 Architecture)
- Integration tests for critical flows (24 tests: Users + TodoItems)
- CI/CD pipeline with GitHub Actions
- Correlation ID logging for request tracing
- JWT SecretKey configuration with documentation
- XML documentation for complex business logic
- API versioning (v1, v2)
- Implement user profile management
- Add email verification
- Implement password reset flow
- Add account lockout after failed attempts
- Implement MFA (Multi-Factor Authentication)
- Add distributed tracing with OpenTelemetry
- Add GraphQL endpoint
- Add response compression and caching
- Implement pagination and sorting
This project is licensed under the MIT License - see the LICENSE file for details.
Built with β€οΈ using .NET 10 and Clean Architecture principles