Jun Kang
Jun Kang

Reputation: 1275

Client specific role based authentication?

Currently, I am authenticating users in my application using role based authentication with OAuth and WebApi. I've set this up like so:

public override async Task GrantResourceOwnerCredentials (OAuthGrantResourceOwnerCredentialsContext context)
{
    var user = await AuthRepository.FindUser(context.UserName, context.Password);

    if (user === null)
    {
        context.SetError("invalid_grant", "The username or password is incorrect");
        return;
    }

    var id = new ClaimsIdentity(context.Options.AuthenticationType);
    id.AddClaim(New Claim(ClaimTypes.Name, context.UserName));

    foreach (UserRole userRole in user.UserRoles)
    {
        id.AddClaim(new Claim(ClaimTypes.Role, userRole.Role.Name));
    }

    context.Validated(id);
}

Protecting my API routes with the <Authorize> tag.

I've since, however, run into an issue where my users can hold different roles for different clients. For example:

User A can be associated to multiple clients: Client A and Client B.
User A can have different "roles" when accessing information from either client. So User A may be an Admin for Client A and a basic User for Client B.

Which means, the following example:

[Authorize(Roles = "Admin")]
[Route("api/clients/{clientId}/billingInformation")]
public IHttpActionResult GetBillingInformation(int clientId) 
{
    ...
}

User A may access billing information for Client A, but not for Client B.

Obviously, what I have now won't work for this type of authentication. What would be the best way to set up Client specific Role based authentication? Can I simply change up what I have now, or would I have to set it up a different way entirely?

Upvotes: 4

Views: 1736

Answers (4)

Bbone
Bbone

Reputation: 1

The requirement is about having users with different authorizations. Don't feel oblige to strictly match a user permissions/authorizations with his roles. Roles are part of user identity and should not depend from client.
I will suggest to decompose the requirement:

NB: Claims should contain only user identity data (name, email, roles, ...). Adding authorizations, access rights claims in the token is not a good choice in my opion:

  • The token size might increase drastically

  • The user might have different authorizations regarding the domain context or the micro service

Below some usefuls links:

https://learn.microsoft.com/en-us/dotnet/framework/security/claims-based-identity-model
https://leastprivilege.com/2016/12/16/identity-vs-permissions/
https://leastprivilege.com/2014/06/24/resourceaction-based-authorization-for-owin-and-mvc-and-web-api/

Upvotes: 0

ste-fu
ste-fu

Reputation: 7434

One solution would be to add the clients/user relationship as part of the ClaimsIdentity, and then check that with a derived AuthorizeAttribute.

You would extend the User object with a Dictionary containing all their roles and the clients that they are authorized for in that role - presumably contained in your db:

public Dictionary<string, List<int>> ClientRoles { get; set; }

In your GrantResourceOwnerCredentials method, you would add these as individual Claims with the Client Ids as the value:

foreach (var userClientRole in user.ClientRoles)
{
    oAuthIdentity.AddClaim(new Claim(userClientRole.Key,
        string.Join("|", userClientRole.Value)));
}

And then create a custom attribute to handle reading the claims value. The slightly tricky part here is getting the clientId value. You have given one example where it is in the route, but that may not be consistent within your application. You could consider passing it explicitly in a header, or derive whatever URL / Route parsing function works in all required circumstances.

public class AuthorizeForCustomer : System.Web.Http.AuthorizeAttribute
{
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        var isAuthorized = base.IsAuthorized(actionContext);

        string clientId = ""; //Get client ID from actionContext.Request;

        var user = actionContext.ControllerContext.RequestContext.Principal as ClaimsPrincipal;
        var claim = user.FindFirst(this.Roles);
        var clientIds = claim.Value.Split('|');

        return isAuthorized && clientIds.Contains(clientId);
    }
}

And you would just swap

[Authorize(Roles = "Admin")] for [AuthorizeForCustomer(Roles = "Admin")]

