PROWARE technologies
PROWARE technologies

Setup/Add JSON Web Token (JWT) Authentication to ASP.NET Core

It is very easy to enable JSON Web Tokens in a ASP.NET Core RESTful API Web Application. See this article for a quick tutorial on what the REST API is. See this article for help adding a REST API to an existing ASP.NET Core MVC Web Application.

To avoid using a database in this example, user email and password are hard coded. In production, users will probably be stored in a database of somekind.

NOTE: this example uses ASP.NET Core 3.1 but should be backwards compatible and hopefully forwards compatible, too.

Create a new ASP.NET Core RESTful API Web Application with the project name of "JWT" or open an existing project.

Use the NuGet Package Manager to install the Microsoft.AspNetCore.Authentication.JwtBearer package by Microsoft.

The project should automatically have a "WeatherForecast.cs" file located in the project's root folder. It goes unmodified.

// WeatherForecast.cs
using System;

namespace JWT
{
	public class WeatherForecast
	{
		public DateTime Date { get; set; }

		public int TemperatureC { get; set; }

		public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

		public string Summary { get; set; }
	}
}

Modify (or create) the "WeatherForecastController.cs" controller file as follows.

// WeatherForecastController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;


using Microsoft.AspNetCore.Authorization; // NOTE: line is newly added


namespace JWT.Controllers
{
	[ApiController]
	[Route("api/[controller]")] // NOTE: this line modified: "api/" was added
	public class WeatherForecastController : ControllerBase
	{
		private static readonly string[] Summaries = new[]
		{
			"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
		};

		private readonly ILogger<WeatherForecastController> _logger;

		public WeatherForecastController(ILogger<WeatherForecastController> logger)
		{
			_logger = logger;
		}

		[HttpGet]
		[Authorize] // NOTE: line is newly added; this will prevent unauthorized access
		public IEnumerable<WeatherForecast> Get()
		{
			var rng = new Random();
			return Enumerable.Range(1, 5).Select(index => new WeatherForecast
			{
				Date = DateTime.Now.AddDays(index),
				TemperatureC = rng.Next(-20, 55),
				Summary = Summaries[rng.Next(Summaries.Length)]
			})
			.ToArray();
		}
	}
}

Modify the Startup.cs file as follows (this is an ASP.NET Core 3.1 file; another version may vary slightly). The comments note where code has been added.

// Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;


using Microsoft.AspNetCore.Authentication.JwtBearer; // NOTE: line is newly added
using Microsoft.IdentityModel.Tokens; // NOTE: line is newly added


namespace JWT
{
	public class Startup
	{
		public Startup(IConfiguration configuration)
		{
			Configuration = configuration;
		}

		public IConfiguration Configuration { get; }

		// This method gets called by the runtime. Use this method to add services to the container.
		public void ConfigureServices(IServiceCollection services)
		{
			services.AddControllers();


			// NOTE: the following lines are newly added
			services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
			{
				options.TokenValidationParameters = new TokenValidationParameters
				{
					ValidateAudience = true,
					ValidAudience = "domain.com",
					ValidateIssuer = true,
					ValidIssuer = "domain.com",
					ValidateLifetime = true,
					ValidateIssuerSigningKey = true,
					IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("THIS IS THE SECRET KEY")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED
				};
			});


		}

		// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
		public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
		{
			if (env.IsDevelopment())
			{
				app.UseDeveloperExceptionPage();
			}


			app.UseAuthentication(); // NOTE: line is newly added
			app.UseStaticFiles(); // NOTE: line is newly added


			app.UseRouting();

			app.UseAuthorization();

			app.UseEndpoints(endpoints =>
			{
				endpoints.MapControllers();
			});
		}
	}
}

Now, create the "Login" controller.

// LoginController.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;

namespace JWT.Controllers
{
	public class UserModel
	{
		public string username { get; set; }
		public string password { get; set; }
		public string email { get; set; }
	}

	[ApiController]
	[Route("api/[controller]")]
	public class LoginController : ControllerBase
	{
		private string CreateJWT(UserModel user)
		{
			var secretkey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("THIS IS THE SECRET KEY")); // NOTE: SAME KEY AS USED IN Startup.cs FILE
			var credentials = new SigningCredentials(secretkey, SecurityAlgorithms.HmacSha256);

			var claims = new[] // NOTE: could also use List<Claim> here
			{
				new Claim(JwtRegisteredClaimNames.Sub, user.username),
				new Claim(JwtRegisteredClaimNames.Email, user.email),
				new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")) // this could the unique ID assigned to the user
			};

			var token = new JwtSecurityToken(issuer: "domain.com", audience: "domain.com", claims: claims, expires: DateTime.Now.AddMinutes(60), signingCredentials: credentials);
			return new JwtSecurityTokenHandler().WriteToken(token);
		}

		private UserModel Authenticate(UserModel login)
		{
			if(login.username == "test" && login.password == "abc123") // NOTE: in production, query a database for user information
				return new UserModel { username = login.username, email = "test@gmail.com" };
			return null;
		}

		[HttpPost]
		public async Task<IActionResult> Post([FromBody]UserModel login)
		{
			return await Task.Run(() =>
			{
			   IActionResult response = Unauthorized();

			   UserModel user = Authenticate(login);

			   if (user != null)
				   response = Ok(new { token = CreateJWT(user) });

			   return response;
		   });
		}
	}
}

