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)