Note that this simple example would only work with a single role, but you get the idea.

Upvotes: 1

Jammer
Jammer

Reputation: 10208

Personally I think you need to move away from using the [Authorize] attribute entirely. It's clear that your requirements for authorisation are more complex than that method "out-the-box" was intended for.

Also in the question about I think Authentication and Authorisation are being used interchangably. What we are dealing with here is Authorisation.

Since you are using Identity and claims based authorisation. I would look at doing this "on-the-fly" so to speak. Along with claims you could make use of dynamic policy generation as well as service based Authorisation using IAuthorizationRequirement instances to build up complex rules and requirements.

Going into depth on the implementation of this is a big topic but there are some very good resources available. The original approach (that I have used myself) was orginally detailed by Dom and Brock of IdentityServer fame.

They did a comprehensive video presentation on this at NDC last year which you can watch here.

Based closely on the concepts discussed in this video Jerrie Pelser blogged about a close implementation which you can read here.

The general components are:

The [Authorize] attributes would be replaced by policy generator such as:

public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
{
    private readonly IConfiguration _configuration;

    public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options, IConfiguration configuration) : base(options)
    {
        _configuration = configuration;
    }

    public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
    {
        // Check static policies first
        var policy = await base.GetPolicyAsync(policyName);

        if (policy == null)
        {
            policy = new AuthorizationPolicyBuilder()
                .AddRequirements(new HasScopeRequirement(policyName, $"https://{_configuration["Auth0:Domain"]}/"))
                .Build();
        }

        return policy;
    }
}

And then you would author any instances of IAuthorizationRequirement required to ensure users are authroised properly, an example of that would be something like:

public class HasScopeRequirement : IAuthorizationRequirement
{
    public string Issuer { get; }
    public string Scope { get; }

    public HasScopeRequirement(string scope, string issuer)
    {
        Scope = scope ?? throw new ArgumentNullException(nameof(scope));
        Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
    }
}

Dom and Brock then also detail a client implementation that ties all of this together which might look something like this:

  public class AuthorisationProviderClient : IAuthorisationProviderClient
  {
    private readonly UserManager<ApplicationUser> userManager;
    private readonly RoleManager<IdentityRole> roleManager;

    public AuthorisationProviderClient(
      UserManager<ApplicationUser> userManager, 
      RoleManager<IdentityRole> roleManager)
    {
      this.userManager = userManager;
      this.roleManager = roleManager;
    }

    public async Task<bool> IsInRole(ClaimsPrincipal user, string role)
    {
      var appUser = await GetApplicationUser(user);
      return await userManager.IsInRoleAsync(appUser, role);
    }

    public async Task<List<Claim>> GetAuthorisationsForUser(ClaimsPrincipal user)
    {
      List<Claim> claims = new List<Claim>();
      var appUser = await GetApplicationUser(user);

      var roles = await userManager.GetRolesAsync(appUser);

      foreach (var role in roles)
      {
        var idrole = await roleManager.FindByNameAsync(role);

        var roleClaims = await roleManager.GetClaimsAsync(idrole);

        claims.AddRange(roleClaims);
      }

      return claims;
    }

    public async Task<bool> HasClaim(ClaimsPrincipal user, string claimValue)
    {
      Claim required = null;
      var appUser = await GetApplicationUser(user);

      var userRoles = await userManager.GetRolesAsync(appUser);

      foreach (var userRole in userRoles)
      {
        var identityRole = await roleManager.FindByNameAsync(userRole);

        // this only checks the AspNetRoleClaims table
        var roleClaims = await roleManager.GetClaimsAsync(identityRole);
        required = roleClaims.FirstOrDefault(x => x.Value == claimValue);

        if (required != null)
        {
          break;
        }
      }

      if (required == null)
      {
        // this only checks the AspNetUserClaims table
        var userClaims = await userManager.GetClaimsAsync(appUser);
        required = userClaims.FirstOrDefault(x => x.Value == claimValue);
      }

      return required != null;
    }

    private async Task<ApplicationUser> GetApplicationUser(ClaimsPrincipal user)
    {
      return await userManager.GetUserAsync(user);
    }
  }

