1024 lines
28 KiB
Markdown
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.
|