JWT?
What is JWT?
JSON Web Token (JWT) is the approach of securely transmitting data across communication channel. For authentication and authorization, it uses the technique of passing digitally signed tokens. JWT comprises of three parts: Header, Payloads and Signature.
Header is used to identity the signing algorithm used and it appears like:
{ “alg”: “HS256”, “typ”: “JWT”}
Payload looks like:
{ “Name”: “Ramana Reddy,”Admin”: “true”,”iat”: “146565644”}
The signature is created by Base64 encoding Header and Payload as:
data = encoded( Header ) + “.” + encoded( Payload ) signature = HMACSHA256 (data, secret key);
More details about JWT can be referred from https://jwt.io/
JWT in Theory
JWT authentication process can be broken into following 4 steps-
1) User is validated against database and claims are generated based on user’s role.
2) Payload containing claims or other user related data is signed with key to generate token and passed back to user.
3) User sends this token with each request, normally in header or cookies and then received token is decrypted to validate claim.
4) Once user is identified, User is allowed to access Resource server based on his claim.
Advantage of Token based authentication paradigm is that instead of storing authentication or authorization related information linked to every user in session, a single signing key is stored at the authorizing server/service. Task of Authorization can be delegated to any server making it completely decoupled. Users are identified by verifying the claims which was generated in the first step based on his/her permission. Claims can be trusted because it was generated by server in the first step and then was digitally signed using one of the algorithm like HMAC SHA256. It is also assured that rights or claims has not been tampered with.
Unique thing here which saves lots of memory and adds to scalability is that only one key is required at server for decrypting the token and identifying the user, no matter what number of users it supports.
After identification is done, identity should persist for the current user throughout the request. This is where every implementation may differ. Next section covers all the four steps involved while using JWT token with ASP.NET Web API.
Implementing JWT with Asp.Net Web API
There are various libraries available for the second and third steps (i.e generating and verifying token) in almost all the languages. If you wish to continue without any library and write all steps yourself, many blogs contain elaborated details about each of the steps.
I used System.IdentityModel.Tokens.Jwt library for generating and validating tokens.
To implement JWT in Web API, I created a filter for authentication which will be executed before every request. It will verify the token contained in the request header and will deny/allow resource based on token. Filter is as follows:
public class JWTAuthenticationFilter : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext filterContext) { if (!IsUserAuthorized(filterContext)) { ShowAuthenticationError(filterContext); return; } base.OnAuthorization(filterContext); } }
I over-rode OnAuthorization method of AuthorizationFilter and injected logic to verify the Token. (Note that, Starting MVC 5, Authentication Filter is also available)
Before I elaborate how IsUserAuthorized Method works which contains all the logic, let me introduce the authentication module which does all the work related to decrypting, encrypting, signing & verifying the token.
AuthenticationModule is where the downloaded library is used.
public class AuthenticationModule { private const string communicationKey = "GQDstc21ewfffffffffffFiwDffVvVBrk"; SecurityKey signingKey = new InMemorySymmetricSecurityKey(Encoding.UTF8.GetBytes(communicationKey)); // The Method is used to generate token for user public string GenerateTokenForUser(string userName, int userId) { var signingKey = new InMemorySymmetricSecurityKey(Encoding.UTF8.GetBytes(communicationKey)); var now = DateTime.UtcNow; var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256Signature, SecurityAlgorithms.Sha256Digest); var claimsIdentity = new ClaimsIdentity(new List<Claim>() { new Claim(ClaimTypes.Name, userName), new Claim(ClaimTypes.NameIdentifier, userId.ToString()), }, "Custom"); var securityTokenDescriptor = new SecurityTokenDescriptor() { AppliesToAddress = "http://www.example.com", TokenIssuerName = "self", Subject = claimsIdentity, SigningCredentials = signingCredentials, Lifetime = new Lifetime(now, now.AddYears(1)), }; var tokenHandler = new JwtSecurityTokenHandler(); var plainToken = tokenHandler.CreateToken(securityTokenDescriptor); var signedAndEncodedToken = tokenHandler.WriteToken(plainToken); return signedAndEncodedToken; } /// Using the same key used for signing token, user payload is generated back public JwtSecurityToken GenerateUserClaimFromJWT(string authToken) { var tokenValidationParameters = new TokenValidationParameters() { ValidAudiences = new string[] { "http://www.example.com", }, ValidIssuers = new string[] { "self", }, IssuerSigningKey = signingKey }; var tokenHandler = new JwtSecurityTokenHandler(); SecurityToken validatedToken; try { tokenHandler.ValidateToken(authToken,tokenValidationParameters, out validatedToken); } catch (Exception) { return null; } return validatedToken as JwtSecurityToken; } private JWTAuthenticationIdentity PopulateUserIdentity(JwtSecurityToken userPayloadToken) { string name = ((userPayloadToken)).Claims.FirstOrDefault(m => m.Type == "unique_name").Value; string userId = ((userPayloadToken)).Claims.FirstOrDefault(m => m.Type == "nameid").Value; return new JWTAuthenticationIdentity(name) { UserId = Convert.ToInt32(userId), UserName = name }; } }
The Authentication module contains three methods-
1) GenerateTokenForUser is used for generating claim when user is authentic. This method contains all information we want to pass to and fro and uses secret key for signing based on algorithm specified. The encrypted information can be UserName, UserId, Roles, expiration time, etc.
2) GenerateUserClaimFromJWT receives the token from header and decrypts it to fetch claims.
3) PopulateUserIdentity is used to create the identity object after getting information from claims using library. I created object of JWTAuthenticationIdentity derived from GenericIdentity of System.Security.Principal namespace. This is so that I can store extra information like role, UserId, etc. The class looks like-
public class JWTAuthenticationIdentity : GenericIdentity { public string UserName { get; set; } public int UserId { get; set; } public JWTAuthenticationIdentity(string userName) : base(userName) { UserName = userName; } }
While logging for the first time, I invoked GenerateTokenForUser method of AuthenticationModule and returned the signed token when user is valid. (See, else part of below snippets)
[HttpPost] public HttpResponseMessage LoginDemo(string userName, string password) { MockAuthenticationService demoService = new MockAuthenticationService(); UserProfile user = demoService.GetUser(userName, password); if (user == null) { return Request.CreateResponse(HttpStatusCode.Unauthorized, "Invalid User", Configuration.Formatters.JsonFormatter); }else { AuthenticationModule authentication = new AuthenticationModule(); string token = authentication.GenerateTokenForUser(user.UserName, user.UserId); return Request.CreateResponse(HttpStatusCode.OK, token, Configuration.Formatters.JsonFormatter); } }
I made the request through Postman (chrome extension). Returned token is shown in screenshot-
In case of invalid user, the result is shown as-
The returned token is passed as header in each request and the actual verification is done through IsUserAuthorized method in our custom filter.
public bool IsUserAuthorized(HttpActionContext actionContext) { var authHeader = FetchFromHeader(actionContext); fetch authorization token from header if (authHeader != null) { var auth = new AuthenticationModule(); JwtSecurityToken userPayloadToken = auth.GenerateUserClaimFromJWT(authHeader); if (userPayloadToken != null) { var identity = auth.PopulateUserIdentity(userPayloadToken); string[] roles = { "All" }; var genericPrincipal = new GenericPrincipal(identity, roles); Thread.CurrentPrincipal = genericPrincipal; var authenticationIdentity = Thread.CurrentPrincipal.Identity as JWTAuthenticationIdentity; if (authenticationIdentity != null && !String.IsNullOrEmpty(authenticationIdentity.UserName)) { authenticationIdentity.UserId = identity.UserId; authenticationIdentity.UserName = identity.UserName; } return true; } } return false; }
A simple implementation to fetch authorization header-
private string FetchFromHeader(HttpActionContext actionContext) { string requestToken = null; var authRequest = actionContext.Request.Headers.Authorization; if (authRequest != null) { requestToken = authRequest.Parameter; } return requestToken; }
IsUserAuthorized method is now self-explanatory. The steps performed are invoking second and third method of AuthenticationModule (as explained earlier) to populate JWTAuthenticationIdentity object which represents the identity of current user, creating GenericPrincipal and assigning it to current principle of request thread. Thus, all the user related information is set here. These user data can be retrieved from identity object of current request.
Let’s try with incorrect token in the header. The method ShowAuthenticationError which is called through OnAuthorize method is described below:
private static void ShowAuthenticationError(HttpActionContext filterContext) { var responseDTO = new ResponseDTO() { Code = 401, Message = "Unable to access, Please login again" }; filterContext.Response = filterContext.Request.CreateResponse(HttpStatusCode.Unauthorized, responseDTO); }
The output in Postman is:
Now, if I pass the correct token generated, the output is shown below. It rightly fetches the list of books which was expected as per Action defined in our controller. Have a look-
[JWTAuthenticationFilter] public class DashboardController : BaseController { public HttpResponseMessage Get() { var listOfbooks = GetAllBooks(); return Request.CreateResponse(HttpStatusCode.OK, listOfbooks); } private List<Books> GetAllBooks() { List<Books> book = new List<Books>(); book.Add(new Books { Id = 1, Name = "ABC Books" }); book.Add(new Books { Id = 2, Name = "XYZ Books" }); book.Add(new Books { Id = 3, Name = "DEF Books" }); return book; } }
Notice the controller decorated with JWTAuthenticationFilter attributes.
Comments
Post a Comment