How can I get a users role inside a WebAPI method

2019-01-30 15:56发布

问题:

I have a stored procedure that updates status. Depending on the role of the user the stored procedure has code that may or may not allow the status change. For this reason I need to pass a role name into a stored procedure. My role name is stored on the client in my javascript code but of course I need a second check on the server. Each user has only one of three roles and when requesting an update of status I can call one of three methods depending on the role that the client has. Here's what I tried.

I am using WebApi with bearer Token based authentication and ASP.NET Identity 2.1 and the application always runs in a browser. My users have been set up with the appropriate roles.

I put in place some code to get the userId and then go to the AspNetUserRoles table to get the role at the start of a method. However I noticed this takes around 500 milliseconds to run. As an alternative I am considering the following:

    [HttpPut]
    [Authorize(Roles = "Admin")]
    [Route("AdminUpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> AdminUpdateStatus(int userTestId, int userTestStatusId)
    {
        return await UpdateStatusMethod(userTestId, userTestStatusId, "Admin");
    }

    [HttpPut]
    [Authorize(Roles = "Student")]
    [Route("StudentUpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> StudentUpdateStatus(int userTestId, int userTestStatusId)
    {
        return await UpdateStatusMethod(userTestId, userTestStatusId, "Student");
    }

    [HttpPut]
    [Authorize(Roles = "Teacher")]
    [Route("TeacherUpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> TeacherUpdateStatus(int userTestId, int userTestStatusId)
    {
        return await UpdateStatusMethod(userTestId, userTestStatusId, "Teacher");
    }

    private async Task<IHttpActionResult> UpdateStatusMethod(int userTestId, int userTestStatusId, string roleName)
    {
        // Call the stored procedure here and pass in the roleName
    }

Is this an efficient way to do this or is there perhaps another more clean way. What I am rather unclear on is if the front or back end caches the users role. I assume that this is done or there is some setting that will allow this to be done.

Note I am using claims to send the Role information to my client here:

public static AuthenticationProperties CreateProperties(
            string userName,
            ClaimsIdentity oAuthIdentity,
            string firstName,
            string lastName,
            int organization)
        {
            IDictionary<string, string> data = new Dictionary<string, string>
                {
                    { "userName", userName},
                    { "firstName", firstName},
                    { "lastName", lastName},
                    { "organization", organization.ToString()},
                    { "roles",string.Join(":",oAuthIdentity.Claims.Where(c=> c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray())}

                };
            return new AuthenticationProperties(data);
        }

However my question here relates to the server and how I can check in my method if a user is in a certain role without going to the database. Maybe there's a way to do this securely with claims but I don't know how to do that.

Any help and advice would be much appreciated.

回答1:

As you stated you are using bearer tokens to protect your end points. I believe that there is little misunderstanding with what those bearer tokens magical string contains inside it. Well those tokens contains all the roles for the user you issued the token for, as well if you are using the default data protection DPAPI in Web API not (JWT Tokens) then those tokens are signed and encrypted so no one can tamper with the data inside the token unless he has the mashineKey for the web server issued this token, so do not worry about data protection.

My recommendation is to read the roles/claims for the user from the database, there is no need for this workarounds and hacks you are trying to do, all you need to do is to set the claims for the users when they login in method GrantResourceOwnerCredentials You can set it like this way by getting the user then reading the roles from DB and setting them as claim of type "Role"

 var identity = new ClaimsIdentity(context.Options.AuthenticationType);
 identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
 identity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
 identity.AddClaim(new Claim(ClaimTypes.Role, "Supervisor"));

Remember this only happens once when the user login, then you will receive a bearer signed and ecrypted token which contains all the claims for this user, no need to any DB access to verify it.

Or if you want to create the identity from Database you can use the below:

 public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager, string authenticationType)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, authenticationType);
        // Add custom user claims here
        return userIdentity;
    }

Then in GrantResourceOwnerCredentials do the below:

ClaimsIdentity oAuthIdentity = await user.GenerateUserIdentityAsync(userManager, OAuthDefaults.AuthenticationType);

Now once you send the bearer token to an protected end point with Authorize attribute such as [Authorize(Roles = "Teacher")] I can assure you that your code will not go to the DB to do any query open SQL profiler and check it will read the claims from the encrypted token along with the Roles claim and check if this user belongs to Teacher role and allow or deny the request.

I've blogged a detailed series of 5 posts about Token Based Authentication along with Authorization server, and JWT tokens. I recommend you to read those posts to get better understanding of bearer tokens.



回答2:

Rather than having 3 separate methods, You can simply check the User.IsInRole("RoleName") available in your controller class. It's using the same logic that is in the [Authorise] attribute. e.g.

public class DataApiController : ApiController
{
    [HttpPut]
    [Route("UpdateStatus/{userTestId:int}/{userTestStatusId:int}")]
    public async Task<IHttpActionResult> UpdateStatus(int userTestId, int userTestStatusId)
    {
        if(User.IsInRole("Admin"))
        {
            //Update method etc....
        }
        //else if(....) else etc
    {
}


回答3:

The role claims by default are stored in the user's login cookie, so the [Authorize(Roles = "Teacher")] should be fast. The only database hit will be when you first sign in (or whenever the sign in cookie is refreshed. The authorize checks are done against IClaimsPrincipal created from the sign in cookie.

You might need to also update some other code for this to work, see this question which is similar to what you are doing: Authorize with Roles