Pass a JSON Web Token (JWT) to minimal API using fetch()
In the previous article we learned to call Web API and minimal API using fetch(). Many a times your APIs are secured using JSON Web Token (JWT) based authentication scheme. And you need to pass the JWT while making API calls. In this part of the article we will learn to do just that.
Open the same project we were working on in the previous part of this article series. We will first enable JWT authentication for our project.
Add the Microsoft.AspNetCore.Authentication.JwtBearer NuGet package in the project using Manage NuGet Packages dialog.
Then add the following code in Program.cs just below the AddRazorPages() call.
// previous lines truncated
builder.Services.AddControllers();
builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters =
new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes
("This is super secret key"))
};
});
var app = builder.Build();
// next lines truncated
As you can see we registered authentication and authorization services with the DI container. We added JWT authentication scheme using AddAuthentication() method. Next, we configured the JWT properties. To keep things simple we use only token signing key. Features such as issuer, audience, and lifetime validation are turned off. In a more realistic case you would use some cryptic and protected key instead of "This is super secret key".
After enabling JWT we wire authentication and authorization middleware as shown below:
// previous lines truncated
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapDefaultControllerRoute();
app.MapRazorPages();
// next lines truncated
We have already created minimal APIs using Map*() methods such as MapGet(), MapPost(), MapPut(), and MapDelete(). We need one more endpoint that can create and return a JWT for our client UI.
Add the following API endpoint that does just that:
app.MapGet("/minapi/getToken",
[AllowAnonymous] () =>
{
var securityKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes
("This is super secret key"));
var credentials = new
SigningCredentials(securityKey,
SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken
(signingCredentials:credentials);
var tokenHandler = new JwtSecurityTokenHandler();
var stringToken = tokenHandler.WriteToken(token);
return Results.Ok(stringToken);
});
The getToken GET handler creates a new JwtSecurityToken object by passing the SigningCredentials. The JwtSecurityTokenHandler class is used to write the token into a string using its WriteToken() method. The JWT string is returned to the caller using Ok() method.
Note that in a more realistic case you will have a login page or dialog that captures user credentials. You will then validate the credentials against some data store. You might integrate ASP.NET Core Identity to manage users. But to remain focused on the main theme of this article we will skip all that configuration and just create sample JWT for our purpose. You may read more details of JWT and minimal APIs here and here.
So far so good.
We will now modify the callApi() JavaScript method we wrote earlier to include a JWT parameter. The modified callApi() function is shown below:
async function callApi(url, verb, data, token)
{
const request = new Request(url);
let options = {
method: verb,
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
};
if (data != null) {
options.body =
JSON.stringify(data);
}
if (token != null) {
options.headers.Authorization =
"Bearer " + token;
}
const response = await fetch
(request, options);
if (!response.ok) {
message.innerHTML =
"Error : " + response.status;
}
else{
message.innerHTML = "Success";
}
if(verb == "GET") {
const json = await response.json();
return json;
}
}
Notice the code shown in bold letters. The callApi() now has token parameter. Inside, we add the token parameter in the headers object. Notice that the headers now has Authorization property. The Authorization property has value in the form -- Bearer <token_here>. The other parts of the callApi() remain unchanged and are discussed in the previous part of this article.
Next, we will have to decorate all the minimal API Map*() handlers with [Authorize] attribute. As an example, the MapGet() handlers are shown below but make sure to add [Authorize] to all the handlers.
app.MapGet("minapi/Customers",
[Authorize] async (AppDbContext db) => {
List<Customer> data =
await db.Customers.ToListAsync();
return Results.Ok(data);
});
app.MapGet("minapi/Customers/{id}",
[Authorize] async (AppDbContext db, string id) => {
Customer emp =
await db.Customers.FindAsync(id);
return Results.Ok(emp);
});
Now we can call the getToken minimal API and grab a JWT that can accompany our CRUD requests. This is done in DOMContentLoaded JavaScript event handler. Look at the code below:
document.addEventListener("DOMContentLoaded",
async function () {
var customerid =
document.getElementById("customerid");
var newcustomerid =
document.getElementById("newcustomerid");
var companyname =
document.getElementById("companyname");
var contactname =
document.getElementById("contactname");
var country =
document.getElementById("country");
var insert = document.getElementById("insert");
var update = document.getElementById("update");
var del = document.getElementById("delete");
var message = document.getElementById("message");
const token =
await callApi("/minapi/getToken","GET");
const json =
await callApi("/minapi/Customers", "GET", null, token);
json.forEach(function(customer){
const option = document.createElement('option');
option.value = customer.customerID;
option.innerHTML = customer.customerID;
customerid.appendChild(option);
});
message.innerHTML = "Customers fetched successfully.";
// next lines truncated
});
Notice the code shown in bold letters. We first call getToken handler using callApi() function. The returned JWT is stored in a variable for use in other event handlers.
Then we make a GET request to the Customers API and pass the token as fourth parameter of callApi(). On the similar lines all the other calls to Customers API will change. For your quick reference all the callApi() calls from dropdown list change and button click event handlers are given below.
// DOMContentLoaded event
const json = await callApi
("/minapi/Customers", "GET", null, token);
// customerid change
const json = await callApi
("/api/Customers/" + customerid.value,
"GET", null, token);
// insert click
await callApi("/api/Customers",
"POST", data, token);
// update click
await callApi("/api/Customers/" + customerid.value,
"PUT", data, token);
// delete click
await callApi("/api/Customers/" +
customerid.value, "DELETE", null, token);
const json = await callApi("/api/Customers",
"GET", null, token);
Modify all the event handlers using the above calls. As you can see all the callApi() calls have four parameters; the last parameter being the JWT created earlier.
Run the application and check whether CRUD operations work as expected. The following figure shows a failed Customers API call if don't supply a valid JWT.
And the following figures shows a successful run upon supplying a valid JWT.
That's it for now! Keep coding!!