Implement JWT authentication in ASP.NET Core minimal APIs

In the previous part
of this article series we learned to integrate Swagger with ASP.NET Core minimal
APIs. So far we created minimal APIs for performing CRUD operations. Now it's
time to add authentication and authorization to the minimal APIs. As with
controller based APIs the most common approach to implement authentication in
minimal APIs is to use JSON Web Token or JWT. To that end this part of this
series will cover that and will also tweak Swagger configuration to use JWT
while invoking the minimal APIs.
Open the same project that we have been building in the pervious parts of
this series.
Since we want to implement JWT authentication add
Microsoft.AspNetCore.Authentication.JwtBearer NuGet package to the project.

Then open appsettings.json and add the following configuration section to it:
"Jwt": {
"Issuer": "example.com",
"Audience": "example.com",
"Key": "This is super secret key for example.com"
}
The Jwt section stores three keys Issuer, Audience, and Key. If you are not
familiar with the general concepts of JWT authentication I would suggest you to
read my earlier articles
here, here, and
here.
We need these settings while configuring JWT authentication during the
application's startup. So, we will read them in the Program.cs as shown below:
builder.Services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = JwtBearerDefaults.
AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.
AuthenticationScheme;
o.DefaultScheme = JwtBearerDefaults.
AuthenticationScheme;
}).AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = false,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
The Add Authentication() method specifies the application's authentication
scheme to be JWT Bearer. The chained called to AddJwtBearer() configures various
token parameters using TokenValidationParameters object. Notice the parameters
marked in bold letters. We pick Issuer, Audience, and Key values from
appsettings.json and use them to set ValidIssuer, ValidAudience, and
IssuerSigningKey properties.
We will also add authorization services by calling:
builder.Services.AddAuthorization();
Before you go ahead, make sure that all the above code is place just before
builder.Build() call.
Since we have enabled JWT bearer authentication for out application we
can now authenticate our minimal APIs. To do so, add [Authorize] attribute to
all the five minimal APIs we created earlier. As an example, GET and POST
endpoints are shown below.
app.MapGet("/minimalapi/employees",
[Authorize](AppDbContext db) =>
{
return Results.Ok(db.Employees.ToList());
});
app.MapPost("/minimalapi/employees",
[Authorize](AppDbContext db, Employee emp) =>
{
db.Employees.Add(emp);
db.SaveChanges();
return Results.Created
($"/minimalapi/employees/{emp.EmployeeID}", emp);
});
As you can see, [Authorize] has been added to the endpoint handler functions
to indicate that the underlying handler can be invoked only by authenticated
user.
After all the "Map" methods, add this code to wire authentication and
authorization middleware.
...
app.UseAuthentication();
app.UseAuthorization();
app.UseSwagger();
...
We haven't written any code that issues a JWT or validates a user yet. But we
will go ahead and try to invoke our minimal APIs using Swagger just to check
what happens when an unauthenticated user tries to invoke the APIs.
So, run the application and invoke the GET endpoint using the Swagger UI.
This time you will get this error:

As you can see, the GET request is returning
HTTP status code 401 (Unauthorized). This is expected because we have
decorated [Authorize] attribute on the handler but haven't passed any JWT while
making the request.
To fix this we need to device a mechanism that issue us a JWT. We can then
pass this JWT while calling the minimal APIs.
Add a new class at the end of Program.cs (you can place it after the Employee
and AppDbContext classes) as shown below:
public class User
{
public string UserName { get; set; }
public string Password { get; set; }
}
The User class represents a system user and contains UserName and Password
properties.
Then add another POST endpoint just below the five minimal APIs we wrote
earlier.
app.MapPost("/minimalapi/security/getToken",
[AllowAnonymous](User user) =>
{
if (user.UserName=="user1" && user.Password=="password1")
{
var issuer = builder.Configuration["Jwt:Issuer"];
var audience = builder.Configuration["Jwt:Audience"];
var securityKey = new SymmetricSecurityKey
(Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]));
var credentials = new SigningCredentials(securityKey,
SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(issuer: issuer,
audience: audience,
signingCredentials: credentials);
var tokenHandler = new JwtSecurityTokenHandler();
var stringToken = tokenHandler.WriteToken(token);
return Results.Ok(stringToken);
}
else
{
return Results.Unauthorized();
}
});
The endpoint URL is /minimalapi/security/getToken and its handler accepts a
User object.
Inside, we check the UserName and Password against hard coded values (we will
integrate ASP.NET Core Identity in the next part of this series).
We read Issuer, Audience, and Key from the appsettings.json file. Based on
these values we create a new JwtSecurityToken object. To convert this
JwtSecurityToken into a string token we use JwtSecurityTokenHandler's WriteToken()
method.
The string token in wrapped in an OK response and sent back to the client.
To see the getToken handler in action you can run the application and invoke
the getToken endpoint using the Swagger UI.

As you can see the response body now contains the JWT sent by the getToken
endpoint. This indicates that our getToken endpoint is working as expected.
So far so good.
We need to pass a JWT obtained from the getToken endpoint with all the CRUD
calls. Since we are using Swagger UI to invoke the minimal APIs it would be nice
if we tell Swagger to pass our JWT while making the API calls. This requires a
bit of Swagger configuration. Take a look at this additional configuration
below:
var securityScheme = new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JSON Web Token based security",
};
var securityReq = new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
};
builder.Services.AddEndpointsApiExplorer();
The above code creates objects of OpenApiSecurityScheme and
OpenApiSecurityRequirement respectively.
These objects specify the authentication scheme related details such as
scheme and bearer format. These objects are then supplied to the AddSwaggerGen()
method as shown below:
builder.Services.AddSwaggerGen(o =>
{
o.SwaggerDoc("v1", info);
o.AddSecurityDefinition("Bearer", securityScheme);
o.AddSecurityRequirement(securityReq);
});
We pass these objects using AddSecurityDefinition() and
AddSecurityRequirement() methods.
If you run the Swagger UI again, you will notice that it now shows the
Authorize button at the top.

Now execute the getToken API again and copy the returned JWT as discussed
earlier.
Then click on the Authorize button to open the following dialog.

Paste the JWT copied earlier into the Value textbox. Notice how the JWT is
specified in the authorization header. It's Bearer, then a white space, followed
by the JWT string. Make sure to remove the
double quotes from the JWT upon pasting. Then click on the Authorize button.

Now click on the Close button to close the dialog.
Now that the authorization header has been assigned a value, we can invoke
the CRUD operations. The following figures shows a successful run of the GET
request.

As you can see a list of employees has been successfully returned from the
minimal API.
In this example we used hard coded user name and password values for the sake
of quickly testing the JWT authentication. Wouldn't it be nice if we plug-in
ASP.NET Core Identity to create and validate user accounts? That's what we will
do in the next part of this article series.
That's it for now! Keep coding!!