Reputation: 375
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
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