PROWAREtech
ASP.NET Core: Json Web Token Authentication Minimal Web API Example
This example deals mostly with the server-side implementation. It uses .NET 8/Visual Studio 2022 and its features are new and not included with .NET 7/.NET 6 or earlier versions.
Pay close attention to all the comments in the code particularly ones that begin with "NOTE:".
Create a new minimal Web API project (AOT) in Visual Studio.
Modify Program.cs
Modify the Program.cs file to look like this:
// Program.cs
using Microsoft.AspNetCore.Authentication.BearerToken; // NOTE: THIS CODE NEWLY ADDED
using System.Security.Claims; // NOTE: THIS CODE NEWLY ADDED
using System.Text.Json.Serialization;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddAuthentication().AddBearerToken(); // NOTE: THIS CODE NEWLY ADDED
builder.Services.AddAuthorization(); // NOTE: THIS CODE NEWLY ADDED
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default);
});
var app = builder.Build();
// NOTE: THE FOLLOWING BLOCK OF CODE NEWLY ADDED
app.MapPost("/signin", async (Signin signin) => // NOTE: THIS IS A POST REQUEST
{
if (signin.email != "test@test.com" && signin.pwd != "abc123") // NOTE: QUERY A DATABASE OR SOME OTHER DATA STORE FOR THE USER EMAIL AND PASSWORD HERE
return Results.NotFound();
var claims = new Claim[] { new Claim(ClaimTypes.Name, signin.email) };
return Results.SignIn(new ClaimsPrincipal(new ClaimsIdentity(claims, BearerTokenDefaults.AuthenticationScheme)));
});
// NOTE: THE FOLLOWING BLOCK OF CODE NEWLY ADDED
app.MapGet("/user", (ClaimsPrincipal user) =>
{
return Results.Ok($"You are {user.Identity?.Name}!");
}).RequireAuthorization(); // NOTE: THIS MAKES THE ENDPOINT REQUIRE THAT THE USER BE LOGGED IN
var sampleTodos = new Todo[] {
new(1, "Walk the dog"),
new(2, "Do the dishes", DateOnly.FromDateTime(DateTime.Now)),
new(3, "Do the laundry", DateOnly.FromDateTime(DateTime.Now.AddDays(1))),
new(4, "Clean the bathroom"),
new(5, "Clean the car", DateOnly.FromDateTime(DateTime.Now.AddDays(2)))
};
var todosApi = app.MapGroup("/todos").RequireAuthorization(); // NOTE: THIS LINE OF CODE MODIFIED; RequireAuthorization() MAKES THESE "todos" ENDPOINTS REQUIRE THAT THE USER BE LOGGED IN
todosApi.MapGet("/", () => sampleTodos);
todosApi.MapGet("/{id}", (int id) => sampleTodos.FirstOrDefault(a => a.Id == id) is { } todo ? Results.Ok(todo) : Results.NotFound());
app.UseDefaultFiles(); // NOTE: THIS CODE NEWLY ADDED
app.UseStaticFiles(); // NOTE: THIS CODE NEWLY ADDED
app.Run();
public record Signin(string email, string pwd); // NOTE: THIS CODE NEWLY ADDED
public record Todo(int Id, string? Title, DateOnly? DueBy = null, bool IsComplete = false);
[JsonSerializable(typeof(Signin))] // NOTE: THIS CODE NEWLY ADDED
[JsonSerializable(typeof(Todo[]))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}
Create jsonman.html
This is used to test the endpoints and adding it is optional.
Create a folder in the project's root named wwwroot.
Paste this HTML code into a new filed named jsonman.html located in the wwwroot folder:
<!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: 25vh;
}
</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>
After these changes, the project's solution explorer should look like this:
Run the Application
Hit CTRL+F5 to run the application and navigate to this URL: /jsonman.html
Select to POST to the endpoint /signin and enter a request body (in JSON) with the user's email and password ("test@test.com" and "abc123" in this example).
Copy just the Bearer accessToken to use in futher requests, as done below by adding the header {"Authorization":"Bearer <accessToken>"}
. NOTE: this is in JSON format but it is added as a standard HTTP header during the request.
To use a minimal Web API with Blazor, simply add the Authorization header using HttpClient
as done in the FetchData.razor page in this example.