18 KiB
IAM Service .NET 10
Identity and Access Management Service built with .NET 10, ASP.NET Core Identity, and Duende IdentityServer following DDD, CQRS, and Clean Architecture patterns.
Overview
This service provides OAuth2/OpenID Connect authentication and authorization:
- OAuth2/OIDC Server - Duende IdentityServer for token management
- User Management - Registration, profile, soft-delete
- Role-Based Access Control - User roles and permissions
- Token Management - Access (15 min), Refresh (7 days) tokens
- Email Verification - SMTP-based email confirmation
- Two-Factor Authentication (2FA) - TOTP with QR code setup
- Social Login - Google and Facebook OAuth integration
- CQRS Pattern - MediatR for Commands/Queries
- Clean Architecture - Domain, Infrastructure, API layers
Tech Stack
| Technology | Purpose |
|---|---|
| .NET 10 | Runtime |
| ASP.NET Core Identity | User/Role management |
| Duende IdentityServer | OAuth2/OIDC server |
| EF Core + PostgreSQL | Data persistence |
| Redis | Distributed caching |
| MediatR | CQRS pattern |
| FluentValidation | Request validation |
| Serilog | Structured logging |
Quick Start
1. Prerequisites
- .NET SDK 10.0.101+
- Docker (for PostgreSQL)
2. Configure Environment
cp .env.example .env
# Edit DATABASE_URL, JWT_SECRET in .env
3. Run with Docker
docker-compose up -d
Service available at: http://localhost:5001
4. Run Locally
dotnet restore
dotnet build
dotnet run --project src/IamService.API
Database Migrations
Prerequisites
# Install EF Core tools (one-time)
dotnet tool install --global dotnet-ef
Create Migration
dotnet ef migrations add <MigrationName> \
--project src/IamService.Infrastructure \
--startup-project src/IamService.API
Apply Migration
dotnet ef database update \
--project src/IamService.Infrastructure \
--startup-project src/IamService.API
Neon Database Setup
- Create database on Neon Console
- Update
appsettings.Development.jsonwith connection string - Run:
dotnet ef database update ...
API Endpoints
Authorization Policies
Note: All API endpoints require authentication (Bearer JWT Token).
Some endpoints require specific roles as shown below.
| Policy | Required Role | Applied To |
|---|---|---|
RequireSuperAdmin |
SuperAdmin | PAM (Privileged Access Management) |
RequireAdmin |
Admin, SuperAdmin | User/Role/Group/Organization management |
RequireAuditor |
Auditor, Admin, SuperAdmin | Audit logs, Compliance reports |
OwnerOrAdmin |
Owner or Admin | User self-service profile management |
Authorization by Controller:
| Controller | Policy | Description |
|---|---|---|
| Users (GET /users, DELETE) | RequireAdmin | List users, delete user |
| Users (GET/PUT /{id}) | OwnerOrAdmin | User access own profile or Admin access any |
| Roles | RequireAdmin | Role management |
| Organizations | RequireAdmin | Organization management |
| Groups | RequireAdmin | Group management |
| Access Requests | RequireAdmin | Access request workflow |
| Access Reviews | RequireAdmin | Periodic access review |
| Privileged Access | RequireSuperAdmin | PAM - most sensitive |
| Audit | RequireAuditor | View audit logs |
| Compliance | RequireAuditor | Compliance reports |
| Verifications | RequireAdmin | Identity verification |
Authentication (/api/v1/auth)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/auth/register |
Register new user | ❌ |
POST |
/connect/token |
OAuth2 token endpoint | ❌ |
POST |
/api/v1/auth/change-password |
Change password | ✅ |
POST |
/api/v1/auth/logout |
Logout (revoke tokens) | ✅ |
Email Verification (/api/v1/auth)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/auth/send-verification-email |
Send email verification link | ✅ |
POST |
/api/v1/auth/confirm-email |
Confirm email with token | ❌ |
Two-Factor Authentication (/api/v1/auth/2fa)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/auth/2fa/enable |
Enable 2FA (get QR code) | ✅ |
POST |
/api/v1/auth/2fa/verify |
Verify TOTP code & activate | ✅ |
POST |
/api/v1/auth/2fa/disable |
Disable 2FA | ✅ |
Social Login (/api/v1/auth)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/v1/auth/external-login/{provider} |
Initiate OAuth flow (Google/Facebook) | ❌ |
GET |
/api/v1/auth/external-callback |
Handle OAuth callback | ❌ |
GET |
/api/v1/auth/linked-accounts |
Get linked OAuth providers | ✅ |
User Management (/api/v1/users)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/v1/users |
List users (paginated) | ✅ |
GET |
/api/v1/users/me |
Get current user | ✅ |
GET |
/api/v1/users/{id} |
Get user by ID | ✅ |
PUT |
/api/v1/users/{id} |
Update user | ✅ |
DELETE |
/api/v1/users/{id} |
Delete user (soft) | ✅ |
Health Endpoints
| Endpoint | Purpose |
|---|---|
/health |
Full health status |
/health/live |
Liveness probe |
/health/ready |
Readiness probe |
Organizations (/api/v1/organizations) - Phase 2
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/v1/organizations/{id} |
Get organization by ID | ✅ |
GET |
/api/v1/organizations/slug/{slug} |
Get organization by slug | ✅ |
POST |
/api/v1/organizations |
Create organization | ✅ |
PUT |
/api/v1/organizations/{id} |
Update organization | ✅ |
DELETE |
/api/v1/organizations/{id} |
Archive organization | ✅ |
GET |
/api/v1/organizations/{id}/hierarchy |
Get hierarchy | ✅ |
GET |
/api/v1/organizations/{id}/children |
Get child orgs | ✅ |
Groups (/api/v1/groups) - Phase 2
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/v1/groups |
List groups | ✅ |
GET |
/api/v1/groups/{id} |
Get group by ID | ✅ |
POST |
/api/v1/groups |
Create group | ✅ |
DELETE |
/api/v1/groups/{id} |
Delete group | ✅ |
POST |
/api/v1/groups/{id}/members |
Add member | ✅ |
DELETE |
/api/v1/groups/{id}/members/{userId} |
Remove member | ✅ |
Access Requests (/api/v1/access-requests) - Phase 3A
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/access-requests |
Create access request | ✅ |
GET |
/api/v1/access-requests |
List requests | ✅ |
GET |
/api/v1/access-requests/{id} |
Get request by ID | ✅ |
POST |
/api/v1/access-requests/{id}/submit |
Submit request | ✅ |
POST |
/api/v1/access-requests/{id}/approve |
Approve | ✅ |
POST |
/api/v1/access-requests/{id}/reject |
Reject | ✅ |
DELETE |
/api/v1/access-requests/{id} |
Cancel request | ✅ |
GET |
/api/v1/access-requests/pending |
Pending requests | ✅ |
Access Reviews (/api/v1/access-reviews) - Phase 3B
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/access-reviews |
Create access review | ✅ |
GET |
/api/v1/access-reviews/{id} |
Get review by ID | ✅ |
POST |
/api/v1/access-reviews/{id}/items |
Add item | ✅ |
POST |
/api/v1/access-reviews/{id}/start |
Start review | ✅ |
POST |
/api/v1/access-reviews/{id}/items/{itemId}/review |
Certify/Revoke | ✅ |
POST |
/api/v1/access-reviews/{id}/complete |
Complete | ✅ |
Privileged Access (/api/v1/privileged-access) - Phase 3B PAM
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/privileged-access/request |
Request JIT access | ✅ |
GET |
/api/v1/privileged-access/active |
Active grants | ✅ |
POST |
/api/v1/privileged-access/{id}/revoke |
Revoke access | ✅ |
Audit (/api/v1/audit) - Phase 4A
| Method | Endpoint | Description | Auth |
|---|---|---|---|
GET |
/api/v1/audit/logs |
Get audit logs (filtered) | ✅ |
Compliance (/api/v1/compliance) - Phase 4A
| Method | Endpoint | Description | Auth |
|---|---|---|---|
POST |
/api/v1/compliance/reports |
Generate report | ✅ |
GET |
/api/v1/compliance/reports |
List reports | ✅ |
GET |
/api/v1/compliance/reports/{id} |
Report detail | ✅ |
POST |
/api/v1/compliance/reports/{id}/complete |
Complete report | ✅ |
GET |
/api/v1/compliance/violations |
Unresolved violations | ✅ |
Authentication Flow
Step 1: Register a New User
curl -X POST http://localhost:5001/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "Password123!",
"firstName": "John",
"lastName": "Doe",
"phoneNumber": "+84901234567"
}'
Response:
{
"success": true,
"data": {
"userId": "8083b412-5040-4b74-baf0-5cb709861957",
"email": "user@example.com",
"fullName": "John Doe"
},
"error": null,
"pagination": null
}
Step 2: Login (Password Grant)
curl -X POST http://localhost:5001/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "client_id=password-client" \
-d "client_secret=password-client-secret" \
-d "username=user@example.com" \
-d "password=Password123!" \
-d "scope=openid profile email api offline_access"
Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjlGMzk1RDQzMDIz...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "5B8FB8189575241FF63C32ED2D0D492B32DA533B...",
"scope": "api email offline_access openid profile"
}
Step 3: Use Access Token
curl http://localhost:5001/api/v1/users/me \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..."
Step 4: Refresh Token
curl -X POST http://localhost:5001/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=eyJhbGciOiJSUzI1NiIs..."
Step 5: Logout
curl -X POST http://localhost:5001/api/v1/auth/logout \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Client Credentials (Service-to-Service)
For service-to-service authentication without user context:
curl -X POST http://localhost:5001/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=goodgo-service" \
-d "client_secret=service-secret" \
-d "scope=api"
Supported OAuth2 Grant Types
| Grant Type | Use Case | Requires User |
|---|---|---|
password |
User login from trusted apps | Yes |
refresh_token |
Token renewal | No (uses refresh token) |
client_credentials |
Service-to-service | No |
Email Verification
Send Verification Email
curl -X POST http://localhost:5001/api/v1/auth/send-verification-email \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
Response:
{
"success": true,
"data": {
"success": true,
"message": "Verification email sent successfully."
},
"error": null,
"pagination": null
}
Confirm Email
curl -X POST http://localhost:5001/api/v1/auth/confirm-email \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "token": "confirmation-token-from-email"}'
Two-Factor Authentication (2FA)
Enable 2FA
curl -X POST http://localhost:5001/api/v1/auth/2fa/enable \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response:
{
"success": true,
"data": {
"qrCodeBase64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAooAAAKK...",
"manualEntryKey": "3R36NBSHHFGRTZLMNSOH5Q2FPETPRYMP",
"recoveryCodes": [
"KEJF-MPAY",
"URMF-YRVX",
"GPK4-G9WX",
"LS34-FNP8",
"WRM0-WAML",
"QY5W-VVER",
"AIEU-GLD6",
"FDJL-3HNY",
"J1MC-JXXT",
"8GVA-OAAM"
]
},
"error": null,
"pagination": null
}
Verify 2FA Code
curl -X POST http://localhost:5001/api/v1/auth/2fa/verify \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
Disable 2FA
curl -X POST http://localhost:5001/api/v1/auth/2fa/disable \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"code": "123456"}'
Social Login
Initiate OAuth Flow
Redirect user to:
GET http://localhost:5001/api/v1/auth/external-login/Google?returnUrl=http://your-app/callback
GET http://localhost:5001/api/v1/auth/external-login/Facebook?returnUrl=http://your-app/callback
Get Linked Accounts
curl http://localhost:5001/api/v1/auth/linked-accounts \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response:
{
"success": true,
"data": {
"linkedProviders": [
{"provider": "Google", "providerDisplayName": "Google"},
{"provider": "Facebook", "providerDisplayName": "Facebook"}
]
}
}
Configuration
Environment Variables
| Variable | Description | Required |
|---|---|---|
ASPNETCORE_ENVIRONMENT |
Environment | No (default: Development) |
DATABASE_URL |
PostgreSQL connection | Yes |
JWT_SECRET |
JWT signing secret (32+ chars) | Yes |
REDIS_HOST |
Redis server host | No (default: localhost) |
REDIS_PORT |
Redis server port | No (default: 6379) |
REDIS_PASSWORD |
Redis password | No |
REDIS_DATABASE |
Redis database number | No (default: 0) |
Token Lifetimes
| Token | Lifetime |
|---|---|
| Access Token | 15 minutes |
| Refresh Token | 7 days |
Redis Caching
The service uses Redis for distributed caching with the ICacheService interface.
Configuration
Add Redis settings in appsettings.json:
{
"Redis": {
"Host": "localhost",
"Port": 6379,
"Password": "",
"Database": 0,
"ConnectTimeout": 5000,
"SyncTimeout": 5000
}
}
Or use environment variables:
REDIS_HOST=your-redis-host
REDIS_PORT=6379
REDIS_PASSWORD=your-password
REDIS_DATABASE=0
ICacheService Interface
public interface ICacheService
{
// Basic operations
Task<T?> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
Task RemoveAsync(string key);
Task<bool> ExistsAsync(string key);
// Get or create pattern
Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiration = null);
// Token blacklist support
Task BlacklistAsync(string key, TimeSpan expiration);
Task<bool> IsBlacklistedAsync(string key);
}
Usage Examples
Basic Get/Set:
// Inject ICacheService
public class MyService
{
private readonly ICacheService _cache;
public MyService(ICacheService cache) => _cache = cache;
public async Task<User?> GetUser(string userId)
{
return await _cache.GetAsync<User>($"user:{userId}");
}
public async Task CacheUser(User user)
{
await _cache.SetAsync($"user:{user.Id}", user, TimeSpan.FromMinutes(15));
}
}
Get or Set Pattern (Cache-Aside):
public async Task<User> GetUserById(string userId)
{
return await _cache.GetOrSetAsync(
$"user:{userId}",
async () => await _repository.GetByIdAsync(userId),
TimeSpan.FromMinutes(15)
);
}
Token Blacklisting (for Logout):
public async Task Logout(string tokenId)
{
// Blacklist the refresh token for its remaining lifetime
await _cache.BlacklistAsync($"token:{tokenId}", TimeSpan.FromDays(7));
}
public async Task<bool> IsTokenRevoked(string tokenId)
{
return await _cache.IsBlacklistedAsync($"token:{tokenId}");
}
### Password Policy
- Minimum 8 characters
- Requires: uppercase, lowercase, digit, special character
## Project Structure
iam-service-net/ ├── src/ │ ├── IamService.API/ # Controllers, CQRS │ │ ├── Controllers/ # AuthController, UsersController │ │ └── Application/ # Commands, Queries, Validations │ ├── IamService.Domain/ # Domain entities │ │ ├── AggregatesModel/ # UserAggregate, RoleAggregate │ │ ├── Events/ # Domain events │ │ └── Exceptions/ # Domain exceptions │ └── IamService.Infrastructure/ # Data access │ ├── IamServiceContext.cs # DbContext with Identity │ └── Repositories/ # Repository implementations ├── tests/ │ ├── IamService.UnitTests/ │ └── IamService.FunctionalTests/ ├── docs/ │ ├── en/ # English documentation │ └── vi/ # Vietnamese documentation ├── Dockerfile └── docker-compose.yml
## Swagger UI
After running the service, access Swagger UI at:
- **Local**: http://localhost:5001/swagger
- **Docker**: http://localhost/api/v1/iam/swagger
## Testing
```bash
# Run all tests
dotnet test
# Run with coverage
dotnet test /p:CollectCoverage=true
Docker
# Build image
docker build -t goodgo/iam-service:latest .
# Run container
docker run -p 5001:8080 --env-file .env goodgo/iam-service:latest
Resources
License
Proprietary - GoodGo Platform