Wojciech X
Wojciech X

Reputation: 375

ReactJS + .NET Framework 4.7.2 - How to use AuthorizeAttribute in WebAPI

I have created simple Web application with Login mechanism using JSON Web Token (JWT). I generate TOKEN in my Web API and then, store it using Redux storage, so I can access it anywhere I want. It looks like this, I am using .subscribe() to track any changes in my loginStore:

  const loginStore = createStore(loginUserReducer);
  //After I log in, I put TOKEN to storage and reload my app
  loginStore.subscribe(() => {
    loginStore.getState().then(x => {
      if(x) { 
        localStorage.setItem('TOKEN', x);
      } else {
        localStorage.clear();
      }
      window.location.reload();
    });
  });

I created my Web API in .Net Framework 4.7.2. Next, I would like to use AuthorizeAttribute for authorization in my HomeController to protect my API from any unwanted calls:

public class HomeController : ApiController
{
    List<string> myList = new List<string>
    {
        "Element1",
        "Element2",
        "Element3"
    };

    [Authorize]//I use this for authorization -> using System.Web.Http;
    [HttpGet]
    [Route("api/mylist")]
    public List<string> MyList()
    {
        return this.myList;
    }
}

In the end, I use fetch() method and try to get data from myList from API and show it in console.log() at front.

  fetch('https://localhost:XXXXX/api/mylist', {
    method: 'GET',
    headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json'
    },
  }).then(x => {
    console.log(x.json());//Outcome from API
  }).catch(err => {
      console.log(err);
  });

What I get is an error message:

"Authorization has been denied for this request."

QUESTION: How do I pass my token/user data/whatever from React to Web API, so AuthorizeAttribute will allow it and return data I want ?

Any solution will be appreciated.

I am not using any user roles right now, but solution with custom attribute and roles validation will be event better :)

Upvotes: 1

Views: 2074

Answers (1)

Wojciech X
Wojciech X

Reputation: 375

Alright! I figured it out and it is actually really easy! I will add a long answer in case someone will need it.

Step 1: Use System.IdentityModel.Tokens.Jwt, Microsoft.AspNet.WebApi.Cors and Microsoft.AspNet.Cors for TOKEN generation.

Step 2: Implement static class TokenManager where you will generate/validate TOKEN and place there you user roles. (Implementation source)

public static class TokenManager
{
    private static string Secret = "my_secret_key";
    //Use
    private static JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();

    public static string GenerateToken(string username)
    {
        byte[] key = Convert.FromBase64String(Secret);
        var descriptor = GenerateTokenDescriptor(username, key);

        JwtSecurityToken token = handler.CreateJwtSecurityToken(descriptor);
        return handler.WriteToken(token);
    }

    private static SecurityTokenDescriptor GenerateTokenDescriptor(string username, byte[] key)
    {
        SymmetricSecurityKey securityKey = new SymmetricSecurityKey(key);

        SecurityTokenDescriptor descriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] {
                new Claim(ClaimTypes.Name, username),
                new Claim(ClaimTypes.Role, "Role1"),//<-- User role!
                new Claim(ClaimTypes.Role, "Role2")}),//<-- User role!
            Expires = DateTime.UtcNow.AddMinutes(30),//Token takes only UTC time
            SigningCredentials = new SigningCredentials(securityKey,
            SecurityAlgorithms.HmacSha256Signature)
        };

        return descriptor;
    }

    public static ClaimsPrincipal GetPrincipal(string token)
    {
        try
        {
            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
            JwtSecurityToken jwtToken = (JwtSecurityToken)tokenHandler.ReadToken(token);
            if (jwtToken == null)
                return null;
            byte[] key = Convert.FromBase64String(Secret);
            TokenValidationParameters parameters = new TokenValidationParameters()
            {
                RequireExpirationTime = true,
                ValidateIssuer = false,
                ValidateAudience = false,
                IssuerSigningKey = new SymmetricSecurityKey(key)
            };
            SecurityToken securityToken;
            ClaimsPrincipal principal = tokenHandler.ValidateToken(token,
                  parameters, out securityToken);
            return principal;
        }
        catch (Exception e)
        {
            return null;
        }
    }
}

NOTE: I have generated my_secret_key using this code:

HMACSHA256 hmac = new HMACSHA256();
string key = Convert.ToBase64String(hmac.Key);

Step 3: Create Custom attribute which will be used for authorization.

public class MyAuthorizeAttribute : AuthorizeAttribute
{
    private readonly string[] allowedroles;
    public MyAuthorizeAttribute(params string[] roles)
    {
        this.allowedroles = roles;
    }
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        //Default outcome 
        bool authorize = false;
        //Get TOKEN
        var authToken = actionContext.Request.Headers.Authorization?.Parameter;
        //Check if TOKEN has parameters
        if (authToken != null)
        {
            //Get roles from TOKEN
            List<string> userRoles = TokenManager.GetPrincipal(authToken).FindAll(ClaimTypes.Role).Select(x => x.Value).ToList();
            //Check if any of User Roles is allowed
            authorize = this.allowedroles.Any(x => userRoles.Any(y => y == x));
        }
        //return outcome
        return authorize;
    }
}

Step 4: Use your attribute in Controller

public class HomeController : ApiController
{
    List<string> myList = new List<string>
    {
        "Element1",
        "Element2",
        "Element3"
    };

    [MyAuthorizeAttribute("Role2")]//Add roles names in parameter
    [HttpGet]
    [Route("api/mylist")]
    public List<string> MyList()
    {
        return this.myList;
    }

    [HttpPost]
    [Route("api/login")]
    public HttpResponseMessage Login()
    {
        var myToken = TokenManager.GenerateToken("username");
        return Request.CreateResponse(HttpStatusCode.OK, myToken);
    }
}

Step 5: Save TOKEN in LOCAL STORAGE:

fetch('https://localhost:XXXXX/api/login', {
        method: 'post',
        headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
      }).then(x => {
          x.json().then(y => {
            localStorage.setItem('TOKEN', y);
            });
      }).catch(err => {
          console.log(err);
      });

Step 6: In the end, pass your TOKEN in the header of your request:

  fetch('https://localhost:XXXXX/api/mylist', {
    method: 'GET',
    headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + localStorage.getItem('TOKEN'),
        'Access-Control-Allow-Origin': '*'
    },
  }).then(x => {
    console.log(x.json());//Outcome from API
  }).catch(err => {
      console.log(err);
  });

Done, should work just fine.

Upvotes: 1

Related Questions