我有一个 API 控制器,我希望使用不记名令牌对消费服务进行授权。 API 端点应确保该服务在其令牌中具有所需的范围 - 但这对我不起作用。
我有一个由某个机构颁发的访问令牌。它的有效负载看起来像这样(显然是虚拟值):
{
"iss": "https://someauthority.com",
"nbf": 1699891816,
"iat": 1699891816,
"exp": 1699895416,
"aud": "https://myapi.com",
"scope": [
"myapi:user-read"
],
"client_id": "MyApiConsumer",
"tenant_id": "fcbebe85-5e17-4986-dffd-ede94e9b6a07",
"tenant_external_id": "7123",
"tenant_owner_client_id": "SomeTenantOwnerApp",
"jti": "ADE83169F38F3EA14B5E99AF998821EF"
}
首先,我使用
Microsoft.AspNetCore.Authentication.JwtBearer
库验证令牌:
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = "https://someauthority.com";
options.Audience = "https://myapi.com";
options.SaveToken = true; // Tried both, with and without
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero,
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) =>
{
var json = new WebClient().DownloadString("https://myapi.com/.well-known/openid-configuration/jwks");
var keys = JsonConvert.DeserializeObject<JwksKeys>(json);
return keys?.Keys;
}
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("AuthenticationFailed");
logger.LogError("Token validation failed", context.Exception);
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("TokenValidated");
logger.LogInformation("Token validated successfully.");
logger.LogInformation("Claims:");
foreach (var claim in context.Principal.Claims)
{
logger.LogInformation($"{claim.Type}: {claim.Value}");
}
return Task.CompletedTask;
}
};
});
在我添加的日志输出中,我可以清楚地看到范围声明就在那里。此外,令牌验证也有效。所以我想我可以继续构建一些可以在控制器中用作装饰器的策略,如下所示:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ScopeUserRead", policy => policy.RequireClaim("scope", "myapi:user-read"));
options.AddPolicy("ScopeUserCreate", policy => policy.RequireClaim("scope", "myapi:user-create"));
options.AddPolicy("ScopeUserWrite", policy => policy.RequireClaim("scope", "myapi:user-read-write"));
});
在我的控制器中,在类上使用
[Authorize]
装饰器,在方法上使用 [Authorize("ScopeUserRead")]
。
不过,使用上面的令牌调用 API 确实会产生 401 错误。
我了解到,我用来构建策略的
RequireClaim
方法从 HttpContext
的 User 对象获取范围声明 - 该对象为空,因为这是访问令牌,而不是 ID 令牌。然而,一些消息来源表示,在验证过程中,应将声明从令牌中的主体对象复制到HttpContext
用户对象。当我像这样检查 User 对象时,我发现这在我的情况下是不正确的:
app.Use(async (context, next) =>
{
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("ClaimsMiddleware");
var user = context.User;
if (user.Identity.IsAuthenticated)
{
logger.LogInformation("Authenticated User Claims:");
foreach (var claim in user.Claims)
{
logger.LogInformation($"{claim.Type}: {claim.Value}");
}
}
else
{
logger.LogInformation("User is not authenticated.");
}
await next.Invoke();
});
所以我想知道我做错了什么,如果我的整个方法只是垃圾,我应该使用不同的库还是应该尝试将声明手动复制到 User 对象(如果可能的话)?
此外,这应该是一个多租户 API,因此控制器方法需要知道
tenant_id
声明的值。有什么方法可以使 tenant_id
作为我的控制器方法的参数,也许通过使用一些装饰器 [FromToken]
或者这只是我的梦想?或者我必须像一样从
HttpContext
检索它吗?
var tenantId = HttpContext.User.FindFirst("tenant_id").Value;
在这种情况下,它需要出现在
User
对象中,但它不是......
根据您的描述,您没有在正确的时间进行身份验证并收到 401 错误, 确保您的中间件顺序正确:
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();