How to implement safe APIs RESTful with JWT on ASP.NET core

H

Introduction

Safety is one of the major concerns when creating APIs, in the end, we all want our solutions to be safe for our users. Otherwise, why should they use our applications, websites, apps, etc?!

Today, I’m going to walk you through how to implement authentication with JWT (JSON Web Token) in ASP.NET Core to protect your APIs.

What is JWT and why use it?

JWT (JSON Web Token) is a RFC open standard (RFC 7519) for data interchange.

This is what you will find in the first paragraph.

JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.

But what does that mean?

Basically, that means that all the information your software needs to handle your access is encapsulated in JSON that can be signed and/or encrypted.

Benefits

  • Stateless: It doesn’t depend on server sessions, which makes simpler the scalability and load balancing
  • Compact: It’s easy to send in HTTP headers (Lightweight). Less verbose than XML
  • Safety: It can use public/private key pairs as X.509 forms for signing. Also supports HMAC, RS256, and other algorithms, you can find more here.
  • Usability: Most programming have JSON Parsers implemented today nativelly

Now let’s rollup the sleeves.

Creating a project:

  • lets crate a new asp.net core project:
dotnet new sln -n JWTExample

dotnet new webapi -n JWTExample.API

dotnet sln add .\\JWTExample.API\\JWTExample.API.cspr

I’m not going to enter on details of what this line does in this post, but you can find further reading here on Microsoft learn page

But basically, we are:

  • Creating a solution
  • Create a ASP.NET Core WebAPI project
  • Adding the project to a Solution

Note: Feel free to create a project using your favorite IDE, that is not a requirement.

Configure the dependencies

  • Now we have to add the correct dependency to the project

Once in the project folder (where your .csproj is) run the command below

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Note: Here you can use the Nuget Package Manager, no worries.

Now we will configure the JWT on the ASP.NET Core, but…

To make this useful and easy to understand, we need some to add more value when we are testing something. I mean, what is the point of creating a secure API with authentication with JWT if we don’t have anything to secure, right?

So, the to not make this article super long, I would like to recommend this nice Microsoft Learn’s tutorial, so if you are a true beginner you can start creating your minimal apis. (I ain’t using a Todo List as an example, but you will get the idea. My code is on github so you can check your solution against it. But remember, try to build something from scratch and play around, it will be fun).

Now let’s move forward.

To implement the Authentication part I’m gonna make use of a really nice resource called Extension Method, so we can keep our Program.cs clean and tidy.

Let’s see the code.

public static class AuthExtensions
{
    public static IServiceCollection AddAuth(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = true,
                    ValidateAudience = true,
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true,
                    ValidIssuer = configuration["JWT:Issuer"],
                    ValidAudience = configuration["JWT:Audience"],
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["JWT:Key"])),
                    
                };
            });

        services.AddAuthorization();
        return services;
    }

Fear not let’s break it into small pieces. I am not digging into the extension methods’ particularities, because you already read it into the Microsoft documentation, otherwise, you can it in your Program.cs, no judgments here.

So, the line:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) is responsible for registering the services required by the authentication services, and the parameter JwtBearerDefaults.AuthenticationScheme is the scheme we will use to authenticate, in our case “Bearer”.

The line .AddJwtBearer enables JWT-Bearer authentication using the default scheme. This authentication scheme extracts and validates the JWT token from the Authorization from the request header. In other words, once you send the request, the Authorization header, will be extracted and validated by the Authentication Scheme, and once it is valid, you will be able to perform the action you request.

This line here options.TokenValidationParameters = new TokenValidationParameters is declaring the TokenValidationParameters, where we set the parameters that will be validated by the AuthenticationServices.

	ValidateIssuer = true
	ValidateAudience = true,
	ValidateLifetime = true,
	ValidateIssuerSigningKey = true,
	ValidIssuer = configuration["JWT:Issuer"],
	ValidAudience = configuration["JWT:Audience"],
	IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(configuration["JWT:Key"])),

these lines above contain all the information we need to validate our token. Also, it is important to mention that here I’ve used a configuration on appsettings.json that’s the reason I am using this line configuration["JWT:Key"] on the last line for the parameters.

Also, you will need to add this, to your appsettings.json to provide a secret key

"Jwt": {  
   "Key": "MySuperDuperSecretKeyFromOuterSpaceInThe100thDimension",
    "Issuer": "a-domain.com",
    "Audience": "a-domain.com"
}

