[go: up one dir, main page]

DEV Community

Ricardo
Ricardo

Posted on • Originally published at rmauro.dev on

JWT Authentication with C# .NET

In this article, we will implement the JWT Bearer Token Authentication in C# .NET 7 - which also works for .NET 6, and preview .NET 8 - using ASP NET.

Security is a significant concern today, with so much sensitive information, and not so much sensitive ones, being transmitted across the internet.

One way to ensure only authorized users access restricted information is through authentication. OAuth and J SON W eb T oken (JWT) are the most common ways to ensure modern Web Applications and Mobile Applications authentication.

๐Ÿ“ข Table of Contents

In this issue, we're going to cover the following topics:

๐Ÿงพ What are Access Token, JWT Token, and Bearer Authorization?

Access tokens, JWT tokens, and Bearer Authorization are commonly used in web applications to authenticate users and provide access to protected resources.

An Access Token is a ticket to authenticate a user and access a protected resource.

JWT Token , on the other hand, is a specific type of Access Token encoded as a JSON object, hence J SON W eb T oken.

It consists of a header, a payload, and a signature, and it is commonly used in modern web applications.

JWT in Raw Format

Sample JWT Token

JWT Structure

Token Header + Payload + Signature

In addition, the JWT token can contain various claims, such as the user's id, expiration date, and application permissions.

Bearer Authorization is a mechanism for transmitting Access Tokens in HTTP requests. It involves including the Access Token in the Authorization header of the HTTP request using the Bearer Authentication Scheme.

The Bearer Scheme indicates that the token being transmitted is an Access Token, allowing the server to authenticate the user and grant access to protected resources.

For example:

JWT Request Flow

Request Access Token + Accessing Protected Resource

  • The Resource Owner sends a request to /tokens/connect/ with his username and password
  • The server validates the request and generates a valid Access Token in the format of a JWT Token, and returns it to the client
  • The client includes the token in the Authorization header (with Bearer scheme) in the subsequent requests to the server
  • The server verifies the token before granting access to protected resources

Set Up Authentication and Authorization

From this point, we are going to implement the JWT Authentication in a C# .NET Web API.

๐Ÿ’ก Here is the full source code: https://github.com/ricardodemauro/Labs.JwtAuthentication

Step 1: Create the Project

For our project, we're going to use C# .NET 7 and start from the Empty Web Project, which is enough for our POC (proof of concept).

Let's name our project Labs.JwtAuthentication.

Visual Studio Template Selection

Visual Studio Asp.Net Core Empty Template

After completion of creation, our project should look like this:

Project Solution

The solution in Visual Studio

Step 2: Installing the dependency packages

Our next step is to install the necessary NuGet packages:

dotnet add Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add System.IdentityModel.Tokens.Jwt
Enter fullscreen mode Exit fullscreen mode

๐Ÿ’ก If you choose to use .NET 6 or a different version. You should use the package version according to the framework restriction.

After the installation of all of the packages, we should end with something like this:

Nuget Packages in Project

Installed Packages

Step 3: Adding Variables to the Configuration File

In this next step, let's create the configurations for the Audience, Issuer, and Signing Key in our appsettings.json.

We will use them to generate the JWT Token (Access Token) and to validate it in the Authentication process.

{
  //๐Ÿ‘‡ JWT Configurations
  "JwtOptions": {
    "Issuer": "https://localhost:7004",
    "Audience": "https://localhost:7004",

    //๐Ÿ‘‡ Used to encrypt and decrypt the jwt token
    "SigningKey": "some-signing-key-here",

    // token expiration time
    "ExpirationSeconds": 3600
  }
}

Enter fullscreen mode Exit fullscreen mode

appsettings.json with JwtOptions

Next, let's create a record class to hold this configuration and use it in the application:

namespace Labs.JwtAuthentication;

public record class JwtOptions(
    string Issuer,
    string Audience,
    string SigningKey,
    int ExpirationSeconds
);
Enter fullscreen mode Exit fullscreen mode

Finally, we will read the configuration and map it to JwtOptions as a Singleton lifetime. Of course, you can also use the Options pattern if you prefer.

var builder = WebApplication.CreateBuilder(args);

var jwtOptions = builder.Configuration
    .GetSection("JwtOptions")
    .Get<JwtOptions>();

builder.Services.AddSingleton(jwtOptions);
Enter fullscreen mode Exit fullscreen mode

This allows us access to the JwtOptions object at any time in the application.

Step 4: Configure the Authentication and Authorization Services

In action, we will configure the Authentication and Authorization services to validate the JWT token when it exists in the HTTP Authorization header.

