curiousBoy
curiousBoy

Reputation: 6834

How to implement jwt token base authentication with custom policy schema for authorization in .net core?

I am trying to create a playground app in .NET Core 3.1 by trying to implement the jwt token based authentication and would like to use my custom policy schema (scary?). I was able to do this pretty comfortable with customized filter attributes which was derived from AuthorizeAttribute in .NET Framework, but having hard time with .NET Core. Because I was using OnAuthorization hook and was capturing the HttpActionContext, resolving the token and checking role policy etc... But now I am using IAuthorizationHandler which I didn't have a chance to make it work in my desired way so far. I read MANY examples, articles but yet I couldn't find same approach to what I am trying to do.

(PS: When I search for hours and cant find similar approach that also makes me feel pretty nervous as I might be going on complete wrong route, or trying to re-invent the wheel.. Let's see if I am..)

I also looked for the IdendityServer4 (yet many people find it way easier to manage - but seems an overkill to me for what I am trying to do. Blame me if I am wrong.)

What I did so far is, I am able to create a token successfully when user logs in. Here is the code: (if you want to see what I am exactly asking, please directly scroll to the end but if you are going to answer please read through)

Behind the scene, I am using salted-hash password in db as well as key-strecthing with PBKDF2 algorithm ( I do appreciate any security concern)

My GenerateToken function:

[HttpPost("token")]
public IActionResult GenerateToken(UserCredentialDto userCredentialDto) {
    bool isValidUser = _appUserManager.IsValidCredentials(userCredentialDto);

    if (!isValidUser) {
        return BadRequest("invalid user/pass combination");
    }

    // assume I am getting all the roles that user has and add them in claims.
    var claims = _appUserManager.GetUserClaims(userCredentialDto); 
    var key = new SymmetricSecurityKey(_jwtSettings.Key);
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
    var token = new JwtSecurityToken(
    issuer: _jwtSettings.Issuer, audience: _jwtSettings.Audience, claims: claims, expires: DateTime.Now.AddMinutes(StaticAFUConfigHelper.TokenExpirationInMinutes), signingCredentials: creds);

    return Ok(new {
        token = new JwtSecurityTokenHandler().WriteToken(token)
    });
}

General background: (I am not using aspnet identity tables)

General workflow is:

So "basically", I would like to get the token (sent the token along with the request or in claims) , resolve it, check user has what I am looking for (specific policy etc..) and proceed based on that.

And in the startup.cs

//Authentication
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options = >{
    options.TokenValidationParameters = new TokenValidationParameters {
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidateLifetime = true,
        ValidateIssuerSigningKey = true,
        ValidIssuer = "my issuer",
        ValidAudience = "my audience",
        IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String("assume this is my secret key"))
    };
    options.SaveToken = true;
});

 // this part I'm also not sure as if I have 100s of policies, 
 // would all of them has to be defined here?
 // and how I specifically assign this to an api method! Anyways please keep reading if you dont mind
 services.AddAuthorization(options =>
                options.AddPolicy("CanReadData", policy => policy.Requirements.Add(new NeedsPolicyAttribute(PolicyEnum.CanReadData))));

Then I created TokenValidationHandler which is derived from AuthorizationHandler with my custom policy attribute NeedsPolicyAttribute ..

NeedsPolicyAttribute :

 public class NeedsPolicyAttribute: IAuthorizationRequirement {
    public PolicyEnum RequiredPolicy {
        get;
    }
    public NeedsPolicyAttribute(PolicyEnum requiredPolicy) {
        RequiredPolicy = requiredPolicy;
    }
}

And the HandleRequirementAsync is :

protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, NeedsPolicyAttribute requirement) {
    var myToken = "1234567889"; // just hardcoded for example - assume I got the JWT from the context.

    SecurityToken validatedToken;
    var handler = new JwtSecurityTokenHandler();

    // assume there was no exception and I was able to validate the token which is a valid token...
    var user = handler.ValidateToken(myToken, _jwtSettings.TokenValidationParameters, out validatedToken);

    // ************************........ATTENTION HERE....... *****************
    // I would like to check if the user has a role and which includes the policy which was required by the api method.
    //if so then, 
    context.Succeed(requirement);

    //if not then
    context.Fail();

    // and finally
    return Task.CompletedTask;
}

And my example API method is decorated as:

  [HttpGet][Route("get/{id}")]
  [Authorize("CanReadData")] // THIS JUST LETS ME TRIGGER MY CUSTOM ATTRIBUTE TO BE CAPTURED BY HandleRequirementAsync BUT I CAN ONLY PROVIDE HARDCODE STRING
   public ActionResult < AppUserDto > GetAppUser(int id) {
     return _appUserManager.Get(id);
}

What I actually want to do is, decorate my API Method with the required policy and validate if the given token has a claim with a role which includes that required policy.

Something like below:

  [HttpGet][Route("get/{id}")]
  [MyPolicyAttrubute(MyPolicyEnum.CanDoBlaBla)] // I want to capture this in HandleRequirementAsync if possible and compare with my user claims..
   public ActionResult < AppUserDto > GetAppUser(int id) {
     return _appUserManager.Get(id);
}

Breathtaking questions:

Upvotes: 1

Views: 1088

Answers (1)

curiousBoy
curiousBoy

Reputation: 6834

After spending hours, I was able to make this work. So I just wanted to post this as an answer to my question, but my "breathtaking questions" (see at the end of my question above) are still. So be aware this solution does not guarantee any of those concerns.

