DEV Community

Cover image for Implementing JWT Authentication in Minimal APIs
Spyros Ponaris
Spyros Ponaris

Posted on

Implementing JWT Authentication in Minimal APIs

In my previous article, I covered validation in Minimal APIs, but security is just as important. Most applications rely on JWT authentication for securing APIs, so in this article, Iโ€™ll walk you through how to create an Identity Service for JWT authentication.

๐Ÿ‘‰ Note: This article will not include ASP.NET Core Identity. If you're interested in a separate article on that, let me know, and Iโ€™ll add an implementation to my GitHub repo.

๐Ÿ“Œ Check out the full implementation here:
https://github.com/stevsharp/minimal-vs-controller-benchmark/tree/main/TestJwt

Step 1: Creating the Identity Service
Instead of placing JWT generation logic inside our API endpoints, it's best to encapsulate it inside a service. This makes our authentication system more modular and testable.

๐Ÿ”น IdentityService.cs
We define a simple IdentityService that handles JWT token generation:

public class IdentityService(JwtConfiguration config)
{
    private readonly JwtConfiguration _config = config;

    public async Task<string> GenerateToken(string username)
    {
        await Task.Delay(100); // Simulate a database call

        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, "123456"), // Example subject ID
            new Claim(JwtRegisteredClaimNames.Email, "admin@admin.gr"),
            new Claim(JwtRegisteredClaimNames.PreferredUsername, username)
        };

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config.Secret));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            issuer: _config.Issuer,
            audience: _config.Audience,
            claims: claims,
            expires: DateTime.UtcNow.AddDays(_config.ExpireDays),
            signingCredentials: creds
        );

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

๐Ÿ”น Registering the Service in Program.cs
To make the service available for dependency injection, register it in the DI container:

builder.Services.AddScoped<IdentityService>();
Enter fullscreen mode Exit fullscreen mode

Step 2: Configuring JWT Authentication
Now, we need to configure authentication in Program.cs.

๐Ÿ”น JWT Configuration Class
Letโ€™s create a class to manage JWT settings in appsettings.json:

public record JwtConfiguration
{
    public string Secret { get; set; } = string.Empty;
    public string Issuer { get; set; } = string.Empty;
    public string Audience { get; set; } = string.Empty;
    public int ExpireDays { get; set; } = 7;
}
Enter fullscreen mode Exit fullscreen mode

Load it into the dependency injection container:

var jwtConfig = builder.Configuration.GetSection("JwtSettings").Get<JwtConfiguration>();
builder.Services.AddSingleton(jwtConfig);
Enter fullscreen mode Exit fullscreen mode

** Configure Authentication Middleware**
Add JWT authentication to the application:

public static class JwtAuthBuilderExtensions
{
    public static AuthenticationBuilder AddJwtAuthentication(this IServiceCollection services, JwtConfiguration jwtConfiguration)
    {
        services.AddAuthorization();

        return services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(x =>
        {
            x.SaveToken = true;
            x.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = jwtConfiguration.Issuer,
                ValidAudience = jwtConfiguration.Audience,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfiguration.Secret)),
                RequireExpirationTime = true,
            };
        });
    }
}

Enter fullscreen mode Exit fullscreen mode

Enable authentication in the request pipeline:

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

Step 3: Implementing Login Endpoint

app.MapPost("/login", async (LoginRequest request, IdentityService identityService, IConfiguration config, ILogger<Program> logger) =>
{
    // Retrieve admin credentials from configuration (Optional for flexibility)
    var adminUsername = config["Auth:AdminUsername"] ?? "admin";
    var adminPassword = config["Auth:AdminPassword"] ?? "password";

    // Validate user credentials
    if (string.IsNullOrWhiteSpace(request.Username) || string.IsNullOrWhiteSpace(request.Password))
    {
        logger.LogWarning("Login failed: Empty username or password.");
        return Results.BadRequest(new { message = "Username and password are required." });
    }

    var userIsAuthenticated = request.Username == adminUsername && request.Password == adminPassword;

    if (!userIsAuthenticated)
    {
        logger.LogWarning("Login failed for user: {Username}", request.Username);
        return Results.Unauthorized();
    }

    // Generate JWT token
    var token = await identityService.GenerateToken(request.Username);

    logger.LogInformation("User {Username} authenticated successfully.", request.Username);

    return Results.Ok(new
    {
        message = "Login successful",
        token
    });
})
.AllowAnonymous();

Enter fullscreen mode Exit fullscreen mode

UserLogin Model

public record LoginRequest
{
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Securing API Endpoints

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
        new WeatherForecast
        (
            DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            Random.Shared.Next(-20, 55),
            summaries[Random.Shared.Next(summaries.Length)]
        ))
        .ToArray();
    return forecast;
})
.RequireAuthorization()
.WithName("GetWeatherForecast")
.WithOpenApi();

Enter fullscreen mode Exit fullscreen mode

Step 5: Testing JWT Authentication

Login: Make a POST /login request with:

json

{
  "username": "admin",
  "password": "password"
}
Enter fullscreen mode Exit fullscreen mode

Youโ€™ll receive a token in response.

Use Token:
Include the token in the Authorization header when making requests:

Authorization: Bearer <your-jwt-token>
Access Protected Routes:
Enter fullscreen mode Exit fullscreen mode

Conclusion
By creating an IdentityService, weโ€™ve modularized JWT authentication, making it easy to maintain and extend. This setup does not include ASP.NET Core Identity, but if youโ€™re interested in that, let me know, and Iโ€™ll write a separate article on how to integrate it.

๐Ÿ“Œ Full code available here:
๐Ÿ”— TestJwt GitHub Repository
https://github.com/stevsharp/minimal-vs-controller-benchmark/tree/main/TestJwt

Let me know if you have any questions or improvements! ๐Ÿš€

๐Ÿ“Œ Useful references
JWT Authentication in ASP.NET Core
๐Ÿ”— https://learn.microsoft.com/en-us/aspnet/core/security/authentication/jwt
Covers how to configure JWT authentication in ASP.NET Core applications.

Minimal APIs in ASP.NET Core
๐Ÿ”— https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis
Overview of Minimal APIs and how they work in .NET.

Authentication and Authorization in .NET
๐Ÿ”— https://learn.microsoft.com/en-us/dotnet/core/security/authentication-identity
General authentication and identity management in .NET.

Top comments (0)