// ๐Ÿ‘‡ Configuring the Authentication Service
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(opts =>
    {
        //convert the string signing key to byte array
        byte[] signingKeyBytes = Encoding.UTF8
            .GetBytes(jwtOptions.SigningKey);

        opts.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = jwtOptions.Issuer,
            ValidAudience = jwtOptions.Audience,
            IssuerSigningKey = new SymmetricSecurityKey(signingKeyBytes)
        };
    });

// ๐Ÿ‘‡ Configuring the Authorization Service
builder.Services.AddAuthorization();
Enter fullscreen mode Exit fullscreen mode

In the code snippet above, we have configured the Authentication service to:

  • Adds the Bearer Scheme as the default scheme for authentication
  • ValidateIssuer: validate who token generated the token (the Issuer)
  • ValidateAudience: validate for whom it was generated (the audience)
  • ValidateLifToken: check if it is not expired
  • ValidateIssuerSigningKey: validate the token signature

This is the most common and minimal validation setup we should use when working with JWT Tokens.

Last, we have added the Authorization service with the default configuration.

Step 5: Set Up Authentication and Authorization Middleware

In this final step, we will include the Authentication and Authorization middlewares in the pipeline to validate the requests with the JWT Token.

And finally, let's indicate which route should be authorized (non-public) and allow anonymous (public) requests.

var app = builder.Build();

// ๐Ÿ‘‡ This add the Authentication Middleware
app.UseAuthentication();
// ๐Ÿ‘‡ This add the Authorization Middleware
app.UseAuthorization();

// ๐Ÿ‘‡ The routes / and /public allow anonymous requests
app.MapGet("/", () => "Hello World!");
app.MapGet("/public", () => "Public Hello World!")
    .AllowAnonymous();

// ๐Ÿ‘‡ The routes /private require authorized request
app.MapGet("/private", () => "Private Hello World!")
    .RequireAuthorization();

// ๐Ÿ‘‡ handles the request token endpoint
app.MapPost("/tokens/connect", (HttpContext ctx, JwtOptions jwtOptions)
    => TokenEndpoint.Connect(ctx, jwtOptions));

app.Run();
Enter fullscreen mode Exit fullscreen mode

Until here, we have completed the setup of the Authentication and Authorization process.

Our application can authenticate and authorize our requests with an Access Token.

Generate the JWT Token / Access Token

From this step forward, we will implement the endpoint to create the JWT Tokens (Access Tokens).

The endpoint skeleton should have this format:

POST /connect/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=johndoe&password=A3ddj3wr
Enter fullscreen mode Exit fullscreen mode

HTTP Request

And the response should be:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8

{
  "access_token": "some-access-token-here",
  "token_type": "bearer",
  "expires_in": 3600
}
Enter fullscreen mode Exit fullscreen mode

HTTP Response

Implementing a Route to Create the JWT Token

Let's create a static class TokenEndpoint. This class will contain all the logic to handle the /connect/token request and generate the tokens.

namespace Labs.JwtAuthentication.Endpoints;

public static class TokenEndpoint
{
    //handles requests from /connect/token
    public static async Task<IResult> Connect(
        HttpContext ctx,
        JwtOptions jwtOptions)
    {
        throw new NotImplementedException();
    }

    //
    static (string, DateTime) CreateAccessToken(
        JwtOptions jwtOptions,
        string username,
        string[] permissions)
    {
        throw new NotImplementedException();
    }
}
Enter fullscreen mode Exit fullscreen mode

Barebones of TokenEndpoint class

Let's implement the method CreateAccessToken:

static string CreateAccessToken(
    JwtOptions jwtOptions,
    string username,
    TimeSpan expiration,
    string[] permissions)
{
    var keyBytes = Encoding.UTF8.GetBytes(jwtOptions.SigningKey);
    var symmetricKey = new SymmetricSecurityKey(keyBytes);

    var signingCredentials = new SigningCredentials(
        symmetricKey,
        // ๐Ÿ‘‡ one of the most popular. 
        SecurityAlgorithms.HmacSha256);

    var claims = new List<Claim>()
    {
        new Claim("sub", username),
        new Claim("name", username),
        new Claim("aud", jwtOptions.Audience)
    };

    var roleClaims = permissions.Select(x => new Claim("role", x));
    claims.AddRange(roleClaims);

    var token = new JwtSecurityToken(
        issuer: jwtOptions.Issuer,
        audience: jwtOptions.Audience,
        claims: claims,
        expires: DateTime.Now.Add(expiration),
        signingCredentials: signingCredentials);

    var rawToken = new JwtSecurityTokenHandler().WriteToken(token);
    return rawToken;
}
Enter fullscreen mode Exit fullscreen mode

Create Access Token Method

The CreateAccessToken method will generate the token with claims:

  • sub: Unique identifier for the end-user
  • name: End-user full name
  • aud: Audience(s) that this Token is intended for.
  • role: User role(s)

A JWT Token can handle various pre-defined and custom claims.

