C#/.NET6:从访问令牌 (OAuth/OpenID) 中的范围为 API 控制器构建策略

C#/.NET6: Build Policies for an API Controller from Scopes in an Access Token (OAuth/OpenID)

提问人:Aileron79 提问时间:11/14/2023 最后编辑:Ruikai FengAileron79 更新时间:11/14/2023 访问量:44

问:

我有一个 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 错误。

我了解到,我用于构建策略的方法从 - 的 User 对象中获取范围声明,该对象为空,因为这是访问令牌,而不是 ID 令牌。但是,一些消息来源说,在验证期间,应将声明从令牌中的 Principal 对象复制到 User 对象。我发现这在我的情况下是不正确的,因为我像这样检查了 User 对象:RequireClaimHttpContextHttpContext

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();
});

所以我想知道我做错了什么,如果我的整个方法只是垃圾,我应该使用不同的库还是应该尝试手动将声明复制到用户对象(如果可能的话)?

此外,这应该是一个多租户 API,因此控制器方法需要知道声明的值。有没有办法让它作为我的控制器方法的参数可用,也许是通过使用一些装饰器,或者这只是我的梦想?或者我必须从喜欢中检索它tenant_idtenant_id[FromToken]HttpContext

var tenantId = HttpContext.User.FindFirst("tenant_id").Value;

在这种情况下,它需要存在于对象中,而它不是......User

.net asp.net-core oauth jwt openid

评论

1赞 sich 11/14/2023
你也有应用程序吗?使用身份验证();应用程序。UseAuthorization();在您的程序.cs代码中?

答:

1赞 Ruikai Feng 11/14/2023 #1

从您的描述来看,您没有在正确的时间进行身份验证并收到 401 错误, 确保您的中间件顺序正确:

app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();

评论

0赞 Aileron79 11/14/2023
我怎么会这么傻,我很清楚这一点!但这是第一次使用令牌身份验证,因此我立即认为这有问题。谢谢你为我指明正确的方向!