web-dev-qa-db-ja.com

すべての場所でif / elseコードを複製せずにASP.NET Coreリソースベースの認証を使用する方法

ユーザークレームとアクセスされるリソースに基づいて承認される必要があるいくつかのコントローラーとアクションメソッドを備えたdotnet core 2.2 apiがあります。基本的に、各ユーザーは各リソースに対して0個または多くの「ロール」を持つことができます。これはすべてASP.NET Identity Claimsを使用して行われます。

だから、私の理解は Resource-based authorization を利用する必要があるということです。しかし、どちらの例もほとんど同じであり、各アクションメソッドに明示的な命令のif/elseロジックが必要です。これは、回避しようとしていることです。

次のようなことができるようになりたい

[Authorize("Admin")] // or something similar
public async Task<IActionResult> GetSomething(int resourceId)
{
   var resource = await SomeRepository.Get(resourceId);

   return Json(resource);
}

そして、どこか他の場所でポリシー/フィルター/要件/何でも承認ロジックを定義し、現在のユーザーのクレームとエンドポイントが受け取ったresourceIdパラメーターの両方にアクセスできます。そのため、ユーザーが特定のresourceIdに対して「管理者」の役割を持っていることを示すクレームがあるかどうかを確認できます。

7
emzero

編集:フィードバックに基づいて動的にします

RBACと.NETのクレームで重要なことは、ClaimsIdentityを作成し、フレームワークにそれを任せることです。以下は、クエリパラメータ「user」を調べ、辞書に基づいてClaimsPrincipalを生成するミドルウェアの例です。

実際にIDプロバイダーに接続する必要を回避するために、ClaimsPrincipalをセットアップするミドルウェアを作成しました。

// **THIS CLASS IS ONLY TO DEMONSTRATE HOW THE ROLES NEED TO BE SETUP **
public class CreateFakeIdentityMiddleware
{
    private readonly RequestDelegate _next;

    public CreateFakeIdentityMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    private readonly Dictionary<string, string[]> _tenantRoles = new Dictionary<string, string[]>
    {
        ["tenant1"] = new string[] { "Admin", "Reader" },
        ["tenant2"] = new string[] { "Reader" },
    };

    public async Task InvokeAsync(HttpContext context)
    {
        // Assume this is the roles
        List<Claim> claims = new List<Claim>
        {
            new Claim(ClaimTypes.Name, "John"),
            new Claim(ClaimTypes.Email, "[email protected]")
        };

        foreach (KeyValuePair<string, string[]> tenantRole in _tenantRoles)
        {
            claims.AddRange(tenantRole.Value.Select(x => new Claim(ClaimTypes.Role, $"{tenantRole.Key}:{x}".ToLower())));
        }

        // Note: You need these for the AuthorizeAttribute.Roles    
        claims.AddRange(_tenantRoles.SelectMany(x => x.Value)
            .Select(x => new Claim(ClaimTypes.Role, x.ToLower())));

        context.User = new System.Security.Claims.ClaimsPrincipal(new ClaimsIdentity(claims,
            "Bearer"));

        await _next(context);
    }
}

これを結び付けるには、スタートアップクラスで IApplicationBuilderseMiddleware 拡張メソッドを使用するだけです。

app.UseMiddleware<RBACExampleMiddleware>();

クエリパラメーター「テナント」を検索し、ロールに基づいて成功または失敗するAuthorizationHandlerを作成します。

public class SetTenantIdentityHandler : AuthorizationHandler<TenantRoleRequirement>
{
    public const string TENANT_KEY_QUERY_NAME = "tenant";

    private static readonly ConcurrentDictionary<string, string[]> _methodRoles = new ConcurrentDictionary<string, string[]>();

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TenantRoleRequirement requirement)
    {
        if (HasRoleInTenant(context))
        {
            context.Succeed(requirement);
        }
        return Task.CompletedTask;
    }

    private bool HasRoleInTenant(AuthorizationHandlerContext context)
    {
        if (context.Resource is AuthorizationFilterContext authorizationFilterContext)
        {
            if (authorizationFilterContext.HttpContext
                .Request
                .Query
                .TryGetValue(TENANT_KEY_QUERY_NAME, out StringValues tenant)
                && !string.IsNullOrWhiteSpace(tenant))
            {
                if (TryGetRoles(authorizationFilterContext, tenant.ToString().ToLower(), out string[] roles))
                {
                    if (context.User.HasClaim(x => roles.Any(r => x.Value == r)))
                    {
                        return true;
                    }
                }
            }
        }

        return false;
    }

    private bool TryGetRoles(AuthorizationFilterContext authorizationFilterContext,
        string tenantId,
        out string[] roles)
    {
        string actionId = authorizationFilterContext.ActionDescriptor.Id;
        roles = null;

        if (!_methodRoles.TryGetValue(actionId, out roles))
        {
            roles = authorizationFilterContext.Filters
                .Where(x => x.GetType() == typeof(AuthorizeFilter))
                .Select(x => x as AuthorizeFilter)
                .Where(x => x != null)
                .Select(x => x.Policy)
                .SelectMany(x => x.Requirements)
                .Where(x => x.GetType() == typeof(RolesAuthorizationRequirement))
                .Select(x => x as RolesAuthorizationRequirement)
                .SelectMany(x => x.AllowedRoles)
                .ToArray();

            _methodRoles.TryAdd(actionId, roles);
        }

        roles = roles?.Select(x => $"{tenantId}:{x}".ToLower())
            .ToArray();

        return roles != null;
    }
}