Create the "wwwroot" folder in the project's root directory. Within this folder create a file named "index.html" with the following code.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>JSON-MAN</title>
    <style>
        * {
            font-family: sans-serif;
            font-size: 30px;
            color: blue;
        }

        body {
            padding: 3%;
        }

        input, table, td:last-of-type {
            width: 100%;
        }

        textarea {
            width: 100%;
            height: 300px;
        }
    </style>
    <script type="text/javascript">
/*
request = {
	verb: "GET POST PUT PATCH DELETE",
	path: "/api/",
	headers: {"header1":"value1","header2":"value2"},
	data: "{'is':'json'}",
	onprogress: function(percent){}
};
*/
function ajax2(request) {
	var obj = "object";
	if (typeof request != obj) { request = {}; }
	var undef = "undefined";
	var canPromise = (typeof Promise != undef);
	var xmlobj;
	if (typeof XMLHttpRequest != undef) {
		xmlobj = new XMLHttpRequest();
	}
	else if (typeof window.ActiveXObject != undef) {
		var aVersions = ["MSXML2.XMLHttp.5.0", "MSXML2.XMLHttp.4.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp", "Microsoft.XMLHttp"];
		for (var i = 0; i < aVersions.length; i++) {
			try {
				xmlobj = new ActiveXObject(aVersions[i]);
				break;
			} catch (err) {
				//void
			}
		}
	}
	if (typeof xmlobj != obj) {
		return {then:function(){return{catch:function(ca){ca("XMLHttpRequest object could not be created");}}}};
	}
	if(typeof request.onprogress == "function" && typeof xmlobj.upload == obj) {
		xmlobj.upload.addEventListener("progress", function (event) {
			request.onprogress(Math.floor(event.loaded / event.total * 100));
		});
	}
	// if no verb is specified then use "get"; if no path is specified then use the current file
	xmlobj.open(request.verb || "get", request.path || location.pathname, canPromise);
	xmlobj.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
	if(typeof request.headers == obj) {
		for(var prop in request.headers) {
			xmlobj.setRequestHeader(prop, request.headers[prop]);
		}
	}
	xmlobj.send(request.data || null);
	if(canPromise) {
		return new Promise(function (resolve, reject) {
			xmlobj.onreadystatechange = function () {
				if (xmlobj.readyState == 4) {
					if (xmlobj.status >= 200 && xmlobj.status < 300) {
						resolve(xmlobj.responseText);
					}
					else {
						reject(xmlobj.statusText);
					}
				}
			};
		});
	}
	else {
		if (xmlobj.status >= 200 && xmlobj.status < 300) {
			return {then:function(th){th(xmlobj.responseText);return{catch:function(){}}}};
		}
		else {
			return {then:function(){return{catch:function(ca){ca(xmlobj.statusText);}}}};
		}
	}
}
var headersobj = null;
function setHeadersColor(input) {
	try {
		headersobj = JSON.parse(input.value);
		if (Array.isArray(headersobj)) {
			headersobj = null;
			input.style.color = "red";
		}
		else {
			input.style.color = "#0b0";
		}
	}
	catch {
		headersobj = null;
		input.style.color = "red";
	}
}
function setBodyColor(input) {
	try {
		JSON.parse(input.value);
		input.style.color = "#0b0";
	}
	catch {
		input.style.color = "red";
	}
}
function submitRequestForm(form) {
	ajax2({
		verb: form.requestmethod.value,
		path: form.endpoint.value,
		headers: headersobj,
		data: form.requestbody.value
	}).then(function (txt) {
		form.responsebody.value = txt;
		return false;
	}).catch(function (err) {
		alert(err);
		return false;
	});
	return false;
}
    </script>
</head>
<body>
    <h1>JSON-MAN</h1>
    <form method="get" action="" onsubmit="return submitRequestForm(this);">
        <div>
            <table><tr><td><select name="requestmethod"><option>GET</option><option>POST</option><option>PUT</option><option>PATCH</option><option>DELETE</option></select></td><td><input type="text" name="endpoint" placeholder="ENDPOINT" /></td></tr></table>
        </div>
        <div>
            <input type="text" name="headers" placeholder='HEADERS EXAMPLE: {"header1":"value1","header2":"value2"}' onchange="setHeadersColor(this);" onkeyup="setHeadersColor(this);" autocomplete="off" />
        </div>
        <div>
            <textarea name="requestbody" placeholder="REQUEST BODY" onchange="setBodyColor(this);" onkeyup="setBodyColor(this);"></textarea>
        </div>
        <div>
            <textarea name="responsebody" placeholder="RESPONSE BODY" readonly></textarea>
        </div>
        <div>
            <button type="submit">submit</button>
        </div>
    </form>
</body>
</html>

Run the project using the Ctrl+F5 key combination and access the /index.html path. From here, the REST API endpoints can be accessed. Try issuing a GET of the /api/weatherforecast endpoint. The response is "Unauthorized." To access this URL, a user must be logged in. Login in from the /api/login endpoint by POSTing a request body of {"username":"test","password":"abc123"}. The response will be the JSON web token. Copy just the token to the computer clipboard. Try a GET request to the endpoint /api/weatherforecast with a header value of {"Authorization":"Bearer token-value-pasted-here"}. To secure a REST API controller, simply add the [Authorize] attribute to each endpoint or for the whole controller.

Coding Video

https://youtu.be/S8KDsW3YIes