NAuth.API is the central backend of the NAuth ecosystem β a complete, modular authentication framework designed for fast and secure user management in modern web applications. Built using .NET 8 and PostgreSQL, it provides a robust REST API for user registration, login, password recovery, role management, and profile updates.
The project supports multi-tenant architecture with database-per-tenant isolation, where each tenant has its own PostgreSQL database, JWT secret, and S3 bucket. Tenant resolution happens via JWT claims or HTTP headers.
This is the main project of the NAuth ecosystem. The frontend component library nauth-react integrates with and consumes this API. The DTO and ACL packages are included in-solution under the NAuth project (also published as a NuGet package).
The project follows a clean architecture approach with separated layers for API, Application, Domain, Infrastructure, and comprehensive test coverage.
- π User Registration - Complete registration flow with email confirmation
- π JWT Authentication - Secure token-based authentication with per-tenant secrets
- π’ Multi-Tenant - Database-per-tenant isolation with independent JWT secrets and S3 buckets
- π Password Recovery - Secure password reset via email with token validation
- βοΈ Profile Management - User profile update and password change
- π₯ Role-Based Access Control - User roles and permissions management
- π§ Email Integration - Email templates via MailerSend
- ποΈ PostgreSQL Database - Schema and migrations included
- π¦ Modular Architecture - Reusable across multiple projects via NuGet package
- π REST API - Complete RESTful API with Swagger documentation
- π³ Docker Support - Dev and production Docker Compose configurations
- β Health Checks - Built-in health check endpoints
- π Security - Non-root containers, encrypted passwords, token validation
- πΌοΈ Image Processing - Profile image upload and processing
- π³ Payment Integration - Stripe payment processing support
- .NET 8.0 - Modern, cross-platform framework for building web APIs
- ASP.NET Core - Web framework for building HTTP services
- Entity Framework Core 9.0 - ORM with lazy loading proxies
- PostgreSQL 16 - Robust relational database
- Npgsql.EntityFrameworkCore.PostgreSQL 9.0 - PostgreSQL provider for EF Core
- JWT (JSON Web Tokens) - Secure authentication with per-tenant signing keys
- BCrypt.Net-Next 4.0 - Strong password hashing
- Token-based Email Verification - Secure email confirmation and password reset
- Swashbuckle.AspNetCore 9.0 - Swagger/OpenAPI documentation
- AWSSDK.S3 - Amazon S3 file storage (per-tenant buckets)
- Stripe.net - Payment processing integration
- SixLabors.ImageSharp - Image processing
- Newtonsoft.Json - JSON serialization
- xUnit 2.4 - Unit testing framework
- Moq 4.20 - Mocking framework
- Coverlet - Code coverage
- EF Core InMemory - In-memory database for tests
- Docker - Containerization with multi-stage builds
- Docker Compose - Dev and production orchestration
- GitHub Actions - CI/CD pipelines (version tagging, NuGet publish, production deploy)
- GitVersion - Semantic versioning (ContinuousDelivery mode)
NAuth/
βββ NAuth.API/ # Web API layer
β βββ Controllers/ # API endpoints (User, Role)
β βββ Handlers/ # MultiTenantHandler (JWT auth per tenant)
β βββ Middlewares/ # TenantMiddleware (X-Tenant-Id header)
β βββ Services/ # TenantResolver, TenantDbContextFactory, TenantContext
β βββ appsettings.*.json # Configuration per environment
β βββ Startup.cs # Application configuration and DI
βββ NAuth.Application/ # Application layer
β βββ Initializer.cs # Dependency injection composition root
βββ NAuth.Domain/ # Domain layer with business logic
β βββ Models/ # Domain models
β βββ Services/ # UserService, RoleService
β βββ Factory/ # UserDomainFactory, RoleDomainFactory
β βββ Exceptions/ # Custom domain exceptions
βββ NAuth.Infra/ # Infrastructure layer
β βββ Context/ # NAuthContext (EF Core)
β βββ Repository/ # Data access repositories
β βββ Migrations/ # EF Core migrations
βββ NAuth.Infra.Interfaces/ # Repository and model interfaces
β βββ ITenantContext.cs # Tenant context interface
βββ NAuth/ # NuGet package (DTOs + ACL)
β βββ DTO/ # Data transfer objects
β βββ ACL/ # NAuthHandler, UserClient, RoleClient
βββ NAuth.Test/ # Test suite
β βββ Domain/ # Domain model and service tests
β βββ Infra/ # Repository tests (EF InMemory)
β βββ ACL/ # Auth handler and client tests
β βββ Tenant/ # Tenant resolution tests
βββ docs/ # Documentation
βββ docker-compose-dev.yml # Development (single tenant + PostgreSQL)
βββ docker-compose-prod.yml # Production (multi-tenant, external DB)
βββ Dockerfile # Multi-stage .NET build
βββ postgres.Dockerfile # Dev PostgreSQL with schema
βββ nauth.sql # Database schema
βββ README.md # This file
| Project | Type | Package | Description |
|---|---|---|---|
| nauth-react | NPM | React component library (login, register, user management) |
nauth-react (NPM)
ββ NAuth.API (HTTP) β you are here
ββ NAuth (NuGet - DTOs + ACL)
NAuth supports database-per-tenant isolation. Each tenant has its own PostgreSQL database, JWT signing secret, and S3 bucket name.
Request β TenantMiddleware β Resolve Tenant β TenantDbContextFactory β Tenant Database
- Authenticated requests: Tenant is read from the
tenant_idclaim in the JWT token - Non-authenticated requests: Tenant is read from the
X-Tenant-IdHTTP header - Fallback: Uses
Tenant:DefaultTenantIdfrom configuration - NuGet package (ACL): Uses global
NAuthSetting.JwtSecretβ no tenant awareness
Tenants are configured in appsettings.json (or injected via environment variables in Docker):
{
"Tenant": {
"DefaultTenantId": "emagine"
},
"Tenants": {
"emagine": {
"ConnectionString": "Host=localhost;Port=5432;Database=emagine_db;Username=...",
"JwtSecret": "your_emagine_jwt_secret_at_least_64_characters_long",
"BucketName": "Emagine"
},
"viralt": {
"ConnectionString": "Host=localhost;Port=5432;Database=viralt_db;Username=...",
"JwtSecret": "your_viralt_jwt_secret_at_least_64_characters_long",
"BucketName": "Viralt"
},
"devblog": {
"ConnectionString": "Host=localhost;Port=5432;Database=devblog_db;Username=...",
"JwtSecret": "your_devblog_jwt_secret_at_least_64_characters_long",
"BucketName": "DevBlog"
},
"bazzuca": {
"ConnectionString": "Host=localhost;Port=5432;Database=bazzuca_db;Username=...",
"JwtSecret": "your_bazzuca_jwt_secret_at_least_64_characters_long",
"BucketName": "Bazzuca"
}
}
}| Tenant | BucketName | Description |
|---|---|---|
emagine |
Emagine | Default tenant |
viralt |
Viralt | Viralt platform |
devblog |
DevBlog | DevBlog platform |
bazzuca |
Bazzuca | Bazzuca platform |
| Component | Layer | Responsibility |
|---|---|---|
MultiTenantHandler |
NAuth.API | Resolves JWT secret per tenant for authentication |
TenantMiddleware |
NAuth.API | Reads X-Tenant-Id header, sets ITenantContext |
TenantResolver |
NAuth.API | Reads tenant config from Tenants:{id}:* in appsettings |
TenantDbContextFactory |
NAuth.API | Creates NAuthContext with tenant-specific connection string |
ITenantContext |
NAuth.Infra.Interfaces | Scoped interface exposing current TenantId |
UserService |
NAuth.Domain | Resolves JWT secret and BucketName per tenant (throws if not configured) |
NAuthHandler |
NAuth (package) | Uses global NAuthSetting.JwtSecret (no multi-tenant) |
The following diagram illustrates the high-level architecture of NAuth.API, including the multi-tenant request flow, layer separation, and external service integrations:
- Clients connect via HTTP/REST β either through the
nauth-reactfrontend library or any application using theNAuthNuGet package (DTOs + ACL). - TenantMiddleware intercepts every request, resolving the tenant from the JWT
tenant_idclaim or theX-Tenant-Idheader, and configures the scopedITenantContext. - MultiTenantHandler authenticates requests using the tenant-specific JWT secret.
- Controllers delegate to domain services (
UserService,RoleService), which contain all business logic. - NAuth.Infra provides data access via EF Core 9 repositories and
UnitOfWork, withTenantDbContextFactoryrouting each request to the correct tenant database. - External Services: MailerSend (transactional emails), Amazon S3 (per-tenant file storage), Stripe (payments), and zTools API (image processing).
- PostgreSQL: Each tenant has its own isolated database (
emagine_db,viralt_db,devblog_db,bazzuca_db).
π Source: The editable Mermaid source is available at
docs/system-design.mmd.
| Document | Description |
|---|---|
| USER_API_DOCUMENTATION | Complete User API endpoint reference |
| ROLE_API_DOCUMENTATION | Complete Role API endpoint reference |
| MULTI_TENANT_API | Multi-tenant architecture and configuration guide |
| system-design.mmd | Editable Mermaid source for the system design diagram |
cp .env.example .envEdit the .env file:
# PostgreSQL Container
POSTGRES_DB=emagine_db
POSTGRES_USER=emagine_user
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_PORT=5432
# NAuth API Ports
API_HTTP_PORT=5004
API_HTTPS_PORT=5005
CERTIFICATE_PASSWORD=your_certificate_password_here
# External Services
ZTOOL_API_URL=http://ztools-api:8080cp .env.prod.example .env.prodEdit the .env.prod file:
# HTTPS Certificate
CERTIFICATE_PASSWORD=your_certificate_password_here
# Tenant: emagine
EMAGINE_CONNECTION_STRING=Host=your_db_host;Port=5432;Database=emagine_db;Username=your_user;Password=your_password
EMAGINE_JWT_SECRET=your_emagine_jwt_secret_at_least_64_characters_long
# Tenant: viralt
VIRALT_CONNECTION_STRING=Host=your_db_host;Port=5432;Database=viralt_db;Username=your_user;Password=your_password
VIRALT_JWT_SECRET=your_viralt_jwt_secret_at_least_64_characters_long
# Tenant: devblog
DEVBLOG_CONNECTION_STRING=Host=your_db_host;Port=5432;Database=devblog_db;Username=your_user;Password=your_password
DEVBLOG_JWT_SECRET=your_devblog_jwt_secret_at_least_64_characters_long
# Tenant: bazzuca
BAZZUCA_CONNECTION_STRING=Host=your_db_host;Port=5432;Database=bazzuca_db;Username=your_user;Password=your_password
BAZZUCA_JWT_SECRET=your_bazzuca_jwt_secret_at_least_64_characters_long- Never commit
.envor.env.prodfiles with real credentials - Only
.env.exampleand.env.prod.exampleare version controlled - JWT secrets must be at least 64 characters for HMAC-SHA256
- Production
BucketNamevalues are configured inappsettings.Production.json, not in.env.prod
The project provides two Docker Compose configurations:
| File | Purpose | Database | Tenants |
|---|---|---|---|
docker-compose-dev.yml |
Development | Included (PostgreSQL container) | Single tenant |
docker-compose-prod.yml |
Production | External (connection strings in .env.prod) |
Multi-tenant (emagine + viralt + devblog + bazzuca) |
# Create the external Docker network
docker network create emagine-networkdocker compose -f docker-compose-dev.yml up -d --buildUses postgres.Dockerfile which applies nauth.sql to a single database.
docker compose --env-file .env.prod -f docker-compose-prod.yml up -d --buildProduction uses external databases β no PostgreSQL container is created. Connection strings for each tenant are provided via .env.prod.
The API container receives per-tenant configuration via environment variables:
Tenants__emagine__ConnectionString: ${EMAGINE_CONNECTION_STRING}
Tenants__emagine__JwtSecret: ${EMAGINE_JWT_SECRET}
Tenants__viralt__ConnectionString: ${VIRALT_CONNECTION_STRING}
Tenants__viralt__JwtSecret: ${VIRALT_JWT_SECRET}
Tenants__devblog__ConnectionString: ${DEVBLOG_CONNECTION_STRING}
Tenants__devblog__JwtSecret: ${DEVBLOG_JWT_SECRET}
Tenants__bazzuca__ConnectionString: ${BAZZUCA_CONNECTION_STRING}
Tenants__bazzuca__JwtSecret: ${BAZZUCA_JWT_SECRET}# Check container status
docker compose -f docker-compose-prod.yml ps
# View logs
docker compose -f docker-compose-prod.yml logs -f
# Test health check (from inside the Docker network)
docker exec nauth-api curl -f http://localhost:80/| Service | URL | Notes |
|---|---|---|
| API HTTP | http://localhost:5004 | Dev only (ports exposed) |
| API HTTPS | https://localhost:5005 | Dev only (ports exposed) |
| Swagger UI | http://localhost:5004/swagger | Dev only |
| Health Check | http://localhost/ | Internal (via Docker network) |
| PostgreSQL | localhost:5432 | Dev only |
Production: Ports are not exposed to the host. The API is accessible only through the
emagine-networkDocker network (ports 80/443 internal). Use a reverse proxy (e.g., Nginx) in a separate project to expose the API externally.
| Action | Command |
|---|---|
| Start dev | docker compose -f docker-compose-dev.yml up -d |
| Start prod | docker compose --env-file .env.prod -f docker-compose-prod.yml up -d |
| Start with rebuild | docker compose --env-file .env.prod -f docker-compose-prod.yml up -d --build |
| Stop services | docker compose -f docker-compose-prod.yml stop |
| View status | docker compose -f docker-compose-prod.yml ps |
| View logs | docker compose -f docker-compose-prod.yml logs -f |
| Remove containers | docker compose -f docker-compose-prod.yml down |
| Remove containers and volumes ( |
docker compose -f docker-compose-prod.yml down -v |
- .NET 8.0 SDK
- PostgreSQL 12+
Create one PostgreSQL database per tenant and apply the schema:
psql -U postgres -c "CREATE DATABASE emagine_db;"
psql -U postgres -d emagine_db -f nauth.sql
psql -U postgres -c "CREATE DATABASE viralt_db;"
psql -U postgres -d viralt_db -f nauth.sql
psql -U postgres -c "CREATE DATABASE devblog_db;"
psql -U postgres -d devblog_db -f nauth.sql
psql -U postgres -c "CREATE DATABASE bazzuca_db;"
psql -U postgres -d bazzuca_db -f nauth.sqlUpdate NAuth.API/appsettings.Development.json with your tenant configuration:
{
"Tenant": {
"DefaultTenantId": "emagine"
},
"Tenants": {
"emagine": {
"ConnectionString": "Host=localhost;Port=5432;Database=emagine_db;Username=postgres;Password=your_password",
"JwtSecret": "your_emagine_jwt_secret_at_least_64_characters",
"BucketName": "Emagine"
},
"viralt": {
"ConnectionString": "Host=localhost;Port=5432;Database=viralt_db;Username=postgres;Password=your_password",
"JwtSecret": "your_viralt_jwt_secret_at_least_64_characters",
"BucketName": "Viralt"
},
"devblog": {
"ConnectionString": "Host=localhost;Port=5432;Database=devblog_db;Username=postgres;Password=your_password",
"JwtSecret": "your_devblog_jwt_secret_at_least_64_characters",
"BucketName": "DevBlog"
},
"bazzuca": {
"ConnectionString": "Host=localhost;Port=5432;Database=bazzuca_db;Username=postgres;Password=your_password",
"JwtSecret": "your_bazzuca_jwt_secret_at_least_64_characters",
"BucketName": "Bazzuca"
}
}
}dotnet build
dotnet run --project NAuth.APIThe API will be available at:
- HTTP: http://localhost:5004
- HTTPS: https://localhost:5005
- Swagger: http://localhost:5004/swagger
All Tests:
dotnet testSpecific Test Class:
dotnet test NAuth.Test/NAuth.Test.csproj --filter "FullyQualifiedName~UserServiceTests"With Coverage:
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencoverNAuth.Test/
βββ Domain/
β βββ Models/ # Domain model tests
β βββ Services/ # UserService, RoleService tests
βββ Infra/
β βββ Repository/ # Repository tests (EF InMemory)
βββ ACL/
β βββ NAuthHandlerTests.cs # Auth handler tests
β βββ UserClientTests.cs # User ACL client tests
β βββ RoleClientTests.cs # Role ACL client tests
βββ Tenant/
βββ TenantTests.cs # Tenant resolution and header tests
1. Register β 2. Verify Email β 3. Login (get JWT) β 4. Access Protected Resources
Multi-tenant requests must include the X-Tenant-Id header for non-authenticated endpoints. Authenticated endpoints resolve the tenant from the JWT tenant_id claim.
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /User/register |
Register new user | No |
| POST | /User/login |
Login and get JWT token | No |
| POST | /User/verifyEmail |
Verify email with token | No |
| POST | /User/requestPasswordReset |
Request password reset | No |
| POST | /User/resetPassword |
Reset password with token | No |
| GET | /User/{id} |
Get user profile | Yes |
| PUT | /User/{id} |
Update user profile | Yes |
| POST | /User/changePassword |
Change password | Yes |
| GET | /User/email/{email} |
Get user by email | Yes |
| GET | /Role/list |
List all roles | Yes |
| POST | /Role/assignRole |
Assign role to user | Admin |
| DELETE | /Role/removeRole/{userId}/{roleId} |
Remove role from user | Admin |
| GET | / |
Health check | No |
Full API documentation available in docs/USER_API_DOCUMENTATION.md and docs/ROLE_API_DOCUMENTATION.md.
- JWT Tokens - HMAC-SHA256 signed, per-tenant secrets
- Token Validation - Issuer/audience validation, expiration checks
- Claims - userId, email, roles, hash, tenant_id
- BCrypt Hashing - Strong password hashing with unique salts
- Password Reset - Secure token-based flow with expiration
- Database Isolation - Each tenant has a separate PostgreSQL database
- JWT Isolation - Each tenant has a unique signing secret
- Bucket Isolation - Each tenant has its own S3 bucket
- Request Isolation - Middleware enforces tenant context per request
- Non-root User - Containers run as
appuser - Secret Management - Environment-based secrets via
.env - HTTPS Support - Certificate-based HTTPS
# Backup emagine tenant
pg_dump -h your_db_host -U your_user emagine_db > backup_emagine_$(date +%Y%m%d_%H%M%S).sql
# Backup viralt tenant
pg_dump -h your_db_host -U your_user viralt_db > backup_viralt_$(date +%Y%m%d_%H%M%S).sql
# Backup devblog tenant
pg_dump -h your_db_host -U your_user devblog_db > backup_devblog_$(date +%Y%m%d_%H%M%S).sql
# Backup bazzuca tenant
pg_dump -h your_db_host -U your_user bazzuca_db > backup_bazzuca_$(date +%Y%m%d_%H%M%S).sql
# Compressed backup
pg_dump -h your_db_host -U your_user emagine_db | gzip > backup_emagine_$(date +%Y%m%d_%H%M%S).sql.gzpsql -h your_db_host -U your_user -d emagine_db < backup_emagine_20260310_120000.sqlCheck logs:
docker compose -f docker-compose-prod.yml logs nauth-apiCommon causes:
- Missing
JwtSecretfor a tenant (check env vars) - Database connection string incorrect
- Port already in use
"JwtSecret not found for tenant":
- Ensure
Tenants__{tenantId}__JwtSecretis set in docker-compose environment - Verify tenant ID matches exactly (case-sensitive)
"ConnectionString not found for tenant":
- Ensure
Tenants__{tenantId}__ConnectionStringis set - Verify the external database is accessible
"BucketName not found for tenant":
- Ensure
BucketNameis configured inappsettings.Production.json
curl http://localhost:5004/Common solutions:
- Wait for the API to fully start (check
start_periodin healthcheck) - Check API logs for startup errors
docker compose -f docker-compose-dev.yml up -d --builddocker compose --env-file .env.prod -f docker-compose-prod.yml up -d --buildThe project includes a deploy-prod.yml workflow that deploys to a production server via SSH:
- Trigger: Manual (
workflow_dispatch) - Process:
- Connects to the server via SSH
- Clones/updates the repository at
/opt/nauth - Injects
.env.prodfrom GitHub Secrets - Runs
docker compose --env-file .env.prod -f docker-compose-prod.yml up --build -d
Required GitHub Secrets:
| Secret | Description |
|---|---|
PROD_SSH_HOST |
Server IP/hostname |
PROD_SSH_USER |
SSH username |
PROD_SSH_KEY |
SSH private key |
PROD_SSH_PORT |
SSH port (default 22) |
PROD_CERTIFICATE_PASSWORD |
HTTPS certificate password |
PROD_EMAGINE_CONNECTION_STRING |
Emagine tenant connection string |
PROD_EMAGINE_JWT_SECRET |
Emagine tenant JWT secret |
PROD_VIRALT_CONNECTION_STRING |
Viralt tenant connection string |
PROD_VIRALT_JWT_SECRET |
Viralt tenant JWT secret |
PROD_DEVBLOG_CONNECTION_STRING |
DevBlog tenant connection string |
PROD_DEVBLOG_JWT_SECRET |
DevBlog tenant JWT secret |
PROD_BAZZUCA_CONNECTION_STRING |
Bazzuca tenant connection string |
PROD_BAZZUCA_JWT_SECRET |
Bazzuca tenant JWT secret |
| Workflow | Trigger | Description |
|---|---|---|
| Version and Tag | Push to main | GitVersion semantic versioning and tagging |
| Publish NuGet | After Version and Tag | Publishes NAuth package to NuGet.org |
| Create Release | After Version and Tag | Creates GitHub release with notes |
| Deploy Production | Manual | SSH deploy to production server |
Commit message prefixes control version bumps:
major:orbreaking:β Major version bumpfeat:orfeature:orminor:β Minor version bumpfix:orpatch:β Patch version bump
- Two-Factor Authentication (2FA) - TOTP and SMS support
- OAuth2 Integration - Social login providers (Google, GitHub, Facebook, Microsoft)
- Admin Dashboard - Web interface for user management
- Advanced RBAC - Fine-grained permissions
- Audit Logging - Track user actions
- Rate Limiting - API request throttling
- Session Management - Multiple device support
- Account Lockout - Brute force protection
- Password Policies - Configurable complexity rules
- Localization - Multi-language support
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Make your changes
- Run tests (
dotnet test) - Commit your changes (
git commit -m 'feat: Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Follow C# coding conventions
- Write unit tests for new features
- Update documentation as needed
- Use commit message prefixes for versioning (
feat:,fix:,major:)
Developed by Rodrigo Landim Carneiro
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with .NET 8
- Database powered by PostgreSQL
- Frontend with React
- Containerization with Docker
- Issues: GitHub Issues
- Discussions: GitHub Discussions
β If you find this project useful, please consider giving it a star!
