Securing a .NET API with JWT Authentication Using Clean Architecture
When building a RESTful API, securing your endpoints is just as important as the business logic itself. While implementing JWT (JSON Web Tokens) in a standard .NET application is common, doing it correctly within a Clean
When building a RESTful API, securing your endpoints is just as important as the business logic itself. While implementing JWT (JSON Web Tokens) in a standard .NET application is common, doing it correctly within a Clean Architecture structure requires careful separation of concerns. You don't want your Core Domain knowing about HTTP requests, libraries, or token generation algorithms.
In this article, I'll show you how I implemented a decoupled, secure JWT authentication system in my latest .NET API, ensuring that the infrastructure logic remains completely isolated from the business rules.
- The Contract (Interface) To keep my API Controllers clean and easily testable, I rely on the Service Pattern. The controller shouldn't care about the specific packages or algorithms used to generate a token.
To achieve this, I defined a simple interface inside my Services folder.
using SarahaAPI.Models;
namespace SarahaAPI.Services
{
public interface IJWTService
{
public string GenerateToken(User user);
}
}
- The Implementation (Service Layer) Next, I implement this interface in the JWTService class. This class handles the actual token generation using the System.IdentityModel.Tokens.Jwt library. By isolating this logic in the Service layer, the API controller only interacts with the interface, keeping it decoupled from the token generation process.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using SarahaAPI.Models;
namespace SarahaAPI.Services
{
public class JWTService : IJWTService
{
readonly IConfiguration _config;
public JWTService(IConfiguration configuration)
{
_config = configuration;
}
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.Email, user.Email)
};
var creds = new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:SecretKey"])), SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken
(
issuer: _config["JWT:Issuer"],
audience: _config["JWT:Audience"],
claims: claims,
signingCredentials: creds,
expires: DateTime.UtcNow.AddDays(1)
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
}
- Configuration and Dependency Injection (Program.cs) Finally, I need to configure ASP.NET Core to validate JWTs and register my services in the Dependency Injection container. I also configured Swagger to accept Bearer tokens so the API can be easily tested from the browser.
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using Saraha.core.Interfaces;
using Saraha.infrastructure.Repositories;
using SarahaAPI.Data;
using SarahaAPI.filters;
using SarahaAPI.Middlewares;
using SarahaAPI.Services;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers(
Options => Options.Filters.Add<LogInputArgumentsFilter>()
);
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: 'Bearer eyJhbGci...'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
});
builder.Services.AddDbContext<SarahaDbContext>(
options => options.UseSqlServer(builder.Configuration.GetConnectionString("DBAdress"))
);
builder.Services.AddAuthentication(
JwtBearerDefaults.AuthenticationScheme
).AddJwtBearer(
options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!))
};
});
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<IMessageService, MessageService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IJWTService, JWTService>();
builder.Services.AddScoped<IAccountService, AccountService>();
var app = builder.Build();
app.UseMiddleware<ExceptionMiddleware>();
app.UseMiddleware<RequestTimingMiddleware>();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Conclusion
By separating the token generation into its own service (IJWTService) using the Service Pattern, I kept my controllers clean and highly testable. The core business logic remains completely isolated from the authentication implementation details.
You can check out the complete Saraha API Clone source code (including BCrypt password hashing and custom Middlewares) on my GitHub repository here:
https://github.com/elshwilahmed/Saraha-API
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.