Question

如何使两个方案的令牌与角色一起工作,而不管使用的是"默认"方案令牌还是"辅助"方案令牌?

目前,任一令牌都将对具有.RequireAuthorization()个的API起作用(200响应),但只有属于DefaultAuthenticateScheme标识的方案的令牌才能对具有.RequireAuthorization(a => a.RequireRole(roleName));的API起作用.

更改哪个方案DefaultAuthenticateScheme指向哪些令牌使用需要管理员角色的API的更改,即使两个令牌都具有此角色,尽管它们属于不同的方案.

那么,这里的解决方案是什么?

Reproduction case

代码

这应该是重现问题所需的一切.JWT令牌是使用测试服务生成的(不用担心!这里没有凭据泄露!).

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
 
namespace AuthorizeProblemSample
{
    public class Program
    {
        public static void Main(string[] args)
        {
            const string roleName = "Administrator";
            const string defaultScheme = "Default";
            const string secondaryScheme = "Secondary";
            var builder = WebApplication.CreateBuilder(args);
 
            builder.Services.AddControllers();
            builder.Services.AddAuthentication(o =>
            {
                o.DefaultAuthenticateScheme = defaultScheme;
                o.DefaultScheme = defaultScheme;
            })

            // JWT credentials generated for this sample using Jamie Kurtz's JWT Builder.
            // No credentials have been harmed in the making of this sample.

            // Default scheme token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2OTUyODkzMjMsImV4cCI6MTcyNjgyNTMyMywiYXVkIjoiZGVmYXVsdEF1ZGllbmNlIiwic3ViIjoidXNlcjFAZXhhbXBsZS5jb20ifQ.SH3mxkdJCjdQ4HUX7sRPLJ2_7baW2OwNhB39fnGduD8
            .AddJwtBearer(defaultScheme, o => o.Audience = "defaultAudience")
            // Secondary scheme token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2OTUyODkzMjMsImV4cCI6MTcyNjgyNTMyMywiYXVkIjoic2Vjb25kYXJ5QXVkaWVuY2UiLCJzdWIiOiJ1c2VyMUBleGFtcGxlLmNvbSJ9.TNamLBog9qxLiebI7F8hu0dX09MjZlGoydKYeDve0ig
            .AddJwtBearer(secondaryScheme, o => o.Audience = "secondaryAudience");
            builder.Services.ConfigureAll<JwtBearerOptions>(o =>
            {
                o.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateActor = false,
                    ValidateIssuer = false,
                    ValidateIssuerSigningKey = false,
                    ValidateLifetime = false,
                    ValidateAudience = true,
                    ValidateTokenReplay = false,
                    SignatureValidator = (t, v) => new JwtSecurityToken(t)
                };
 
                o.Events = new JwtBearerEvents()
                {
                    OnTokenValidated = context =>
                    {
                        var claims = context.Principal!.Claims.Append(new Claim(ClaimTypes.Role, roleName));
                        var claimsIdentity = new ClaimsIdentity(claims, context.Principal!.Identity!.AuthenticationType, ClaimTypes.Name, ClaimTypes.Role);
                        context.Principal = new ClaimsPrincipal(claimsIdentity);
                        return Task.CompletedTask;
                    }
                };
            });
 
            builder.Services.AddAuthorization(opts =>
            {
                const string policyName = "myPolicy";
                opts.AddPolicy(policyName, policy =>
                {
                    policy.RequireAuthenticatedUser();
                    policy.AddAuthenticationSchemes(defaultScheme, secondaryScheme);
                });
                opts.DefaultPolicy = opts.GetPolicy(policyName)!;
            });
 
            var app = builder.Build();
 
            app.UseAuthentication();
            app.UseRouting();
            app.UseAuthorization();
            app.MapGet("/一", () => "hello").RequireAuthorization();
            app.MapGet("/两个", () => "world").RequireAuthorization(a => a.RequireRole(roleName));
            app.MapGet("/INFO", (HttpRequest req) =>
            {
                var result = new StringBuilder();
                result.AppendFormat("User is in {0} role?: {1}", roleName, req.HttpContext.User.IsInRole(roleName));
                result.AppendLine();

                result.AppendFormat("User is authenticated?: {0}", req.HttpContext.User.Identity.IsAuthenticated);
                result.AppendLine();
 
                var roleClaims = req.HttpContext.User.Claims.Where(c => c.Type == ClaimTypes.Role);
                foreach (var roleClaim in roleClaims)
                {
                    result.AppendFormat("Role: {0}", roleClaim.Value);
                }
                return result.ToString();
            }).RequireAuthorization();
 
            app.Run();
        }
    }
}

正如您所看到的,在通过OnTokenValidated事件向入站请求添加Administrator角色时,这两种方案的工作方式是相同的.

创建演示项目的步骤

  1. 创建新的空ASP.NET 7.0 Web API(使用 top-level ,不需要HTTPS或Docker).
  2. 添加以下程序包:
