DEV Community

Cover image for JWT Authentication & Authorization in ASP.NET
Mo
Mo

Posted on

JWT Authentication & Authorization in ASP.NET

Hey everyone, welcome to our deep dive into JWTs in ASP.NET Core! Whether you’re a beginner or a seasoned developer, this article will guide you through the ins and outs of implementing secure authentication and authorization using JSON Web Tokens (JWT). Grab your favourite beverage, sit back, and let’s get started! 😊


What is JWT

What is JWT? 🔍

JWT stands for JSON Web Token. Think of it as a digital passport that securely transmits information between parties. It’s compact, URL-safe, and self-contained, meaning all the necessary data for authentication is right inside the token. A JWT consists of three key parts:

  • Header:

    Specifies the token type and the hashing algorithm used (e.g., HMAC SHA256).

  • Payload:

    Contains the claims—statements about an entity (usually the user) such as their ID, email, and roles. These claims are essential for both authentication and authorization.

  • Signature:

    Ensures that the token hasn’t been tampered with. It’s created using the header, payload, and a secret key.

You can explore JWTs and see their inner workings at jwt.io 🔗.


Authentication & Authorization

How Does the Authentication & Authorization Process Work? 🛡️

When a client sends a request to a secure API, here’s what happens behind the scenes:

  1. Client Request:

    The client sends a request including the JWT in the Authorization header.

  2. Authentication:

    • The ASP.NET authentication provider extracts the JWT.
    • It validates the token’s signature, expiration, and issuer.
    • If valid, the user is authenticated.
  3. Authorization:

    • The server checks if the user has the required claims to access the resource.
    • If the claims match the required policy, access is granted; otherwise, a 403 Forbidden error is returned.

Policy-Based vs. Role-Based Access Control

Policy-Based vs. Role-Based Access Control 🔐

Role-Based Access Control (RBAC)

  • RBAC assigns roles directly to users.
  • For example, only users with an Admin role might access certain endpoints.
  • This method can become unwieldy as your user base grows.

Policy-Based Access Control

  • Policy-Based access control defines policies (like CanRead) that are assigned to roles.
  • When a user’s role changes, their permissions update automatically without modifying each user individually.
  • This approach is more flexible and scalable, especially for larger applications.

In our demo, we focus on policy-based access control for that extra bit of flexibility!


Implementing JWT in ASP.NET Core: A Step-by-Step Guide 💻

Let’s walk through how to set up JWT authentication in an ASP.NET Core application.

Clone the project

Clone the project from Github and take a look at the authentication and authorisation configuration in the DependencyInjection.cs file:

services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer();
        services.AddAuthorization();
        services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
        services.AddSingleton<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
Enter fullscreen mode Exit fullscreen mode

We have all authentication configuration in the JwtBearerOptionsSetup file:

internal class JwtBearerOptionsSetup(IOptions<JwtOptions> jwtOptions): IPostConfigureOptions<JwtBearerOptions>
{
    private readonly JwtOptions _jwtOptions = jwtOptions.Value;

    public void PostConfigure(string? name, JwtBearerOptions options)
    {
        options.RequireHttpsMetadata = false;
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = _jwtOptions.Issuer,
            ValidateAudience = true,
            ValidAudience = _jwtOptions.Audience,
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecretKey)),
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

This setup ensures that every token is validated against your issuer, audience, and signing key.

To add permission authorisation, we have added two files PermissionAuthorizationHandler and PermissionAuthorizationPolicyProvider.

In the PermissionAuthorizationHandler we make sure that the request has enough permission:

public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        HashSet<string> permissions = context
            .User
            .Claims
            .Where(x => x.Type == Constants.Claims.Permissions)
            .Select(x => x.Value)
            .ToHashSet();

        if (permissions.Contains(requirement.Permission))
        {
            context.Succeed(requirement);
        }

        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, only requests with a valid JWT will be able to access these endpoints.

Another important file is JwtProvider which is responsible for creating the JWT token:

public sealed class JwtProvider(IOptions<JwtOptions> jwtOptions, IPermissionService permissionService) : IJwtProvider
{
    private readonly JwtOptions _jwtOptions = jwtOptions.Value;

    public async Task<string> GenerateJwtTokenAsync(User user)
    {
        List<Claim> claims =
        [
            new(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
            new(JwtRegisteredClaimNames.Email, user.Email!),
            new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        ];

        HashSet<string> permissions = await permissionService.GetPermissionsAsync(user.Id);

        claims.AddRange(permissions.Select(permission => new Claim(Constants.Claims.Permissions, permission)));

        SigningCredentials signingCredentials =
            new(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.SecretKey)), SecurityAlgorithms.HmacSha256);

        JwtSecurityToken token = new(_jwtOptions.Issuer, _jwtOptions.Audience, claims,
            expires: DateTime.UtcNow.AddMinutes(_jwtOptions.TimeoutMinutes), signingCredentials: signingCredentials);

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

That's all the configuration we need to know about JWT authentication and authorisation.


Wrap-Up & Key Takeaways 📝

Today, we explored:

  • JWT Fundamentals:

    Understanding JWT as a secure, compact way to transfer claims between parties.

  • Authentication & Authorization Flow:

    From initial user login to token validation and access control.

  • Policy-Based Access Control:

    Offering flexibility over traditional role-based approaches by defining policies like CanRead that are associated with roles.

  • Implementation in ASP.NET Core:

    A step-by-step guide including configuration, creating a login endpoint, and securing your APIs with JWT.

Implementing JWT in your ASP.NET Core applications not only simplifies authentication but also scales beautifully with your growing user base. If you have any questions or feedback, feel free to drop a comment below!

Happy coding! 💻✨

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.