Why you should place it there? Don’t worry it will make sense later.

Also, you can add these lines here to your Program.cs file to configure the ASP.NET Core Middleware:

app.UseAuthentication();
app.UseAuthorization();

Hey, what about our extension method? No problem, we will add this to our Program.cs as well.

builder.Services.AddAuth(builder.Configuration);

Your Program.cs might look like this


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();
builder.Services.AddDbContext<BooksDbContext>(opt => 
    opt.UseInMemoryDatabase("BooksDB"));

builder.Services.AddAuth(builder.Configuration);

var app = builder.Build();

var booksGroup = app.MapGroup("/books");

booksGroup.MapGet("/", BooksEndpoints.GetAllBooks);
booksGroup.MapGet("/{id}", BooksEndpoints.GetBookById);
booksGroup.MapPost("/", BooksEndpoints.CreateBook);
booksGroup.MapPut("/{id}", BooksEndpoints.UpdateBook);
booksGroup.MapDelete("/{id}", BooksEndpoints.DeleteBook);

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

app.UseAuthentication();
app.UseAuthorization();

app.UseHttpsRedirection();

await app.RunAsync();

Is that enough? No, we will implement some more features.

Now we are going to see the implementation of a simple endpoint with hardcoded values :

public class LoginEndpoints
{
    private readonly IConfiguration _configuration;
    public LoginEndpoints(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public Task<IResult> Login(UserLogin login)
    {
        if (login.Username == "admin" && login.Password == "admin")
        {
            var token = GenerateToken(login.Username);
            return Task.FromResult(Results.Ok(token));
        }

        return Task.FromResult(Results.Unauthorized());
    }

    private string GenerateToken(string loginUsername)
    {
        var secretKey = _configuration["JWT:Key"];
        var audienceToken = _configuration["JWT:Audience"];
        var issuerToken = _configuration["JWT:Issuer"];
        var newSymmetricSecurityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretKey));
        var credentials = new SigningCredentials(newSymmetricSecurityKey, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: issuerToken,
            audience: audienceToken,
            expires: DateTime.Now.AddMinutes(30),
            signingCredentials: credentials
        );
        
        return new JwtSecurityTokenHandler().WriteToken(token);
    }
}

Here we can see a method Login with some admin values for username and password, just to make it simple and we have a Generate Token that takes the configurations from the appsettings.json

Now we have to protect our endpoints.

To achieve that, if you are using minimal apis, you can add the .RequiresAuthorization() to your .MapGroup() call like this.

var booksGroup = app.MapGroup("/books")
    .RequireAuthorization(); // Add this line

If you are using the controller you can use it like this

[Authorize] //Add this line
[Route("api/{controller}")]
public class BooksController: ControllerBase

So if there is an endpoint that doesn’t need to be secured, we can use like this.

For minimal APIs:

app
	.MapGet("/", BooksEndpoints.GetAllBooks)
  .AllowAnonymous();

For controllers:

[AllowAnonymous]
public Task<IActionResult> Get()

There are options you could try out and see what suits you best.

Now we can use Postman or any other HTTP client to test it out.

Try to login, get the token, add it to your headers, and post something.

Before I wrap up, i would like to give you some advices.

  • Use a complex secret key for IssuerSiningKey .
  • Use Https in Production
  • Implement refresh token routes, for safety
  • Monitor and Register suspicious activities.

Authentication is important for applications, it helps us to protect our users’ data. Also, gives trust to our software.

Security is an ongoing process, as much your app grows you need to review and adjust the authentication and protection.

Today that is all I have for you, I hope you like it. If you have any doubt, suggestions, or anything else, please comment it out. Also, consider to like this post and/or share with a friend.

Thank you for your time and see you next time.

You can find the source code for this article here: https://github.com/PellizzoniCode/JWTExample

Gio.

Add Comment

GioPellizzoni

Passionate about all things C# and .NET, I enjoy sharing insights from years of experience building scalable, maintainable software solutions. From unraveling complex software architecture problems to diving deep into the latest .NET features, my blog is a space for practical tips, in-depth analysis, and real-world examples. Whether you're a beginner or a seasoned developer, there's something here for everyone who's curious about the craft of building great software.

Get in touch

Would like you to get in touch? Please find me on Linkedin or send me an e-mail