Next, let's add the implementation of the method Connect:

public static async Task<IResult> Connect(
    HttpContext ctx,
    JwtOptions jwtOptions)
{
    // validates the content type of the request
    if (ctx.Request.ContentType != "application/x-www-form-urlencoded")
        return Results.BadRequest(new { Error = "Invalid Request" });

    var formCollection = await ctx.Request.ReadFormAsync();

    // pulls information from the form
    if (formCollection.TryGetValue("grant_type", out var grantType) == false)
        return Results.BadRequest(new { Error = "Invalid Request" });

    if (formCollection.TryGetValue("username", out var userName) == false)
        return Results.BadRequest(new { Error = "Invalid Request" });

    if (formCollection.TryGetValue("password", out var password) == false)
        return Results.BadRequest(new { Error = "Invalid Request" });

    //creates the access token (jwt token)
    var tokenExpiration = TimeSpan.FromSeconds(jwtOptions.ExpirationSeconds);
    var accessToken = TokenEndpoint.CreateAccessToken(
        jwtOptions,
        userName,
        TimeSpan.FromMinutes(60),
        new[] { "read_todo", "create_todo" });

    //returns a json response with the access token
    return Results.Ok(new
    {
        access_token = accessToken,
        expiration = (int)tokenExpiration.TotalSeconds,
        type = "bearer"
    });
}
Enter fullscreen mode Exit fullscreen mode

Implementation of Connect method

The Connect method reads the form and validates each property before generating the access token. Once everything is set, we call the method CreateAccessToken method.

Finally, let's include Map the route /tokens/connect POST.

app.MapPost("/tokens/connect", (HttpContext ctx, JwtOptions jwtOptions)
    => TokenEndpoint.Connect(ctx, jwtOptions));
Enter fullscreen mode Exit fullscreen mode

With all of this, we have a complete application capable of generating and validating access tokens in the format of JWT Tokens.

Testing the Endpoints

Create a valid HTTP Request using Visual Studio, Visual Studio, Postman, Insomnia, or any tool that you like.

๐Ÿ’ก In the code sample, you can use the .http file with all APIs calls.
https://github.com/ricardodemauro/Labs.JwtAuthentication/blob/master/http-test.http

A Sample Request to Get a Valid Access Token:

POST https://localhost:7004/tokens/connect HTTP/1.1
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=johndoe&password=A3ddj3wr
Enter fullscreen mode Exit fullscreen mode

Result of Connect Endpoint:

HTTP/1.1 200 OK
Connection: close
Content-Type: application/json; charset=utf-8
Date: Thu, 27 Apr 2023 17:35:45 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
  "access_token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJz......",
  "expiration": 3600,
  "type": "bearer"
}
Enter fullscreen mode Exit fullscreen mode

Making a Request Without Access Token to Private Route:

GET https://localhost:7004/private
Enter fullscreen mode Exit fullscreen mode

HTTP 401 - Unauthorized Result of Private Route:

HTTP/1.1 401 Unauthorized
Content-Length: 0
Connection: close
Date: Thu, 27 Apr 2023 17:36:27 GMT
Server: Kestrel
WWW-Authenticate: Bearer
Enter fullscreen mode Exit fullscreen mode

Making a Request With Access Token to Private Route

GET https://localhost:7004/private
Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJz......
Enter fullscreen mode Exit fullscreen mode

HTTP 200 - Ok Result of Authorized Route:

HTTP/1.1 200 OK
Connection: close
Content-Type: text/plain; charset=utf-8
Date: Thu, 27 Apr 2023 17:37:10 GMT
Server: Kestrel
Transfer-Encoding: chunked

Private Hello World!
Enter fullscreen mode Exit fullscreen mode

Conclusion

JWT authentication is a secure and effective way to authenticate users in web applications.

C# .NET provides a simple and easy-to-implement way to use JWT Authentication and Authorization.

Following the steps outlined in this article, you can implement JWT Authentication in your C# .NET 8 Web Applications and ensure that only authorized users can access protected resources.

Top comments (3)

Collapse
 
ant_f_dev profile image
Anthony Fung

Great walkthrough.

When I first encountered JWTs, I didn't realise they were pronounced as jot. I'd just go about calling them jay double-you tee s ๐Ÿ˜

Collapse
 
rmaurodev profile image
Ricardo

I still do that! :)

Collapse
 
ant_f_dev profile image
Anthony Fung

No harm as long as everyone understands - and everyone will understand jay double-you tee

Actually, I remember when I was learning WPF for the first time. I'd never encountered XAML files before (markup for WPF apps - a bit like HTML but more extensive). I'd call them my their letter names too - Ex A Em El

It was only when I started working with more people that I heard them pronounce it as Zamel

Again, everyone understood so no hard done ๐Ÿ˜