<ItemGroup>
  <PackageReference Include="IdentityModel" Version="6.2.0" />
  <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="7.0.11" />
</ItemGroup>
  1. 将上面的整个代码粘贴到Program.cs中
  2. 运行它并发出请求

端点

/INFO

如果您使用任一JWT令牌请求/INFO,您将看到该令牌所代表的用户确实具有管理员角色,它被认为是经过身份验证的,并且这是唯一的角色.

输出如下所示:

User is in Administrator role?: True
User is authenticated?: True
Role: Administrator

/一

当针对/一个令牌发出请求时,这两个令牌都可以工作(200响应),大概是因为它不需要判断角色.当你调用它的时候,你应该会得到"Hello"的结果.

/两个

/两个要求调用者具有角色Administrator.您将观察到的行为是,它适用于属于DefaultScheme方案(200响应)的令牌,但不适用于属于SecondaryScheme方案(403响应)的令牌.

但如果您随后更改DefaultAuthenticateScheme = SecondaryScheme,则相反的情况将变为正确:/两个将适用于属于SecondaryScheme(200响应)的令牌,但不适用于属于DefaultScheme(403响应)的令牌.这就是问题所在.

Notes

  1. 同样的问题也发生在用[Authorize][Authorize(Roles = "Administrator")]属性修饰的非最小API上,但我在这里使用了最小API来减少示例代码的大小.
  2. 我想知道我在OnTokenValidated中的代码是否实际上没有按照我预期的方式工作,但是注释掉整个事件会导致用户在调用/INFO时当然没有Administrator或任何角色,并且在调用/两个时拒绝这两个令牌,所以很明显,当调用/两个时,它是代码的一部分,使得DefaultAuthenticateScheme‘S令牌成功.
  3. 我不确定这些是Windows权限还是任何类型的操作系统权限的 idea 是从哪里来的.用户是应用程序级别的用户,权限是应用程序级别的权限,旨在控制对API中某些功能的访问.尽管我的项目没有使用ASP.NET核心标识,但角色是基于对经过身份验证的用户的角色声明,并且同样,角色是应用程序域的一部分(请参见this question),关于如何为使用ASP.NET核心标识的用户创建角色以更好地了解它们是什么.

推荐答案

(原答案如下)

不知何故,我认为即使在正常身份验证流的情况下也不会调用OnAuthenticationFailed-如果您有几个模式-有些会失败,一个可能会成功,所以强制对所有可用受众进行身份验证显然会使第一个(默认)模式的第二个令牌成功(安全问题).

事实上,问题的根源是对/two端点的.RequireAuthorization(a => { /*... */ })调用-身份验证策略不是简单地组合在一起,而是从头开始定义,因此完全忽略了默认策略.传递的委托缺少AddAuthenticationSchemes调用,因此仅接受默认方案.


Original answer:

如果您设置JwtBearerEvents.OnAuthenticationFailed HANDLER,您会很快发现原因:

IDX10214: Audience validation failed.
Audiences: 'secondaryAudience'.
Did not match:
   validationParameters.ValidAudience:  'defaultAudience'
or validationParameters.ValidAudiences: 'null'.

将两个受众都设置为TokenValidationParameters应该可以做到这一点:

o.TokenValidationParameters = new TokenValidationParameters()
{
    /* ... */
    ValidateAudience = true,
    ValidAudiences = new[] { "defaultAudience", "secondaryAudience" },
    /* ... */
};

为什么默认情况下不填充?我不知道.

Also it seems like empty .RequireAuthorization() just requires auth processing, but does not require it to succeed (/one endpoint also fails to validate the token).
Changing to .RequireAuthorization(a => a.RequireAuthenticatedUser()) will make /one fail just as /two.

Csharp相关问答推荐

Autofac:如何防止丢弃通过ServicCollection注册的服务?

总是丢弃返回的任务和使方法puc无效之间有区别吗?

HttpContext. RequestAborted当Android APP失go 连接时未取消

如何阻止注释被包含在C#release build. exe中

C#.NET依赖项注入顺序澄清

为什么我的表单在绑定到对象时提交空值?

如何将此方法参数化并使其更灵活?

异步实体框架核心查询引发InvalidOperation异常

C#Null判断处理失败

Swagger没有显示int?可以为空

VS 2022 for ASP.NET Core中缺少自定义项模板

在DoubleClick上交换DataGridViewImageColumn的图像和工具提示

在字符串C#之前获取数字

将操作从编辑页重定向到带参数的索引页

如何在特定环境中运行dotnet测试?

如何将默认区域性更改为fr-FR而不是en-US?

将字符串类型日期输入(yyyy-mm-ddthh:mm:ss)转换为MM/dd/yyyy格式

在C#和HttpClient中使用REST API

项目参考和方法签名问题

我什么时候不应该在Dispose中调用EgSuppressFinalize(This)?