OpenIdConnect access_token 大小和访问索赔服务器端

OpenIdConnect access_token size and accessing claims server side

我试图在这里围绕几个概念进行思考,但我不希望这个问题过于宽泛 - 基本上我们正在尝试做的是使用角色声明作为权限来锁定我们的 API 但我发现 access_token 变得太大了。

我们在服务器端使用 OpenIddict 和 ASP.NET Identity 3。我们已经实现了默认的 AspNetRoleClaims table 来存储我们对每个角色的声明 - 将它们用作权限。

我们使用基于自定义策略的声明授权来锁定我们的 API 端点,如下所示:

Custom Policy Based Authorization

我发现的主要问题是包含我们声明的 access_token 变得非常大。我们正在尝试使数据库中的 ClaimType 和 Value 非常小,以使索赔足迹更小。我们有一个基本的 CRUD 类型权限方案,因此对于我们 SPA 客户端应用程序中的每个 "module" 或屏幕,有 4 个权限。我们添加到应用程序中的模块越多,access_token 中的声明就增长得越多,我们的授权承载 header 变得非常大。我担心随着应用程序的增长,这变得不太可扩展。

所以声明嵌入在 access_token 中,当我访问我的端点时,它被这样的自定义策略锁定...

[Authorize(Policy="MyModuleCanRead")]
[HttpGet]
public IEnumerable<MyViewModel> Get()

然后我可以在 AuthorizationHandler 中访问我的 ASP.NET 身份用户和 User.Claims。

如果这是一个明显的问题,请提前道歉 - 但我想知道 - 为了让基于自定义策略的授权工作 - 它是否绝对要求声明在 id_token 或access_token 以便调用处理程序?

如果我从 access_token 中删除声明,则我的 AuthorizationHandler 代码不会受到攻击,我无法访问使用自定义策略锁定的端点。

我想知道是否可以使用自定义声明策略,但在授权处理程序中有检查声明的实际代码,这样声明就不会随每个 HTTP 请求一起传递,而是在服务器端获取来自授权 cookie 或来自数据库。

* 更新 *

Pintpoint 使用授权处理程序的回答以及关于如何从 cookie 中删除其他角色声明的评论实现了我想要的。

以防万一这对其他人有帮助 - 这是覆盖 UserClaimsPrincipalFactory 并防止将角色声明写入 cookie 的代码。 (我有很多角色声明作为权限,cookie 和请求 header 变得太大了)

public class AppClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
    public AppClaimsPrincipalFactory(UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
    {
    }
    public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
    {
        if (user == null)
        {
            throw new ArgumentNullException(nameof(user));
        }
        var userId = await UserManager.GetUserIdAsync(user);
        var userName = await UserManager.GetUserNameAsync(user);
        var id = new ClaimsIdentity(Options.Cookies.ApplicationCookieAuthenticationScheme,
            Options.ClaimsIdentity.UserNameClaimType,
            Options.ClaimsIdentity.RoleClaimType);
        id.AddClaim(new Claim(Options.ClaimsIdentity.UserIdClaimType, userId));
        id.AddClaim(new Claim(Options.ClaimsIdentity.UserNameClaimType, userName));
        if (UserManager.SupportsUserSecurityStamp)
        {
            id.AddClaim(new Claim(Options.ClaimsIdentity.SecurityStampClaimType,
                await UserManager.GetSecurityStampAsync(user)));
        }

        // code removed that adds the role claims 

        if (UserManager.SupportsUserClaim)
        {
            id.AddClaims(await UserManager.GetClaimsAsync(user));
        }
        return new ClaimsPrincipal(id);
    }
}

I am wondering if it is possible to use a custom claims policy but have the actual code that checks for the Claims inside the Authorization handler, so that the claims are not passed with each HTTP request, but are fetched server side from the Authorization cookie or from the database.

绝对有可能。方法如下:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IAuthorizationHandler, PermissionAuthorizationHandler>();

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Has-Edit-User-Profiles-Permission", builder =>
            {
                builder.RequirePermission("Edit-User-Profiles");
            });
        });
    }
}

public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
    public PermissionAuthorizationRequirement(string permission)
    {
        if (string.IsNullOrEmpty(permission))
        {
            throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
        }

        Permission = permission;
    }

    public string Permission { get; set; }
}

public class PermissionAuthorizationHandler :
    AuthorizationHandler<PermissionAuthorizationRequirement>
{
    private readonly UserManager<ApplicationUser> _userManager;

    public PermissionAuthorizationHandler(UserManager<ApplicationUser> userManager)
    {
        if (userManager == null)
        {
            throw new ArgumentNullException(nameof(userManager));
        }

        _userManager = userManager;
    }

    protected override async Task HandleRequirementAsync(
        AuthorizationHandlerContext context,
        PermissionAuthorizationRequirement requirement)
    {
        if (context.User == null)
        {
            return;
        }

        var user = await _userManager.GetUserAsync(context.User);
        if (user == null)
        {
            return;
        }

        // Use whatever API you need to ensure the user has the requested permission.
        if (await _userManager.IsInRoleAsync(user, requirement.Permission))
        {
            context.Succeed(requirement);
        }
    }
}

public static class PermissionAuthorizationExtensions
{
    public static AuthorizationPolicyBuilder RequirePermission(
        this AuthorizationPolicyBuilder builder, string permission)
    {
        if (builder == null)
        {
            throw new ArgumentNullException(nameof(builder));
        }

        if (string.IsNullOrEmpty(permission))
        {
            throw new ArgumentException("The permission cannot be null or empty.", nameof(permission));
        }

        return builder.AddRequirements(new PermissionAuthorizationRequirement(permission));
    }
}