Whilst this implementation doesn't address your exact requirements (which would be hard to do anyway), this is almost certainly the approach that I would adopt given the scenario you illustrated in the question.

Upvotes: 1

Daniel Frykman
Daniel Frykman

Reputation: 62

You could remove the authorize tag and do the role validation inside the function instead.

Lambda solution:

Are there roles that are added based on CustomerID and UserID?
If so you could do something like the example below where you get the customer based of the values you have and then return the response.

string userID = RequestContext.Principal.Identity.GetUserId();
var customer = Customer.WHERE(x => x.UserID == userID && x.clientId == clientId && x.Roles == '1')

Can you provide us with abit more information about what you use to store the connection/role between the Customer and User.

EDIT:

Here is an example on how you could use the ActionFilterAttribute. It gets the CustomerId from the request and then takes the UserId of the identity from the request. So you can replace [Authorize] with [UserAuthorizeAttribute]

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
    public class UserAuthorizeAttribute : System.Web.Http.Filters.ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            try
            {
                var authHeader = actionContext.Request.Headers.GetValues("Authorization").First();
                if (string.IsNullOrEmpty(authHeader))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Missing Authorization-Token")
                    };
                    return;
                }

                ClaimsPrincipal claimPrincipal = actionContext.Request.GetRequestContext().Principal as ClaimsPrincipal;
                if (!IsAuthoticationvalid(claimPrincipal))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Invalid Authorization-Token")
                    };
                    return;
                }

                if (!IsUserValid(claimPrincipal))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Invalid User name or Password")
                    };
                    return;
                }

                //Finally role has perpession to access the particular function
                if (!IsAuthorizationValid(actionContext, claimPrincipal))
                {
                    actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("Permission Denied")
                    };
                    return;
                }

            }
            catch (Exception ex)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Missing Authorization-Token")
                };
                return;
            }

            try
            {
                //AuthorizedUserRepository.GetUsers().First(x => x.Name == RSAClass.Decrypt(token));
                base.OnActionExecuting(actionContext);
            }
            catch (Exception)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    Content = new StringContent("Unauthorized User")
                };
                return;
            }
        }

        private bool IsAuthoticationvalid(ClaimsPrincipal claimPrincipal)
        {
            if (claimPrincipal.Identity.AuthenticationType.ToLower() == "bearer"
                && claimPrincipal.Identity.IsAuthenticated)
            {
                return true;
            }
            return false;
        }

        private bool IsUserValid(ClaimsPrincipal claimPrincipal)
        {
            string userID = claimPrincipal.Identity.GetUserId();
            var securityStamp = claimPrincipal.Claims.Where(c => c.Type.Equals("AspNet.Identity.SecurityStamp", StringComparison.OrdinalIgnoreCase)).Single().Value;

            var user = _context.AspNetUsers.Where(x => x.userID.Equals(userID, StringComparison.OrdinalIgnoreCase)
                && x.SecurityStamp.Equals(securityStamp, StringComparison.OrdinalIgnoreCase));
            if (user != null)
            {
                return true;
            }
            return false;
        }

        private bool IsAuthorizationValid(HttpActionContext actionContext, ClaimsPrincipal claimPrincipal)
        {
            string userId = claimPrincipal.Identity.GetUserId();
            string customerId = (string)actionContext.ActionArguments["CustomerId"];
            return AllowedToView(userId, customerId);
        }

        private bool AllowedToView(string userId, string customerId)
        {
            var customer = _context.WHERE(x => x.UserId == userId && x.CustomerId == customerId && x.RoleId == '1')
            return false;
        }
    }

Upvotes: 1

Related Questions