This article cover in detail JWT token usage in .Net MVC Web application.
- So why JWT (JSON Web Token) required while making a WEB API call.
JSON Web Tokens are a good way of securely transmitting information between client application and WEB API.
How to make WEB API call using JWT token
Before understanding code lets first understand architecture of the project.
As highlighted in below screenshot of solution explorer.
- MVC web application: This is main web project which will show result to end user on web pages.
- Web API Service: web application makes call to service layer to retrieve data from database.
- Data Access Layer: Web API connect to SQL database through data access layer.
Step 1: .Net MVC Web application calling JQuery function MakeWebApiCall
Below code is written on “.cshtml” under view folder. This code makes call to Jquery function “MakeWebApiCall.
Step 2: MVC Web Application under script folder
Below is Jquery function makes a call to Web API to get data from database and bind that data to drop down control on ".cshtml" view.
This function is written on apiaccess.Js file.
function MakeaWebApiCall(webapiurl) {
try {
$.ajax({
async: true,
type: "GET",
url: webapiurl,
contentType: "application/json; charset=utf-8",
dataType: "json",
headers: {
'CustomAuthorization': 'Bearer ' + getToken()
},
success: function (data)
{
LoadDropdown(data);
}
});
}
catch (ex) { }
}
While making a WebAPI call from web application you need to provide JWT token for validation. If this token is not provided then request to WEB API will be failed with Unauthorize error. In this way if someone try to call Web API URL from outside of application it will fail with error. For every call a fresh token is created and stored in session.
Step 3: MVC Web Application under script folder
Below two Jqery functions are written in apiaccess.Js file to get new JWT token.
function getToken()
{
let accessToken = sessionStorage.getItem("AccessToken");
if (accessToken)
{
let expiresOn = sessionStorage.getItem("TokenExpiry");
if (!expiresOn && new Date(expiresOn) < new Date())
{
getRefreshToken();
}
}
else
{
getRefreshToken();
}
accessToken = sessionStorage.getItem("AccessToken");
return accessToken;
}
function getRefreshToken() {
$.ajax({
async: false,
type: "GET",
url: '/home/refreshtoken',
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
sessionStorage.setItem("AccessToken", JSON.parse(data).Token);
sessionStorage.setItem("TokenExpiry", JSON.parse(data).ExpiryDate);
},
error: function (e) {
console.log(e.message);
}
});
}
Step 4: Calling MVC Web application controller
From step 3 getRefreshToken() token Jquery function makes a call to RefreshToken() funtion written in homeController of MVC Web application.
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Reporting.Models;
using System.Net.Http;
using System.Net.Http.Headers;
using Newtonsoft.Json;
namespace Reporting.Controllers
{
[CustomAuthorize]
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
private void ConfigureHttpClient(HttpClient client)
{
string apiName = "API/";
string apiUrl = ConfigurationManager.AppSettings["WebAPIUrl"];
if (apiUrl.Contains(apiName))
{
apiUrl = apiUrl.Replace(apiName, "");
}
// Log.Info("ConfigureHttpClient():"+ apiUrl);
client.BaseAddress = new Uri(apiUrl);//new Uri(ConfigurationManager.AppSettings["WebAPIUrl"]);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
public JsonResult RefreshToken()
{
string client = Encryption.DecryptFromBase64String(ConfigurationManager.AppSettings["ClientSecret"].ToString()
, Encoding.Base64Decode(ConfigurationManager.AppSettings["EncKey"]));
string phrase = Encryption.DecryptFromBase64String(ConfigurationManager.AppSettings["PassPhrase"].ToString()
, Encoding.Base64Decode(ConfigurationManager.AppSettings["EncKey"]));
var clientSecret = CryptoUtil.OpenSslEncrypt(client, phrase);
TokenRequest tokenRequest = new TokenRequest
{
ClientSecret = clientSecret,
Username = User.Identity.Name
};
using (var httpClient = new HttpClient())
{
var response = httpClient.PostAsJsonAsync(ConfigurationManager.AppSettings["WebAPIAuthUrl"] + "Authenticate", tokenRequest).Result;
if (!response.IsSuccessStatusCode)
{
//LOG ERROR WITH Statuscode
var responseContent = string.Concat("Status Code:" + response.StatusCode.GetHashCode(), " Reason:", response.ReasonPhrase);
// TODO: log error
throw new Exception(responseContent);
}
TokenResponse tokenResponse = JsonConvert.DeserializeObject<TokenResponse>(response.Content.ReadAsStringAsync().Result);
this.Session["AccessToken"] = tokenResponse;
//Log.Info("Token Response in RefreshToken:"+ tokenResponse.ToString());
return Json(JsonConvert.SerializeObject(tokenResponse), JsonRequestBehavior.AllowGet);
}
}
}
}
Step 5
From step 4 Refreshetoken() function makes a call to WEBAPI “Autheticate”.
Below Web API class is having Authenticate function which will generate token and send back to step 4.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Service.Models;
using System.Configuration;
using Common.All;
using Common.All.Logging;
namespace WEBAPI.Service.Controllers
{
public class AccountController : ApiController
{
[HttpPost]
public IHttpActionResult Authenticate([FromBody] TokenRequest tokenrequest)
{
//Log.Info("Service-Account controller-authenticate called.");
IHttpActionResult response;
using (HttpResponseMessage responseMsg = new HttpResponseMessage())
{
bool isclientSecretValid = false;
if (tokenrequest != null)
{
string ServiceSecret = Encryption.DecryptFromBase64String(ConfigurationManager.AppSettings["ServiceSecret"].ToString()
, Encoding.Base64Decode(ConfigurationManager.AppSettings["EncKey"]));
string Servicephrase = Encryption.DecryptFromBase64String(ConfigurationManager.AppSettings["Servicephrase"].ToString()
, Encoding.Base64Decode(ConfigurationManager.AppSettings["EncKey"]));
string clientSecret = CryptoUtil.OpenSslDecrypt(tokenrequest.ClientSecret, phrase);
isclientSecretValid = ServiceSecret == clientSecret ? true : false;
}
else
{
// if credentials are not valid send unauthorized status code in response
responseMsg.StatusCode = HttpStatusCode.Unauthorized;
response = ResponseMessage(responseMsg);
return response;
}
// if credentials are valid
if (isclientSecretValid)
{
TokenResponse tokenResponse = GenerateToken(tokenrequest.Username.ToLower());
return Ok(tokenResponse);
}
else
{
// if credentials are not valid send unauthorized status code in response
responseMsg.StatusCode = HttpStatusCode.Unauthorized;
response = ResponseMessage(responseMsg);
return response;
}
}
}
private TokenResponse GenerateToken(string username)
{
//Log.Info("Service-Account controller-GenerateToken called.");
//Set issued at date
DateTime issuedAt = DateTime.UtcNow;
//set the time when it expires
DateTime expires = DateTime.UtcNow.AddDays(7);
var tokenHandler = new JwtSecurityTokenHandler();
//create a identity and add claims to the user which we want to log in
ClaimsIdentity claimsIdentity = new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.Name, username)
});
string sec = ConfigurationManager.AppSettings["secretkey"].ToString();
var now = DateTime.UtcNow;
var securityKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(sec));
var signingCredentials = new Microsoft.IdentityModel.Tokens.SigningCredentials(securityKey, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256Signature);
//create the jwt
var token =
(JwtSecurityToken)
tokenHandler.CreateJwtSecurityToken(issuer: ConfigurationManager.AppSettings["issuer"], audience: ConfigurationManager.AppSettings["audience"],
subject: claimsIdentity, notBefore: issuedAt, expires: expires, signingCredentials: signingCredentials);
var tokenString = tokenHandler.WriteToken(token);
//Log.Info("Service-Account controller-GenerateToken-tokenString:"+ tokenString);
TokenResponse tokenResponse = new TokenResponse
{
Token = tokenString,
ExpiryDate = expires
};
return tokenResponse;
}
}
}
Below two extra classes required for above class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Service.Models
{
public class TokenRequest
{
public string Username { get; set; }
public string ClientSecret { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Service.Models
{
public class TokenResponse
{
public string Token { get; set; }
public DateTime ExpiryDate { get; set; }
}
}
Step 6: WEB API Service project
When MVC web application makes a token based call to WEB API, below code in Web API service project validates in coming request based on token provided.
Register TokenValidationHandler class. This class validate JWT token passed from MVC application. Token Validation handler class exist in Web API service Layer under Model folder.
Below is TokenValidationHandler.cs
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Service.Models;
namespace Service
{
internal class TokenValidationHandler : DelegatingHandler
{
// private ILogger _logger;
private static bool TryRetrieveToken(HttpRequestMessage request, out string token)
{
//Log.Info("TryRetrieveToken() called.");
token = null;
IEnumerable<string> authzHeaders;
if (!request.Headers.TryGetValues("CustomAuthorization", out authzHeaders) || authzHeaders.Count() > 1)
{
return false;
}
var bearerToken = authzHeaders.ElementAt(0);
token = bearerToken.StartsWith("Bearer ") ? bearerToken.Substring(7) : bearerToken;
return true;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// _logger = new FileLogger();
//Log.Info("SendAsync() called inside service TokenValidationHandler.cs.");
HttpStatusCode statusCode;
string token;
//determine whether a jwt exists or not
if (!TryRetrieveToken(request, out token))
{
statusCode = HttpStatusCode.Unauthorized;
//allow requests with no token - whether a action method needs an authentication can be set with the claimsauthorization attribute
return base.SendAsync(request, cancellationToken);
}
// _logger.LogInfo("token: "+token + Environment.NewLine);
try
{
string sec = ConfigurationManager.AppSettings["secretkey"].ToString();
var now = DateTime.UtcNow;
var securityKey = new SymmetricSecurityKey(System.Text.Encoding.Default.GetBytes(sec));
SecurityToken securityToken;
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
TokenValidationParameters validationParameters = new TokenValidationParameters()
{
ValidAudience = ConfigurationManager.AppSettings["audience"],
ValidIssuer = ConfigurationManager.AppSettings["issuer"],
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
LifetimeValidator = this.LifetimeValidator,
IssuerSigningKey = securityKey
};
//extract and assign the user of the jwt
Thread.CurrentPrincipal = handler.ValidateToken(token, validationParameters, out securityToken);
HttpContext.Current.User = handler.ValidateToken(token, validationParameters, out securityToken);
// _logger.LogInfo("before calling base.SendAsync(request, cancellationToken);");
return base.SendAsync(request, cancellationToken);
}
catch (SecurityTokenValidationException e)
{
statusCode = HttpStatusCode.Unauthorized;
// _logger.LogError("Exception in TokenvalidationHandler:" + statusCode.ToString(), e);
}
catch (Exception ex)
{
statusCode = HttpStatusCode.InternalServerError;
// _logger.LogError(ex.Message,ex);
}
return Task<HttpResponseMessage>.Factory.StartNew(() => new HttpResponseMessage(statusCode){ });
}
public bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
if (expires != null)
{
if (DateTime.UtcNow < expires) return true;
}
return false;
}
}
}
Step 7: Config file settings
Few keys used in code from MVC web application .config file.
<add key="ClientSecret" value="/F0Jv7lxY1S1Iatj60WYs194eZiB0In+AF2PN23REnc=" />
<add key="PassPhrase" value="/F0Jv7lxY1RTZhX9G+8xGrlaJ+CHzode0t3WTncI6HSkP/q+k1knFw==" />
<add key="WebAPIUrl" value="http://localhost:1234/api" />
<add key="WebAPIAuthUrl" value=”http://localhost:1234/api/Account/” />
<add key="EncKey" value="STUrIUJDZktQKWdWRVlwLA==" />
Few Keys used in Web API service Layer
<add key="EncKey" value="STUrIUJDZktQKWdWRVlwLA==" />
<add key="ClientSecret" value="/F0Jv7lxY1S1Iatj60WYs194eZiB0In+AF2PN23REnc=" />
<add key="PassPhrase" value="/F0Jv7lxY1RTZhX9G+8xGrlaJ+CHzode0t3WTncI6HSkP/q+k1knFw==" />
<add key="secretkey"
value="202b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429090fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65tr5" />
<add key="audience" value="MVC-Client" />
<add key="issuer" value="API-Service" />