Web API: Securing Minimal APIs with JWT

By | 26/07/2023

In this post, we will see how to implement JWT (JSON Web Token) authentication in Minimal APIs.
We are going to use the same project that we created in the post “Web API – Minimal API with ASP.NET Core“, only changing the type of Dog.Id from Guid to int.
These are all the classes:

[DOGS.CS]

namespace MinimalAPI.Model;

public class Dog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Breed { get; set; }
    public string Color { get; set; }   
}


[DATACONTEXT.CS]

using Microsoft.EntityFrameworkCore;

namespace MinimalAPI.Model;

public class DataContext: DbContext
{
    public DataContext(DbContextOptions<DataContext> options)
    : base(options) { }

    public DbSet<Dog> Dogs => Set<Dog>();
}


[IDOGCOMMANDS.CS]

using MinimalAPI.Model;

namespace MinimalAPI.Commands;

public interface IDogCommands
{
    Task<bool> AddDog(Dog dog);

    Task<List<Dog>> GetAllDogs();

    Task<Dog> GetDogById(int id);

    Task<bool> UpdateDog(Dog dog, int id);

    Task<bool> DeleteDog(int id);

    Task Save();
}


[DOGCOMMANDS.CS]

using Microsoft.EntityFrameworkCore;
using MinimalAPI.Model;

namespace MinimalAPI.Commands;

public class DogCommands:IDogCommands
{
    private readonly DataContext _dataContext;

    public DogCommands(DataContext dataContext)
    {
        _dataContext = dataContext;
    }

    public async Task<bool> AddDog(Dog dog)
    {
        try
        {
            await _dataContext.Dogs.AddAsync(dog);
            return true;
        }
        catch (Exception)
        {
            return false;
        }
    }

    public async Task<List<Dog>> GetAllDogs()
    {
        return await _dataContext.Dogs.AsNoTracking().ToListAsync();
    }

    public async Task<Dog> GetDogById(int id)
    {
        return await _dataContext.Dogs.FirstOrDefaultAsync(d => d.Id == id);
    }

    public async Task<bool> UpdateDog(Dog dog, int id)
    {
        var dogInput = await _dataContext.Dogs.FirstOrDefaultAsync(d => d.Id == id);

        if(dogInput == null)
        {
            return false;
        }

        dogInput.Name = dog.Name;
        dogInput.Color = dog.Color;
        dogInput.Breed = dog.Breed;

        await _dataContext.SaveChangesAsync();

        return true;
    }

    public async Task<bool> DeleteDog(int id)
    {
        var dogInput = await _dataContext.Dogs.FirstOrDefaultAsync(d => d.Id == id);

        if (dogInput == null)
        {
            return false;
        }

        _dataContext.Dogs.Remove(dogInput);
        return true;
    }

    public async Task Save()
    {
        await _dataContext.SaveChangesAsync();
    }
}


[PROGRAM.CS]

using Microsoft.EntityFrameworkCore;
using MinimalAPI.Commands;
using MinimalAPI.Model;

var builder = WebApplication.CreateBuilder(args);
// definition of DataContext
builder.Services.AddDbContext<DataContext>(opt => opt.UseInMemoryDatabase("DbDog"));
// definition of Dependency Injection
builder.Services.AddScoped<IDogCommands, DogCommands>();

// Add services to the container.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

// Definition Get Method
app.MapGet("/dog", async (IDogCommands commands) =>
    await commands.GetAllDogs());

// Definition Get{Id} Method
app.MapGet("/dog/{id}", async (int id, IDogCommands commands) =>
{
    var dog = await commands.GetDogById(id);

    if (dog == null) return Results.NotFound();

    return Results.Ok(dog);
});

// Definition Post Method
app.MapPost("/dog", async (Dog dog, IDogCommands commands) =>
{
    await commands.AddDog(dog);
    await commands.Save();

    return Results.Ok();
});

// Definition Put Method
app.MapPut("/dog/{id}", async (int id, Dog dog, IDogCommands commands) =>
{
    var updateOk = await commands.UpdateDog(dog, id);

    if (!updateOk) return Results.NotFound();

    return Results.NoContent();
});

// Definition Delete Method
app.MapDelete("/dog/{id}", async (int id, IDogCommands commands) =>
{
    var deleteOk = await commands.DeleteDog(id);
    if (deleteOk)
    {
        await commands.Save();
        return Results.Ok();
    }

    return Results.NotFound();
});

app.Run();



If we run the application, the following will be the result:


Using a tool like Insomnia, we can check that everything works fine:

adding a new dog:


selecting all dogs:


selecting a specific dog:


updating a dog:


deleting a dog:


Now, we will modify the code in order to implement the authentication with JWT.
First of all, we add two NuGet packages using the commands:

Install-Package Microsoft.AspNetCore.Authentication.JwtBearer

Install-Package System.IdentityModel.Tokens.Jwt


Then, we will add the code to implement JWT.
We start to create a class called AuthenticationExtensions, used to add JWT Bearer Authentication:
[AUTHENTICATIONEXTENSIONS.CS]

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;

namespace MinimalAPI.Extensions;

public static class AuthenticationExtensions
{
    public static IServiceCollection AddJwtAuthentication(this IServiceCollection services, string jwtSecretKey)
    {
        // Add JWT Bearer Authentication
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                // Configure token validation parameters
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    // Validate the signature of the token
                    ValidateIssuerSigningKey = true,
                    // Set the secret key used to validate the token's signature
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSecretKey)),
                    // Skip issuer validation (optional)
                    ValidateIssuer = false,
                    // Skip audience validation (optional)
                    ValidateAudience = false,
                    // Validate the token's expiration time
                    ValidateLifetime = true,
                    // Set the tolerance for validating the token's expiration time
                    ClockSkew = TimeSpan.Zero,
                    // Require the token to have an expiration time
                    RequireExpirationTime = true,
                    LifetimeValidator = (before, expires, token, parameters) =>
                    {
                        var tokenLifetimeMinutes = (expires - before)?.TotalMinutes;
                        return tokenLifetimeMinutes <= 10; // Set the maximum token lifetime to 10 minutes
                    }
                };
            });

        return services;
    }
}


