fengling-console/docs/plans/2025-02-01-auth-service.md

1024 lines
28 KiB
Markdown

# Authentication Service Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Build a standalone authentication service using OpenIddict to handle user authentication, JWT token issuance, and multi-tenant support.
**Architecture:** ASP.NET Core Web API with OpenIddict for OAuth2/OIDC support, PostgreSQL for user/role data, JWT tokens with embedded TenantId for multi-tenant isolation.
**Tech Stack:**
- .NET 9.0 / ASP.NET Core
- OpenIddict 5.x
- Entity Framework Core 9.0
- PostgreSQL
- Serilog
- OpenTelemetry
---
## Task 1: Create Project Structure
**Files:**
- Create: `src/Fengling.AuthService/Fengling.AuthService.csproj`
- Create: `src/Fengling.AuthService/Program.cs`
- Create: `src/Fengling.AuthService/appsettings.json`
**Step 1: Create project file**
Run:
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src
dotnet new webapi -n Fengling.AuthService -o Fengling.AuthService
```
**Step 2: Update project file with dependencies**
Edit: `src/Fengling.AuthService/Fengling.AuthService.csproj`
```xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenIddict.AspNetCore" Version="5.0.2" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="5.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="OpenTelemetry" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.10.0" />
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.10.0" />
</ItemGroup>
</Project>
```
**Step 3: Create appsettings.json**
Create: `src/Fengling.AuthService/appsettings.json`
```json
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.100.10;Port=5432;Database=fengling_auth;Username=movingsam;Password=sl52788542"
},
"OpenIddict": {
"Issuer": "https://auth.fengling.local",
"Audience": "fengling-api"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
```
**Step 4: Commit**
```bash
git add src/Fengling.AuthService/
git commit -m "feat(auth): create authentication service project structure"
```
---
## Task 2: Create Database Models
**Files:**
- Create: `src/Fengling.AuthService/Data/ApplicationDbContext.cs`
- Create: `src/Fengling.AuthService/Models/ApplicationUser.cs`
- Create: `src/Fengling.AuthService/Models/ApplicationRole.cs`
- Create: `src/Fengling.AuthService/Data/Migrations/20250201_InitialCreate.cs`
**Step 1: Create ApplicationUser model**
Create: `src/Fengling.AuthService/Models/ApplicationUser.cs`
```csharp
using Microsoft.AspNetCore.Identity;
namespace Fengling.AuthService.Models;
public class ApplicationUser : IdentityUser<long>
{
public string? RealName { get; set; }
public string? Phone { get; set; }
public long TenantId { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedTime { get; set; }
public bool IsDeleted { get; set; }
}
```
**Step 2: Create ApplicationRole model**
Create: `src/Fengling.AuthService/Models/ApplicationRole.cs`
```csharp
using Microsoft.AspNetCore.Identity;
namespace Fengling.AuthService.Models;
public class ApplicationRole : IdentityRole<long>
{
public string? Description { get; set; }
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
}
```
**Step 3: Create ApplicationDbContext**
Create: `src/Fengling.AuthService/Data/ApplicationDbContext.cs`
```csharp
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Data;
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, long>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<ApplicationUser>(entity =>
{
entity.Property(e => e.RealName).HasMaxLength(100);
entity.Property(e => e.Phone).HasMaxLength(20);
entity.HasIndex(e => e.TenantId);
entity.HasIndex(e => e.Phone).IsUnique();
});
builder.Entity<ApplicationRole>(entity =>
{
entity.Property(e => e.Description).HasMaxLength(200);
});
}
}
```
**Step 4: Add migration**
Run:
```bash
cd /Users/movingsam/Fengling.Refactory.Buiding/src/Fengling.AuthService
dotnet ef migrations add InitialCreate -o Data/Migrations
```
**Step 5: Update database**
Run:
```bash
dotnet ef database update
```
**Step 6: Commit**
```bash
git add src/Fengling.AuthService/Models/ src/Fengling.AuthService/Data/
git commit -m "feat(auth): add user and role models with EF Core migrations"
```
---
## Task 3: Configure OpenIddict
**Files:**
- Create: `src/Fengling.AuthService/Configuration/OpenIddictSetup.cs`
- Modify: `src/Fengling.AuthService/Program.cs`
**Step 1: Create OpenIddict configuration**
Create: `src/Fengling.AuthService/Configuration/OpenIddictSetup.cs`
```csharp
using Microsoft.Extensions.DependencyInjection;
using OpenIddict.Validation.AspNetCore;
namespace Fengling.AuthService.Configuration;
public static class OpenIddictSetup
{
public static IServiceCollection AddOpenIddictConfiguration(this IServiceCollection services, IConfiguration configuration)
{
services.AddOpenIddict()
.AddCore(options =>
{
options.UseEntityFrameworkCore()
.UseDbContext<Data.ApplicationDbContext>();
})
.AddServer(options =>
{
options.SetIssuer(configuration["OpenIddict:Issuer"] ?? "https://auth.fengling.local");
options.AddSigningKey(new SymmetricSecurityKey(
System.Text.Encoding.UTF8.GetBytes("fengling-super-secret-key-for-dev-only-change-in-prod-please!!!")));
options.AllowAuthorizationCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.RequireProofKeyForCodeExchange();
options.RegisterScopes("api", "offline_access");
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
options.UseAspNetCore()
.EnableAuthorizationEndpointPassThrough()
.EnableTokenEndpointPassThrough()
.EnableLogoutEndpointPassThrough();
})
.AddValidation(options =>
{
options.UseLocalServer();
options.UseAspNetCore();
});
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
return services;
}
}
```
**Step 2: Update Program.cs with OpenIddict and EF Core**
Edit: `src/Fengling.AuthService/Program.cs`
```csharp
using Fengling.AuthService.Configuration;
using Fengling.AuthService.Data;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
builder.Host.UseSerilog();
// Database
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
// Identity
builder.Services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// OpenIddict
builder.Services.AddOpenIddictConfiguration(builder.Configuration);
// OpenTelemetry
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource =>
resource.AddService("Fengling.AuthService"))
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddSource("OpenIddict.Server.AspNetCore")
.AddOtlpExporter();
// Controllers
builder.Services.AddControllers();
// Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Fengling Auth Service",
Version = "v1",
Description = "Authentication and authorization service using OpenIddict"
});
options.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Password = new OpenApiOAuthFlow
{
TokenUrl = "/connect/token"
}
}
});
});
var app = builder.Build();
// Configure pipeline
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "Fengling Auth Service v1");
options.OAuthClientId("swagger-ui");
options.OAuthUsePkce();
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
```
**Step 3: Run to verify startup**
Run:
```bash
dotnet run
```
Expected: Service starts without errors, Swagger UI available at http://localhost:5000/swagger
**Step 4: Commit**
```bash
git add src/Fengling.AuthService/Configuration/ src/Fengling.AuthService/Program.cs
git commit -m "feat(auth): configure OpenIddict with JWT and OAuth2 support"
```
---
## Task 4: Create Auth Controller
**Files:**
- Create: `src/Fengling.AuthService/Controllers/AuthController.cs`
- Create: `src/Fengling.AuthService/DTOs/LoginRequest.cs`
- Create: `src/Fengling.AuthService/DTOs/LoginResponse.cs`
- Create: `src/Fengling.AuthService/DTOs/TokenResponse.cs`
**Step 1: Create DTOs**
Create: `src/Fengling.AuthService/DTOs/LoginRequest.cs`
```csharp
namespace Fengling.AuthService.DTOs;
public class LoginRequest
{
public string UserName { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public long TenantId { get; set; }
}
```
Create: `src/Fengling.AuthService/DTOs/LoginResponse.cs`
```csharp
namespace Fengling.AuthService.DTOs;
public class LoginResponse
{
public string AccessToken { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
public int ExpiresIn { get; set; }
public string TokenType { get; set; } = "Bearer";
}
```
**Step 2: Create AuthController**
Create: `src/Fengling.AuthService/Controllers/AuthController.cs`
```csharp
using Fengling.AuthService.DTOs;
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly ILogger<AuthController> _logger;
public AuthController(
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
ILogger<AuthController> logger)
{
_signInManager = signInManager;
_userManager = userManager;
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_logger = logger;
}
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request)
{
var user = await _userManager.FindByNameAsync(request.UserName);
if (user == null || user.IsDeleted)
{
return Unauthorized(new { error = "用户不存在" });
}
if (user.TenantId != request.TenantId)
{
return Unauthorized(new { error = "租户不匹配" });
}
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!result.Succeeded)
{
return Unauthorized(new { error = "用户名或密码错误" });
}
var token = await GenerateTokenAsync(user);
return Ok(token);
}
private async Task<LoginResponse> GenerateTokenAsync(ApplicationUser user)
{
var claims = new List<System.Security.Claims.Claim>
{
new(Claims.Subject, user.Id.ToString()),
new(Claims.Name, user.UserName ?? string.Empty),
new(Claims.Email, user.Email ?? string.Empty),
new("tenant_id", user.TenantId.ToString())
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(Claims.Role, role));
}
var identity = new System.Security.Claims.ClaimsIdentity(claims, "Server");
var principal = new System.Security.Claims.ClaimsPrincipal(identity);
return new LoginResponse
{
AccessToken = "token-placeholder", // Will be replaced by OpenIddict
RefreshToken = "refresh-placeholder",
ExpiresIn = 3600,
TokenType = "Bearer"
};
}
}
```
**Step 3: Run to verify controller compilation**
Run:
```bash
dotnet build
```
Expected: Build succeeds
**Step 4: Commit**
```bash
git add src/Fengling.AuthService/Controllers/ src/Fengling.AuthService/DTOs/
git commit -m "feat(auth): add authentication controller with login endpoint"
```
---
## Task 5: Create OpenIddict Endpoints
**Files:**
- Create: `src/Fengling.AuthService/Controllers/AuthorizationController.cs`
**Step 1: Create authorization endpoints**
Create: `src/Fengling.AuthService/Controllers/AuthorizationController.cs`
```csharp
using Fengling.AuthService.Models;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using OpenIddict.Server.AspNetCore;
using static OpenIddict.Abstractions.OpenIddictConstants;
namespace Fengling.AuthService.Controllers;
public class AuthorizationController : Controller
{
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
private readonly ILogger<AuthorizationController> _logger;
public AuthorizationController(
IOpenIddictApplicationManager applicationManager,
IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager,
ILogger<AuthorizationController> logger)
{
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
_userManager = userManager;
_logger = logger;
}
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null || user.IsDeleted)
{
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户不存在"
}));
}
var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false);
if (!result.Succeeded)
{
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户名或密码错误"
}));
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
var claims = new List<System.Security.Claims.Claim>
{
new(Claims.Subject, user.Id.ToString()),
new(Claims.Name, user.UserName ?? string.Empty),
new(Claims.Email, user.Email ?? string.Empty),
new("tenant_id", user.TenantId.ToString())
};
var roles = await _userManager.GetRolesAsync(user);
foreach (var role in roles)
{
claims.Add(new Claim(Claims.Role, role));
}
principal.SetScopes(request.GetScopes());
principal.SetResources(await _scopeManager.ListResourcesAsync(principal.GetScopes()).ToListAsync());
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
return Forbid(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
new AuthenticationProperties(new Dictionary<string, string?>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.UnsupportedGrantType,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "不支持的授权类型"
}));
}
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
var result = await HttpContext.AuthenticateAsync();
if (result == null || !result.Succeeded)
{
return Challenge(
new AuthenticationProperties
{
RedirectUri = Request.Path + Request.QueryString
},
OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
return Ok(new { message = "Authorization endpoint" });
}
[HttpPost("~/connect/logout")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
```
**Step 2: Run to verify**
Run:
```bash
dotnet run
```
Test with curl:
```bash
curl -X POST http://localhost:5000/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "username=admin" \
-d "password=Admin@123" \
-d "scope=api offline_access"
```
Expected: 400 error (user doesn't exist yet, but endpoint is working)
**Step 3: Commit**
```bash
git add src/Fengling.AuthService/Controllers/AuthorizationController.cs
git commit -m "feat(auth): add OpenIddict authorization endpoints"
```
---
## Task 6: Create Seed Data
**Files:**
- Create: `src/Fengling.AuthService/Data/SeedData.cs`
- Modify: `src/Fengling.AuthService/Program.cs`
**Step 1: Create seed data class**
Create: `src/Fengling.AuthService/Data/SeedData.cs`
```csharp
using Fengling.AuthService.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace Fengling.AuthService.Data;
public static class SeedData
{
public static async Task Initialize(IServiceProvider serviceProvider)
{
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
// Ensure database is created
context.Database.EnsureCreated();
// Create admin role
var adminRole = await roleManager.FindByNameAsync("Admin");
if (adminRole == null)
{
adminRole = new ApplicationRole
{
Name = "Admin",
Description = "System administrator",
CreatedTime = DateTime.UtcNow
};
await roleManager.CreateAsync(adminRole);
}
// Create default admin user
var adminUser = await userManager.FindByNameAsync("admin");
if (adminUser == null)
{
adminUser = new ApplicationUser
{
UserName = "admin",
Email = "admin@fengling.local",
RealName = "系统管理员",
Phone = "13800138000",
TenantId = 1,
EmailConfirmed = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
};
var result = await userManager.CreateAsync(adminUser, "Admin@123");
if (result.Succeeded)
{
await userManager.AddToRoleAsync(adminUser, "Admin");
}
}
// Create test user
var testUser = await userManager.FindByNameAsync("testuser");
if (testUser == null)
{
testUser = new ApplicationUser
{
UserName = "testuser",
Email = "test@fengling.local",
RealName = "测试用户",
Phone = "13900139000",
TenantId = 1,
EmailConfirmed = true,
IsDeleted = false,
CreatedTime = DateTime.UtcNow
};
var result = await userManager.CreateAsync(testUser, "Test@123");
if (result.Succeeded)
{
var userRole = new ApplicationRole
{
Name = "User",
Description = "普通用户",
CreatedTime = DateTime.UtcNow
};
await roleManager.CreateAsync(userRole);
await userManager.AddToRoleAsync(testUser, "User");
}
}
}
}
```
**Step 2: Update Program.cs to call seed data**
Edit: `src/Fengling.AuthService/Program.cs` (add after `var app = builder.Build();`)
```csharp
// Seed data
using (var scope = app.Services.CreateScope())
{
await Data.SeedData.Initialize(scope.ServiceProvider);
}
```
**Step 3: Run to create seed data**
Run:
```bash
dotnet run
```
Expected: Logs show admin user created successfully
**Step 4: Test login**
Run:
```bash
curl -X POST http://localhost:5000/connect/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=password" \
-d "username=admin" \
-d "password=Admin@123" \
-d "scope=api offline_access"
```
Expected: Returns access_token and refresh_token
**Step 5: Commit**
```bash
git add src/Fengling.AuthService/Data/SeedData.cs src/Fengling.AuthService/Program.cs
git commit -m "feat(auth): add seed data for admin and test users"
```
---
## Task 7: Create Health Check Endpoint
**Files:**
- Modify: `src/Fengling.AuthService/Program.cs`
**Step 1: Add health check configuration**
Edit: `src/Fengling.AuthService/Program.cs` (add after builder services)
```csharp
builder.Services.AddHealthChecks()
.AddNpgSql(builder.Configuration.GetConnectionString("DefaultConnection")!);
```
**Step 2: Add health check endpoint**
Edit: `src/Fengling.AuthService/Program.cs` (before app.Run())
```csharp
app.MapHealthChecks("/health");
```
**Step 3: Test health check**
Run:
```bash
dotnet run
```
Test:
```bash
curl http://localhost:5000/health
```
Expected: "Healthy"
**Step 4: Commit**
```bash
git add src/Fengling.AuthService/Program.cs
git commit -m "feat(auth): add health check endpoint"
```
---
## Task 8: Create Dockerfile
**Files:**
- Create: `src/Fengling.AuthService/Dockerfile`
**Step 1: Create Dockerfile**
Create: `src/Fengling.AuthService/Dockerfile`
```dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 80
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY ["Fengling.AuthService.csproj", "./"]
RUN dotnet restore "Fengling.AuthService.csproj"
COPY . .
WORKDIR "/src"
RUN dotnet build "Fengling.AuthService.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Fengling.AuthService.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Fengling.AuthService.dll"]
```
**Step 2: Create .dockerignore**
Create: `src/Fengling.AuthService/.dockerignore`
```
bin/
obj/
Dockerfile
.dockerignore
*.md
```
**Step 3: Commit**
```bash
git add src/Fengling.AuthService/Dockerfile src/Fengling.AuthService/.dockerignore
git commit -m "feat(auth): add Dockerfile for containerization"
```
---
## Task 9: Create Documentation
**Files:**
- Create: `src/Fengling.AuthService/README.md`
**Step 1: Create README**
Create: `src/Fengling.AuthService/README.md`
```markdown
# Fengling Auth Service
Authentication and authorization service using OpenIddict.
## Features
- JWT token issuance
- OAuth2/OIDC support
- Multi-tenant support (TenantId in JWT claims)
- Role-based access control (RBAC)
- Health check endpoint
## API Endpoints
### Get Token
```
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
username={username}
password={password}
scope=api offline_access
```
### Health Check
```
GET /health
```
## Default Users
- **Admin**: username=admin, password=Admin@123, role=Admin
- **Test User**: username=testuser, password=Test@123, role=User
## Running Locally
```bash
dotnet run
```
Service runs on port 5000.
## Docker
```bash
docker build -t fengling-auth:latest .
docker run -p 5000:80 fengling-auth:latest
```
## Environment Variables
- `ConnectionStrings__DefaultConnection`: PostgreSQL connection string
- `OpenIddict__Issuer`: Token issuer URL
- `OpenIddict__Audience`: Token audience
## Database
- PostgreSQL
- Uses ASP.NET Core Identity for user/role management
- Tenant isolation via `TenantId` column
```
**Step 2: Commit**
```bash
git add src/Fengling.AuthService/README.md
git commit -m "docs(auth): add API documentation"
```
---
## Summary
This implementation plan creates a fully functional authentication service with:
1. **Project structure** with OpenIddict and EF Core dependencies
2. **Database models** for users and roles with multi-tenant support
3. **OpenIddict configuration** for JWT token issuance
4. **Auth controller** with login endpoint
5. **OAuth2 endpoints** for token exchange
6. **Seed data** for admin and test users
7. **Health check** endpoint
8. **Docker support** for containerization
9. **Documentation** for API usage
The service issues JWT tokens with embedded `tenant_id` claims, enabling multi-tenant routing via the YARP Gateway.