TenantRoleRequirementは非常に単純なクラスです。

public class TenantRoleRequirement : IAuthorizationRequirement { }

次に、startup.csファイルに次のようにすべてを接続します。

services.AddTransient<IAuthorizationHandler, SetTenantIdentityHandler>();

// Although this isn't used to generate the identity, it is needed
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
    options.Audience = "https://localhost:5000/";
    options.Authority = "https://localhost:5000/identity/";
});

services.AddAuthorization(authConfig =>
{
    authConfig.AddPolicy(Policies.HasRoleInTenant, policyBuilder => {
        policyBuilder.RequireAuthenticatedUser();
        policyBuilder.AddRequirements(new TenantRoleRequirement());
    });
});

メソッドは次のようになります。

// TOOD: Move roles to a constants/globals
[Authorize(Policy = Policies.HasRoleInTenant, Roles = "admin")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
    return new string[] { "value1", "value2" };
}

以下はテストシナリオです:

  1. 正: https:// localhost:44337/api/values?tenant = tenant1

  2. 否定: https:// localhost:44337/api/values?tenant = tenant2

  3. 否定: https:// localhost:44337/api/values

このアプローチの重要な点は、私が実際に403を返すことは決してないということです。コードはIDを設定し、フレームワークに結果を処理させます。これにより、認証と承認が分離されます。

〜乾杯

3
Rogala

コメントに基づいて編集

私の理解によると、現在のユーザー(それに関連するすべての情報)、コントローラー(またはアクション)に指定する役割、およびエンドポイントが受信するパラメーターにアクセスする必要があります。 Web APIは試していませんが、asp.netコアMVCの場合は、ポリシーベースの承認でAuthorizationHandlerを使用してこれを実現し、ロール-リソースアクセスを決定するために特別に作成された挿入サービスと組み合わせることができます。

これを行うには、まず_Startup.ConfigureServices_でポリシーを設定します。

_services.AddAuthorization(options =>
{
    options.AddPolicy("UserResource", policy => policy.Requirements.Add( new UserResourceRequirement() ));
});
services.AddScoped<IAuthorizationHandler, UserResourceHandler>();
services.AddScoped<IRoleResourceService, RoleResourceService>();
_

次にUserResourceHandlerを作成します。

_public class UserResourceHandler : AuthorizationHandler<UserResourceRequirement>
{
    readonly IRoleResourceService _roleResourceService;

    public UserResourceHandler (IRoleResourceService r)
    {
        _roleResourceService = r;
    }

    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext authHandlerContext, UserResourceRequirement requirement)
    {
        if (context.Resource is AuthorizationFilterContext filterContext)
        {
            var area = (filterContext.RouteData.Values["area"] as string)?.ToLower();
            var controller = (filterContext.RouteData.Values["controller"] as string)?.ToLower();
            var action = (filterContext.RouteData.Values["action"] as string)?.ToLower();
            var id = (filterContext.RouteData.Values["id"] as string)?.ToLower();
            if (_roleResourceService.IsAuthorize(area, controller, action, id))
            {
                context.Succeed(requirement);
            }               
        }            
    }
}
_

エンドポイントが受け取ったパラメーターにアクセスするには、_context.Resource_をAuthorizationFilterContextにキャストすることで、RouteDataにアクセスできるようにします。 UserResourceRequirementについては、空のままにすることができます。

_public class UserResourceRequirement : IAuthorizationRequirement { }
_

IRoleResourceServiceについては、何でも注入できるようにするための単純なサービスクラスです。このサービスは、アクションの属性でロールを指定する必要がないように、コードでロールをアクションにペアリングする代用です。そうすることで、実装を自由に選択できます。たとえば、データベースから、構成ファイルから、またはハードコードから。

RoleResourceServiceのユーザーにアクセスするには、IHttpContextAccessorを挿入します。 IHttpContextAccessorを注入可能にするには、_Startup.ConfigurationServices_メソッド本体にservices.AddHttpContextAccessor()を追加することに注意してください。

以下は設定ファイルから情報を取得する例です:

_public class RoleResourceService : IRoleResourceService
{
    readonly IConfiguration _config;
    readonly IHttpContextAccessor _accessor;
    readonly UserManager<AppUser> _userManager;

    public class RoleResourceService(IConfiguration c, IHttpContextAccessor a, UserManager<AppUser> u) 
    {
        _config = c;
        _accessor = a;
        _userManager = u;
    }

    public bool IsAuthorize(string area, string controller, string action, string id)
    {
        var roleConfig = _config.GetValue<string>($"RoleSetting:{area}:{controller}:{action}"); //assuming we have the setting in appsettings.json
        var appUser = await _userManager.GetUserAsync(_accessor.HttpContext.User);
        var userRoles = await _userManager.GetRolesAsync(appUser);
        // all of needed data are available now, do the logic of authorization
        return result;
    } 
}
_

データベースから設定を取得することは確かにもう少し複雑ですが、AppDbContextを注入できるため、実行できます。ハードコード化されたアプローチについては、それを行うための多くの方法が存在します。

すべてが完了したら、アクションにポリシーを使用します。

_[Authorize(Policy = "UserResource")] //dont need Role name because of the RoleResourceService
public ActionResult<IActionResult> GetSomething(int resourceId)
{
    //existing code
}
_

実際、適用したいあらゆるアクションに「UserResource」ポリシーを使用できます。

2
Riza