# 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 net9.0 enable enable ``` **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 { 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 { 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 { public ApplicationDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); builder.Entity(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(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(); }) .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(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); // Identity builder.Services.AddIdentity() .AddEntityFrameworkStores() .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 _signInManager; private readonly UserManager _userManager; private readonly IOpenIddictApplicationManager _applicationManager; private readonly IOpenIddictAuthorizationManager _authorizationManager; private readonly IOpenIddictScopeManager _scopeManager; private readonly ILogger _logger; public AuthController( SignInManager signInManager, UserManager userManager, IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager, ILogger logger) { _signInManager = signInManager; _userManager = userManager; _applicationManager = applicationManager; _authorizationManager = authorizationManager; _scopeManager = scopeManager; _logger = logger; } [HttpPost("login")] public async Task 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 GenerateTokenAsync(ApplicationUser user) { var claims = new List { 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 _signInManager; private readonly UserManager _userManager; private readonly ILogger _logger; public AuthorizationController( IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager, IOpenIddictScopeManager scopeManager, SignInManager signInManager, UserManager userManager, ILogger logger) { _applicationManager = applicationManager; _authorizationManager = authorizationManager; _scopeManager = scopeManager; _signInManager = signInManager; _userManager = userManager; _logger = logger; } [HttpPost("~/connect/token")] [Produces("application/json")] public async Task 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 { [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 { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "用户名或密码错误" })); } var principal = await _signInManager.CreateUserPrincipalAsync(user); var claims = new List { 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 { [OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.UnsupportedGrantType, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "不支持的授权类型" })); } [HttpGet("~/connect/authorize")] [HttpPost("~/connect/authorize")] [IgnoreAntiforgeryToken] public async Task 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 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(); var userManager = scope.ServiceProvider.GetRequiredService>(); var roleManager = scope.ServiceProvider.GetRequiredService>(); // 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.