我正在开发一个多租户 SPA 应用程序,该应用程序调用后端 .Net Core Web API 来获取数据。前端 UI 将使用 MSAL 和 Microsoft 的 v2 公共端点根据 AAD 对用户进行身份验证并获取 ID 和访问令牌。
在我的 Web API 中,我想验证颁发者,但如此处所述,使用通用端点提供的元数据使正常的颁发者验证无法使用。
我已经看到了对几个地方的参考,在这些地方可以覆盖或自定义令牌验证,但我不确定哪个是首选,或者这些方法中的任何一个是否会导致不良的副作用。
一种方法使用 JwtBearer 选项的事件:
options.Events.TokenValidated
,另一种方法使用 TokenValidationParameters 的 IssuerValidator
委托。
除了确保发行者存在于经过验证的发行者数据库中之外,我不想编写任何令牌验证逻辑。该逻辑应该放在
IssuerValidator
还是 TokenValidated
中?
我当前的代码如下所示(当前为单个租户设置)
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://myauthority.com";
options.Audience = "https://myaudience.com/api/v1";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "myauthority.com",
ValidateAudience = true,
ValidAudience = "https://myaudience.com",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
};
});
我在使用
IssuerValidator
时看到的问题之一是,似乎没有一种方法可以注入或传递对 dbContext 的引用,以便能够在数据库中查找租户 ID。
有人解决过这个问题或做过类似的事情吗?
您可以在
OnTokenValidated
事件中检查,要访问数据库上下文,您可以尝试:
options.Events.OnTokenValidated = async (context) =>
{
var dbContext = context.HttpContext.RequestServices.GetRequiredService<BloggingContext>();
var blogs = await dbContext.Blogs.ToListAsync();
if (!true)
{
throw new SecurityTokenValidationException($"Tenant xxxxx is not registered");
}
};
哇,这让我走上了很长的路!正如您所指出的,大多数文档都指向设置
ValidateIssuer = false
并就此离开。我尝试了IssuerValidator
,但没有取得任何进展。我发现的是IAuthorizationHandler
。我使用 IMyService 代替 DBContext 创建了一个 PoC。我离开了ValidateIssuer = false
。
public class IssuerAuthorizationHandler : IAuthorizationHandler
{
private readonly IMyService _service;
public IssuerAuthorizationHandler(IMyService service)
{
_service = service ?? throw new ArgumentNullException(nameof(service));
}
public Task HandleAsync(AuthorizationHandlerContext context)
{
if (context.User.FindFirst("iss") != null)
{
string issuer = context.User.FindFirst("iss").Issuer;
// do issuer validation here
}
else
{
// fail the authentication
context.Fail();
}
return Task.CompletedTask;
}
}
将此添加到 DI
services.AddScoped<IAuthorizationHandler, IssuerAuthorizationHandler>();
希望这有帮助
更新:
显示何时调用授权处理程序的过滤器管道
我的解决方案基于 matt_lethargic 的答案,但我想抛出一个特定的响应并控制发生的事情,而不是一揽子授权失败重定向。
在
Program.cs
中,我添加了一个 AuthorizationHandler 来检查租户,以及一个 MiddlewareResultHandler 来执行重定向。
// Check for approved Tenant
builder.Services.AddScoped<IAuthorizationHandler, ApprovedTenantRequirementHandler>();
builder.Services.AddScoped<IAuthorizationMiddlewareResultHandler, ApprovedTenantAuthorizationMiddlewareResultHandler>();
还在
Program.cs
中,我设置了默认策略以要求获得批准的租户。
builder.Services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder()
.AddRequirements(new ApprovedTenantRequirement())
.Build();
});
需求本身就很枯燥
ApprovedTenantRequirement.cs
public class ApprovedTenantRequirement : IAuthorizationRequirement
{
}
这是我们在数据库中检查租户 ID 的地方
ApprovedTenantRequirementHandler.cs
public class ApprovedTenantRequirementHandler : AuthorizationHandler<ApprovedTenantRequirement>
{
private readonly ApplicationContext _applicationContext;
public ApprovedTenantRequirementHandler(ApplicationContext applicationContext)
{
_applicationContext = applicationContext;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ApprovedTenantRequirement requirement)
{
var validTenantIds = await _applicationContext.Tenants.Select(t => t.TenantIdGuid).ToListAsync();
var tenantIdClaim = context.User.FindFirst(c => c.Type == "tid");
if (tenantIdClaim is null)
{
return; // Requirement wasn't met - handle this later
}
if (validTenantIds.Contains(tenantIdClaim.Value))
{
context.Succeed(requirement); // Happy days
}
}
}
中间件结果处理程序采用具有特定失败要求的失败策略并返回 403。我正在构建一个 api,因此我返回一些 JSON 以及一条解释所发生情况的消息。前端会预料到这一点并将其显示给用户。这意味着我们可以为授权失败提供一致的用户体验,即使失败的原因各不相同。
public class ApprovedTenantAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly AuthorizationMiddlewareResultHandler defaultHandler = new();
public async Task HandleAsync(
RequestDelegate next,
HttpContext context,
AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult)
{
// If the authorization was forbidden and the resource had an approved tenant requirement,
// provide a custom 403 response.
if (authorizeResult.Forbidden && authorizeResult.AuthorizationFailure!.FailedRequirements
.OfType<ApprovedTenantRequirement>().Any())
{
// Return a 403 and let them know why they have been denied.
context.Response.StatusCode = StatusCodes.Status403Forbidden;
await context.Response.WriteAsJsonAsync(new {message = "Your organisation has not been approved to access this application;"});
return;
}
// Fall back to the default implementation.
await defaultHandler.HandleAsync(next, context, policy, authorizeResult);
}
}