feat: add api security foundation

This commit is contained in:
xiong
2026-07-26 12:46:42 +08:00
parent f31977ef9a
commit e8b03c57bc
20 changed files with 436 additions and 23 deletions

View File

@@ -0,0 +1,90 @@
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using System.Security.Claims;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.IdentityModel.Tokens;
using Tiku.Api.Options;
using Tiku.Application.Security;
using Tiku.Domain.Tenancy;
namespace Tiku.IntegrationTests.Api;
public sealed class SecurityFoundationTests
{
private static readonly JwtOptions JwtOptions = new()
{
Issuer = "tiku-backend",
Audience = "tiku-api",
SigningKey = "development-only-tiku-signing-key-change-before-production"
};
[Fact]
public async Task Authenticated_policy_returns_unauthorized_without_token()
{
await using var factory = CreateFactory();
using var client = factory.CreateClient();
var response = await client.GetAsync("/api/_security/authenticated");
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Fact]
public async Task Tenant_admin_policy_returns_forbidden_for_non_admin_member()
{
await using var factory = CreateFactory();
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new(
"Bearer",
CreateToken([
new Claim(TikuClaimTypes.UserId, Guid.NewGuid().ToString()),
new Claim(TikuClaimTypes.TenantId, Guid.NewGuid().ToString()),
new Claim(TikuClaimTypes.TenantRole, TenantRole.Student.ToString())
]));
var response = await client.GetAsync("/api/_security/tenant-admin");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
[Fact]
public async Task Controller_pipeline_loads_authenticated_current_user()
{
var userId = Guid.NewGuid();
await using var factory = CreateFactory();
using var client = factory.CreateClient();
client.DefaultRequestHeaders.Authorization = new(
"Bearer",
CreateToken([
new Claim(TikuClaimTypes.UserId, userId.ToString())
]));
var response = await client.GetAsync("/api/_security/authenticated");
var body = await response.Content.ReadAsStringAsync();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains(userId.ToString(), body, StringComparison.OrdinalIgnoreCase);
Assert.Contains("true", body, StringComparison.OrdinalIgnoreCase);
}
private static WebApplicationFactory<Program> CreateFactory()
{
return new WebApplicationFactory<Program>();
}
private static string CreateToken(IEnumerable<Claim> claims)
{
var credentials = new SigningCredentials(
JwtOptions.CreateSecurityKey(),
SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
JwtOptions.Issuer,
JwtOptions.Audience,
claims,
expires: DateTime.UtcNow.AddMinutes(5),
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}

View File

@@ -22,6 +22,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="xunit" />
<PackageReference Include="xunit.runner.visualstudio">
@@ -30,4 +31,4 @@
</PackageReference>
</ItemGroup>
</Project>
</Project>