Then, we add a class called TokenGenerator, used to generate a Token:
[TOKENGENERATOR.CS]

using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace MinimalAPI.Extensions;

public static class TokenGenerator
{
    // Generates a JWT token with the specified secret key and token expiry time
    public static string GenerateToken(string jwtSecretKey, int tokenExpiryMinutes)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(jwtSecretKey);

        // Configure the token descriptor
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new List<Claim>
                {
                    new Claim(ClaimTypes.Name, "token_user") // Set the claim with the name of the token user
                }),
            Expires = DateTime.UtcNow.AddMinutes(tokenExpiryMinutes), // Set the token expiration time
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) // Set the signing credentials for the token
        };

        // Create the JWT token based on the token descriptor
        var token = tokenHandler.CreateToken(tokenDescriptor);

        // Write the JWT token as a string
        var generatedToken = tokenHandler.WriteToken(token);

        return generatedToken;
    }

    // Generates a JWT token endpoint result for use in an API controller
    public static IActionResult GenerateTokenEndpoint(string jwtSecretKey, int tokenExpiryMinutes)
    {
        // Generate a JWT token using the provided secret key and token expiry time
        var token = GenerateToken(jwtSecretKey, tokenExpiryMinutes);

        // Return the generated token as an OK response
        return new OkObjectResult(token);
    }
}


Finally, we modify Program.cs to implement JWT:
[PROGRAM.CS]

using Microsoft.EntityFrameworkCore;
using MinimalAPI.Commands;
using MinimalAPI.Extensions;
using MinimalAPI.Model;

var builder = WebApplication.CreateBuilder(args);

// Define the DataContext for the application and configure it to use an in-memory database
builder.Services.AddDbContext<DataContext>(opt => opt.UseInMemoryDatabase("DbDog"));

// Define the dependency injection for the IDogCommands interface
builder.Services.AddScoped<IDogCommands, DogCommands>();

// Add services to the container
// Enables API Explorer for generating OpenAPI documentation
builder.Services.AddEndpointsApiExplorer(); 
 // Adds Swagger generation for the API documentation
builder.Services.AddSwaggerGen();
// Adds authorization services for JWT authentication
builder.Services.AddAuthorization(); 

var jwtSecretKey = "password123casdsadsaiodiasdsadas";
var tokenExpiryMinutes = 10;

// Add JWT Authentication using the provided secret key
builder.Services.AddJwtAuthentication(jwtSecretKey);

var app = builder.Build();

// Enable authentication and HTTPS redirection
// Enables authentication middleware for JWT authentication
app.UseAuthentication(); 
// Redirects HTTP requests to HTTPS
app.UseHttpsRedirection(); 
// Enables authorization middleware for JWT authentication
app.UseAuthorization(); 

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
     // Adds Swagger UI to the pipeline for API documentation in development mode
    app.UseSwagger(); 
    // Configures Swagger UI endpoint for API documentation in development mode
    app.UseSwaggerUI();
}

// Definition Get Method
// Adds authorization requirement for this endpoint
app.MapGet("/dog", async (IDogCommands commands) =>
    await commands.GetAllDogs()).RequireAuthorization(); 

// Definition Get{Id} Method
// Adds authorization requirement for this endpoint
app.MapGet("/dog/{id}", async (int id, IDogCommands commands) =>
{
    var dog = await commands.GetDogById(id);

    if (dog == null) return Results.NotFound();

    return Results.Ok(dog);
}).RequireAuthorization(); 

// Definition Post Method
// Adds authorization requirement for this endpoint
app.MapPost("/dog", async (Dog dog, IDogCommands commands) =>
{
    await commands.AddDog(dog);
    await commands.Save();

    return Results.Ok();
}).RequireAuthorization(); 

// Definition Put Method
// Adds authorization requirement for this endpoint
app.MapPut("/dog/{id}", async (int id, Dog dog, IDogCommands commands) =>
{
    var updateOk = await commands.UpdateDog(dog, id);

    if (!updateOk) return Results.NotFound();

    return Results.NoContent();
}).RequireAuthorization(); 

// Definition Delete Method
// Adds authorization requirement for this endpoint
app.MapDelete("/dog/{id}", async (int id, IDogCommands commands) =>
{
    var deleteOk = await commands.DeleteDog(id);
    if (deleteOk)
    {
        await commands.Save();
        return Results.Ok();
    }

    return Results.NotFound();
}).RequireAuthorization();

// POST /token to generate JWT token
// Allows anonymous access to the "/token" endpoint for generating the JWT token
app.MapPost("/token", (HttpContext context) =>
{
    // Generate JWT token
    var generatedToken = TokenGenerator.GenerateToken(jwtSecretKey, tokenExpiryMinutes);
    return TokenGenerator.GenerateTokenEndpoint(jwtSecretKey, tokenExpiryMinutes);
}).AllowAnonymous(); 
app.Run();


We have done and now, if we run the application and we call an endpoint without using the token for the authorization, we will receive an error:


Instead, if we request a token and then we use it for the authorization, we will be able to use the endpoints:



Leave a Reply

Your email address will not be published. Required fields are marked *