26

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.

Samantha J T Star
  • 30,952
  • 84
  • 245
  • 427
  • Have you thought about using Claims? – Excommunicated Oct 28 '14 at 18:54
  • 2
    Are you sure it is the lookup of the role that is really causing the delay. It should be pretty efficient. What is your SP doing? How many users do you have? – Pleun Oct 29 '14 at 15:25
  • I was doing the look up of the role manually with a call to the database table. Timing the look up it took about 500 ms. I am sure in production it will be much quicker but what I would like to understand more of is does the WebAPI framework go to the database every single time to check a role before the method is run or is there some kind of caching involved. I have many other delays so I want to speed up everything I can. – Samantha J T Star Oct 29 '14 at 15:59
  • Are your user roles defined in database? If so, why do you have to pass the role back to the procedure? Can't you just query it by the user name instead? – Ray Cheng Oct 29 '14 at 23:25
  • The user roles are defined in a different SQL Azure database. With SQL Azure cloud database it's my understanding it's not possible to do a query between databases. – Samantha J T Star Oct 30 '14 at 08:10
  • How about moving the roles to the same database? – Pleun Oct 30 '14 at 20:37

3 Answers3

34

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.

Taiseer Joudeh
  • 8,953
  • 1
  • 41
  • 45
  • Thanks for your help and advice. I will read your blogs and try to implement your solution over the next few days. If I have a question I will update here. Just one quick question at this time. How would you suggest I deal with the matter of needing to handle multiple different Authorize attributes. Right now the method of having one private methods with three or more public methods (each with a different Authorize) works. But I think there may be a better way. Can you suggest any way I could do this with just one method and a test inside of that for the role? – Samantha J T Star Oct 30 '14 at 18:18
  • You welcome @SamanthaJ. What you are doing is not wrong, and you can use nested if else inside your action method such as if(User.IsInRole("Teacher")){} so you will end up having one action (public endpoint) instead of three, then you attribute this action method with [Authorize(Roles = "Teacher,Student,Admin")] As well this check will not hit the DB because the claims principal is created based on your access token. – Taiseer Joudeh Oct 30 '14 at 21:41
  • @SamanthaJ did you try the solution suggested? Working at your end? – Taiseer Joudeh Nov 01 '14 at 13:10
  • @TaiseerJoudeh Please do you think you could help me with this question http://goo.gl/qiOdmT – Axel Nov 10 '14 at 03:41
  • 1
    I think I'm missing something really obvious here but, how do you obtain the claims that you set on the token? – Carlos Martinez Dec 31 '14 at 05:30
  • @TaiseerJoudeh : can I add `,` separated roles to claim like this `identity.AddClaim(new Claim(ClaimTypes.Role, "Client,User"));` – Amit Kumar Apr 25 '16 at 12:25
12

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
    {
}
Excommunicated
  • 1,252
  • 8
  • 14
  • But will each of those User.IsInRole("Admin") not involve a database access? What if I had 5 roles then that would be five database accesses to check if the user was in the last of the roles would it not? – Samantha J T Star Oct 30 '14 at 18:12
  • 3
    Depending on your principal. If you are using a ClaimsPrincipal, then no, there is not a DB call for each one as it will get the roles from the Claim. – Excommunicated Oct 30 '14 at 21:08
  • 2
    +1 And that is the correct answer. @Pleun Identity framework won't issue a call to DB for role check. See answer from Hao Kung for explanation. – trailmax Oct 30 '14 at 22:19
  • Also, the roles collection can be cached in a cookie by specifying an attribute on the RoleProvider configuration in web.config. That means there is no DB lookup when you use User.IsInRole(rolename). – Paul Taylor Nov 04 '14 at 19:50
  • @PaulTaylor here you go: ClaimsPrincipal principal = Request.GetRequestContext().Principal as ClaimsPrincipal; var name = User.Identity.Name; var claimValue = principal.Claims.Where(c => c.Type == "sub").Single().Value; – Taiseer Joudeh Dec 31 '14 at 11:38
  • The user object is in the HttpContext for those who don't know: HttpContext.Current.User.IsInRole – Sal Jul 21 '17 at 19:30
2

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

Community
  • 1
  • 1
Hao Kung
  • 28,040
  • 6
  • 84
  • 93
  • Is it possible for this user login cookie to be changed by the user? I am concerned about a user changing there cookie and then doing something that they should not do. Also is there a better way I can do a role check inside of the method rather than the way I propose with three methods with an [Authorize] on each all pointing to the same private method? – Samantha J T Star Oct 30 '14 at 03:50
  • 3
    No the cookie is encrypted and should be safe from tampering, I don't think you can easily do a single Authorize with OR semantics, so having 3 separate methods each mapping to one specific Role seems fine. But you could write a manual If User.IsInRole() for each of role in the method directly – Hao Kung Oct 30 '14 at 21:53
  • The Roles argument to the Authorize attribute accepts a comma-delimited list of Rolenames. – Paul Taylor Nov 04 '14 at 19:51