One of the concerns I had in the code piece in StartUp.cs in my question above was :

// if I have 100s of policies, would all of them have to be defined here?
 services.AddAuthorization(options =>
                options.AddPolicy("CanReadData", policy => policy.Requirements.Add(new NeedsPolicyAttribute(PolicyEnum.CanReadData))));

because the example codes were adding them one by one as hardcoded string which was bothering me since the beginning as I want to use Enum not hardcoded values. And I didn't want to add many lines in Startup.cs which was also needed to be updated everytime I add a new policy to application.

So that was easy actually. All I did was:

I wrote an extension to get all the enum values like below:

 public static class EnumUtils {
  public static IEnumerable < T > GetAllEnumValues < T > () {
   return System.Enum.GetValues(typeof(T)).Cast < T > ();
  }
 }

So I was able to use it like below. So that lets me use any new created Policy enum value on top of API methods as an attribute without touching StartUp.cs now.

    services.AddAuthorization(options => {
     // add all the policies to option to be able to use in ExtendedAuthorizeAttribute on api methods.
     foreach(var policyEnum in EnumUtils.GetAllEnumValues < PolicyEnum > ())
           options.AddPolicy(policyEnum.ToString(), policy => policy.Requirements.Add(new ExtendedAuthorizeAttribute(policyEnum)));
    });

Then I added the policies that user has:

public List < Claim > GetUserClaims(AuthRequestDto authRequestDto) {
    var userRoles = _unitOfWork.Roles.GetUserRoles(authRequestDto.UserId);
    var policies = userRoles.SelectMany(x = >x.RolePolicies.Where(p = >p.Policy.IsActive).Select(y = >y.Policy.Name)).Distinct().ToList();

    var claims = new List < Claim > ();
    policies.ForEach(policy = >claims.Add(new Claim("UserPolicy", policy)));
    claims.Add(new Claim("Id", authRequestDto.UserId.ToString()));
    return claims;
}

And attached them to my token so once user make a request with that token, I can resolve it and check against the required policy on api method.

Then I created new Attribute as ExtendedAuthorizeAttribute which derives from AuthroizeAttribute AND implements the IAuthorizationRequirement

So 2 things here: I derived my custom attribute from AuthroizeAttribute because I want it to triggered automatically for authorization to check if user has the required policy for that api method. And I implemented IAuthorizationRequirement because this lets me use my attribute as "requirement" in HandleRequirementAsync method.

So the attribute I created was:

/// <summary>
/// Extended Authorize Attribute is derived from Authorize Attribute
/// also implements IAuthorizationRequirement.
/// Deriving from AuthorizeAttribute accepts only string for policy names
/// By using this extension class, it let's me use Policy Enum then it converts it to string
/// before passing it to AuthorizeAttribute which was not possible in controller.  
/// </summary>
public class ExtendedAuthorizeAttribute: AuthorizeAttribute,
IAuthorizationRequirement {
    public ExtendedAuthorizeAttribute(PolicyEnum policyEnum = PolicyEnum.General) : base(policyEnum.ToString()) {}
}

And TokenValidationHandler became like below:

public class TokenValidationHandler: AuthorizationHandler < ExtendedAuthorizeAttribute > {
    private readonly JwtSettings _jwtSettings;
    private readonly IHttpContextAccessor _contextAccessor;

    public TokenValidationHandler(JwtSettings jwtSettings, IHttpContextAccessor contextAccessor) {
        _jwtSettings = jwtSettings;
        _contextAccessor = contextAccessor;
    }

    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, ExtendedAuthorizeAttribute requirement) {

        // injected the IHttpContextAccessor to get the token from the request.
        var rawToken = !_contextAccessor.HttpContext.Request.Headers.ContainsKey("Authorization") ? string.Empty: _contextAccessor ? .HttpContext ? .Request ? .Headers["Authorization"].ToString();

        if (string.IsNullOrEmpty(rawToken)) {
            context.Fail();
            return Task.CompletedTask;
        }

        var token = ScrubToken(rawToken);
        var handler = new JwtSecurityTokenHandler();

        try {
            // validates the given token and returns claims principal for user if validated.
            var user = handler.ValidateToken(token, _jwtSettings.TokenValidationParameters, out SecurityToken _);

            // Check if UserPolicies claims include the required the policy 
            if (IsRequiredPolicyExistOnUser(user.Claims ? .ToList(), requirement)) {
                context.Succeed(requirement);
            } else {
                context.Fail();
            }

        } catch(Exception e) {
            // TODO: Logging!
            context.Fail();
        }

        return Task.CompletedTask;
    }

    private bool IsRequiredPolicyExistOnUser(List < Claim > userClaims, ExtendedAuthorizeAttribute requirement) {
        return userClaims != null && userClaims.Any() && userClaims.Where(x = >x.Type == "UserPolicy").Any(c = >c.Value == requirement.Policy.ToString());
    }

    private string ScrubToken(string rawToken) {
        return rawToken.Replace("Bearer ", "");
    }
}

And finally I was able to use this on my api methods like below:

[HttpGet]
[Route("get/{id}")]
[ExtendedAuthorize(PolicyEnum.CanReadData)]
public ActionResult < AppUserDto > GetAppUser(int id) {
    return _appUserManager.Get(id);
}

and it worked just like the way I wanted. But again, breathtaking questions are still as of now!

Upvotes: 1

Related Questions