PROWAREtech
ASP.NET Core: Add JWT Authentication to MVC
This example mostly deals with the server implementation. See this article to learn how to GET from an endpoint and see this article to learn how to POST to an endpoint. See this example for one that deals with the client-side in Blazor (WebAssembly).
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, .NET 6 and .NET 8.
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. For .NET Core 3.1, use version 3.1.x; for .NET 6, use version 6.0.x; for .NET 8, use version 8.0.x.
The project should automatically have a "WeatherForecast.cs" file located in the project's root folder. It goes unmodified. If this file does not exist, create it!
// 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 .NET 8 Program.cs file as follows. The comments note where code has been added. The secret key can be stored in appsettings.json.
// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer; // NOTE: line is newly added
using Microsoft.IdentityModel.Tokens; // NOTE: line is newly added
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// NOTE: the following block of code is newly added
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "domain.com", // NOTE: USE THE REAL DOMAIN NAME
ValidateIssuer = true,
ValidIssuer = "domain.com", // NOTE: USE THE REAL DOMAIN NAME
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("396B5DD9-CC75-411C-9311-5B6E1F391B89")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED; REPLACE THIS GUID WITH A UNIQUE ONE
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.UseAuthentication(); // NOTE: line is newly added
app.Run();
Modify the .NET 6 Program.cs file as follows. The comments note where code has been added. The secret key can be stored in appsettings.json.
// Program.cs
using Microsoft.AspNetCore.Authentication.JwtBearer; // NOTE: line is newly added
using Microsoft.IdentityModel.Tokens; // NOTE: line is newly added
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
// NOTE: the following block of code is newly added
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = "domain.com", // NOTE: USE THE REAL DOMAIN NAME
ValidateIssuer = true,
ValidIssuer = "domain.com", // NOTE: USE THE REAL DOMAIN NAME
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("396B5DD9-CC75-411C-9311-5B6E1F391B89")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED; REPLACE THIS GUID WITH A UNIQUE ONE
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.UseAuthentication(); // NOTE: line is newly added
app.Run();
Modify the Startup.cs file as follows. 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", // NOTE: USE THE REAL DOMAIN NAME
ValidateIssuer = true,
ValidIssuer = "domain.com", // NOTE: USE THE REAL DOMAIN NAME
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes("396B5DD9-CC75-411C-9311-5B6E1F391B89")) // NOTE: THIS SHOULD BE A SECRET KEY NOT TO BE SHARED; REPLACE THIS GUID WITH A UNIQUE ONE
};
});
}
// 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("396B5DD9-CC75-411C-9311-5B6E1F391B89")); // NOTE: USE THE SAME KEY AS USED IN THE PROGRAM.CS OR STARTUP.CS FILE
var credentials = new SigningCredentials(secretkey, SecurityAlgorithms.HmacSha256);
var claims = new[] // NOTE: could also use List<Claim> here
{
new Claim(ClaimTypes.Name, user.username), // NOTE: this will be "User.Identity.Name" value; this could also specify the email address of the user as many sites use for the user name
new Claim(JwtRegisteredClaimNames.Sub, user.username), // NOTE: this could be the email
new Claim(JwtRegisteredClaimNames.Email, user.email),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString("N")) // NOTE: this could the unique ID assigned to the user by a database
};
var token = new JwtSecurityToken(issuer: "domain.com", audience: "domain.com", claims: claims, expires: DateTime.Now.AddMinutes(60), signingCredentials: credentials); // NOTE: USE THE REAL DOMAIN NAME
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);
form.reset();
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.
See this article to learn how to GET from an endpoint using HttpClient and see this article to learn how to POST to an endpoint also using HttpClient.