In the modern digital landscape, Web APIs are critical for enabling communication between different systems and services. However, they also present a lucrative target for attackers. Ensuring the security of your Web APIs is paramount. In this blog post, we’ll explore best practices for securing Web APIs in .NET C#.
- Data Protection using HTTPS only.
- Authentication.
- Token Expiry and Rotation.
- Role-Based Access Control (RBAC).
- Input Validation.
- Rate Limiting and Throttling.
- CORS (Cross-Origin Resource Sharing)
- Logging and Monitoring.
We will cover every section in detail and will see it in action. So let’s get started.
1- Data Protection using HTTPS only:
Always use HTTPS to encrypt data in transit. Avoid using HTTP to prevent data from being intercepted. HTTPS is more securable and reliable protocol.
Program.cs
app.UseHttpsRedirection();
2- Authentication:
Always use a strong authentication mechanism to protect our API more securely, allowing only authenticated users. We can achieve robust authentication mechanisms such as OAuth, OpenID Connect, or JWT (JSON Web Tokens). Avoid basic authentication with plaintext passwords.
Perform the following steps.
- Create a new Asp.Net Web API Project.
- Add a new controller named AuthController.
- Install the nuget package “Microsoft.AspNetCore.Authentication.JwtBearer” d) Now, add the following code.
AuthController.cs:
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.ComponentModel.DataAnnotations;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace BlogApiApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthController : ControllerBase
{
private readonly IConfiguration _configuration;
List UserList;
public AuthController(IConfiguration configuration)
{
_configuration = configuration;
UserList = new List();
UserList.Add(new UserLoginModel { Username = "admin", Password = "admin123" });
UserList.Add(new UserLoginModel { Username = "Jhon", Password = "Jhon123"});
}
[HttpPost("login")]
public IActionResult Login([FromBody] UserLoginModel user) {
//Validate user credentials(this is just an example, replace with your logic)
var UserListRes = UserList.Where(x => x.Username ==
user.Username && user.Password == user.Password).FirstOrDefault();
if (UserListRes != null)
{
//2- JWT Token Auth.
var token = GenerateJwtToken(UserListRes.Username,
"Admin");
return Ok(new { Token = token });
}
return Unauthorized();
}
private string GenerateJwtToken(string username, string role) {
try
{
var jwtSettings =
_configuration.GetSection("JwtSettings");
var securityKey = new
SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"])); var credentials = new SigningCredentials(securityKey,
SecurityAlgorithms.HmacSha256);
var claims = new List
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role) // Assign Role
dynamically
};
var token = new JwtSecurityToken(
issuer: jwtSettings["Issuer"],
audience: jwtSettings["Audience"],
claims: claims,
expires:
DateTime.Now.AddMinutes(Convert.ToDouble(jwtSettings["ExpiryInMinutes"])), signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
catch (Exception ex)
{
throw ex;
}
}
}
}
public class UserLoginModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
}
Pragram.cs:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using System.Reflection.Metadata;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
#region Configure JWT authentication
var jwtSettings = builder.Configuration.GetSection("JwtSettings"); builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new
SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"])) };
});
#endregion
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Appsetting.json:
{
"AllowedHosts": "*",
"JwtSettings": {
"Issuer": "YourIssuer",
"Audience": "YourAudience",
"Key": "YourVeryStrongSecretKeyThatIsLongEnough123456"
}
}
Test Result:
Now, Let’s run the Web API project by pressing F5 and hitting the API through Postman. Finally, our API worked and returned the token.
3- Token Expiry and Rotation:
Ensure that tokens have a short lifespan and can be rotated regularly. Use refresh tokens for obtaining new access tokens. As you can see we have setup the token expiry is 15 mins.
Appsetting.json:
{
"AllowedHosts": "*",
"JwtSettings": {
"Issuer": "YourIssuer",
"Audience": "YourAudience",
"Key": "YourVeryStrongSecretKeyThatIsLongEnough123456",
"ExpiryInMinutes": 15 //Token Timeout.
}
}
Test Results: Token expired after 15 minutes. Now you have to generate a new token and then would be able to access again against that token.
4- Role-Based Access Control (RBAC)
Implement role-based access control to restrict access to API endpoints based on user roles. 4 (i). Every user must have role assigned.
- a) Create a new controller “StudentController.cs”.
- b) Add the [Authorize] on the top of the HTTP method which can only be accessible to authorized users only.
- c) Create new HTTP methods for Admin and User roles.
d) Admin will be able to access the GetSecureData & WelcomeAdmin methods. e) User will be able to access the GetSecureData & WelcomeUser methods.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace BlogApiApp.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class StudentController : ControllerBase
{
[HttpGet("getSecureData")]
[Authorize]
public IActionResult GetSecureData()
{
return Ok(new { Message = "This is secure data." }); }
[HttpGet("welcomeAdmin")]
[Authorize(Roles = "Admin")]
public IActionResult WelcomeAdmin()
{
return Ok(new { Message = "Welcome Admin!" });
}
[HttpGet("welcomeUser")]
[Authorize(Roles = "User")]
public IActionResult WelcomeUser()
{
return Ok(new { Message = "Welcome User!" });
}
}
}
f) Assign roles to the user by modifying the AuthController.cs
public AuthController(IConfiguration configuration)
{
_configuration = configuration;
UserList = new List();
UserList.Add(new UserLoginModel { Username = "admin", Password = "admin123", Role="Admin" });
UserList.Add(new UserLoginModel { Username = "Jhon", Password = "Jhon123", Role= "User" });
}
[HttpPost("login")]
public IActionResult Login([FromBody] UserLoginModel user) {
//Validate user credentials(this is just an example, replace with your logic)
var UserListRes = UserList.Where(x => x.Username ==
user.Username && user.Password == user.Password).FirstOrDefault(); if (UserListRes != null)
{
//2- JWT Token Auth.
var token = GenerateJwtToken(UserListRes.Username,
UserListRes.Role);
return Ok(new { Token = token });
}
return Unauthorized();
}
public class UserLoginModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string Role { get; set; }
}
Test Results:
- a) Get the token of the Jhon who is a normal user only.
- b) Jhon is able to access the “WelcomeUser” HTTP method because his role has accessed on it.
- c) Jhon not able to access the “WelcomeAdmin” HTTP method because his role does not have rights.
4 (ii). Assign multiple roles to an user and allow acces to the HTTP method. We can achieve this by making policies for user’s roles according to the requirements and have to register into the program.cs
#region Configure JWT authentication
var jwtSettings = builder.Configuration.GetSection("JwtSettings"); builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
op tions.TokenValidationParameters = new
TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtSettings["Issuer"],
ValidAudience = jwtSettings["Audience"],
IssuerSigningKey = new
SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"])) };
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminPolicy", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("UserPolicy", policy =>
policy.RequireRole("User"));
options.AddPolicy("UserAndAdminPolicy", policy =>
policy.RequireRole("User", "Admin"));
});
#endregion
- a) Create a new HTTP method in studentController which would be accessible for admin and user roles.
- b) We have to define the policy on the method level.
[HttpGet("welcomeAll")]
[Authorize(Policy = "UserAndAdminPolicy")]
public IActionResult WelcomeAll()
{
return Ok(new { Message = "Welcome All100!" });
}
Test Results:
- a) For Admin:
- b) For User:
5- Input Validation:
Validate query parameters, headers, and body content to ensure they meet expected formats and constraints. We can achieve this through the moded based validation approach.
As you can see, we have made the first three fields are mandatory and the last email field will be validate as per the email format which will handle by the .NET framework.
public class UserLoginModel
{
[Required]
public string Username { get; set; }
[Required]
public string Password { get; set; }
[Required]
public string Role { get; set; }
[EmailAddress]
public string Email { get; set; }
}
Test Result:
6- Rate Limiting and Throttling.
Rate Limiting: Implement rate limiting to prevent abuse and denial-of-service attacks. Use policies to limit the number of requests a client can make within a specified time frame.
Throttling: Implement throttling to control the rate at which requests are processed, ensuring fair usage and preventing overloading.
6(i). For Unregistered IPs.
Anyone can consume the API as per the define throttling limit.
a) Install the nuget package “AspNetCoreRateLimit”.
b) Add the below code block in appsetting.json
// Throttling.
"IpRateLimiting": {
"EnableEndpointRateLimiting": true,
"StackBlockedRequests": false,
"RealIpHeader": "X-Real-IP",
"ClientIdHeader": "X-ClientId",
"HttpStatusCode": 429,
"GeneralRules": [
{
"Endpoint": "*",
"Period": "1m", // 3 calls /min for public
"Limit": 3
},
{
"Endpoint": "*",
"Period": "1h", // 5 calls /hr for public
"Limit": 5
}
]
}
c) Add the below code block in program.cs
#region Throttling
// Add memory cache to store request counts
builder.Services.AddMemoryCache();
// Load configuration settings
builder.Services.Configure(builder.Configuration. GetSection("IpRateLimiting"));
builder.Services.Configure(builder.Configuration .GetSection("IpRateLimitPolicies"));
// Add rate limiting services
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
// Add rate limiting processing
builder.Services.AddInMemoryRateLimiting();
#endregion
d) Enable throttling in program.cs
app.UseIpRateLimiting();
Test Results:
I tried to hit API method more than 3 calls per minute.
6(ii). For Registered IPs:
If you want to define throttling rules’ limit for the specific IPs which consumes your API. This case is related to the premium users who bought the license and they can consume as per their specific subscription plan.
"IpRateLimitPolicies": {
"IpRules": [
{
"Ip": "10.164.21.219", //IpRules: You can also define specific rate limits for certain IP addresses.
"Rules": [
{
"Period": "1m",
"Limit": 8
}
]
}
]
}
7- CORS (Cross-Origin Resource Sharing):
Configure CORS policies to control which domains can access your API, preventing unauthorized cross-origin requests.
a) Use the below code block in Program.cs. After applying CORS the API will only be able to consume by iERP origin. You can add multiple origins with comma separated.
#region CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("SpecificOriginPolicy", policy =>
{
policy.WithOrigins("https://iERP.com") // Only allow this origin
.AllowAnyMethod()
.AllowAnyHeader();
});
});
#endregion
b) Enable the CORS in Program.cs
app.UseCors("SpecificOriginPolicy"); // Enable CORS
c) Now, enable the CORS on the controller level.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace BlogApiApp.Controllers
{
[Route("api/[controller]")]
[EnableCors("SpecificOriginPolicy")] //CORS
[ApiController]
public class StudentController : ControllerBase
{
[HttpGet("getSecureData")]
[Authorize]
public IActionResult GetSecureData()
{
return Ok(new { Message = "This is secure data." }); }
[HttpGet("welcomeAdmin")]
[Authorize(Roles = "Admin")]
public IActionResult WelcomeAdmin()
{
return Ok(new { Message = "Welcome Admin!" });
}
[HttpGet("welcomeUser")]
[Authorize(Roles = "User")]
public IActionResult WelcomeUser()
{
return Ok(new { Message = "Welcome User!" });
}
[HttpGet("welcomeAll")]
[Authorize(Policy = "UserAndAdminPolicy")]
public IActionResult WelcomeAll()
{
return Ok(new { Message = "Welcome All100!" });
}
}
}
8- Logging and Monitoring:
We will use NLog to log the request and responses of the API.
a) Install the following nuget packages
- NLog.Extensions.Logging
- NLog.Web.AspNetCore
- NLog
b) Add a middleware to catch every request and log response against it.
using NLog;
using System.Text;
namespace BlogApiApp.Helpers
{
public class RequestResponseLoggingMiddleware
{
private readonly RequestDelegate _next;
private static readonly NLog.ILogger Logger =
LogManager.GetCurrentClassLogger();
public RequestResponseLoggingMiddleware(RequestDelegate next) {
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
// Log request details
var request = context.Request;
var originalBodyStream = context.Response.Body;
using (var responseBodyStream = new MemoryStream())
{
context.Response.Body = responseBodyStream;
// Log the request
Logger.Info($"Request Method: {request.Method}");
Logger.Info($"Request Path: {request.Path}");
Logger.Info($"Query String: {request.QueryString}");
// Log request headers
foreach (var header in request.Headers)
{
Logger.Info($"Header: {header.Key} =
{header.Value}");
}
// Log the request body
string requestBody = string.Empty;
if (request.Method == HttpMethods.Post ||
request.Method == HttpMethods.Put)
{
request.EnableBuffering(); // Enable buffering to
read the request body
using (var reader = new StreamReader(request.Body,
Encoding.UTF8, leaveOpen: true))
{
requestBody = await reader.ReadToEndAsync();
request.Body.Seek(0, SeekOrigin.Begin); //
Reset the stream position
}
Logger.Info($"Request Body: {requestBody}");
}
// Call the next middleware in the pipeline
await _next(context);
// Log the response
context.Response.Body.Seek(0, SeekOrigin.Begin);
var responseBody = await new
StreamReader(context.Response.Body).ReadToEndAsync(); context.Response.Body.Seek(0, SeekOrigin.Begin);
Logger.Info($"Response Status Code:
{context.Response.StatusCode}");
Logger.Info($"Response Body: {responseBody}");
// Copy the contents of the new memory stream (which contains the response body) to the original stream
await
responseBodyStream.CopyToAsync(originalBodyStream);
}
}
}
}
c) Add the below code block in appsetting.json
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
d) Add into the program.cs
#region Logger
var logger =
NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
#endregion
And don’t forget to register the middleware in program.cs
app.UseMiddleware(); // Nlog Add custom middleware to the pipeline
e) Add the config file “nlog.config” to register the rules.
We define the path, type of logs and format of file name.
TestResult:
You can set format of the logs in the NLog.config file as per your requirement.
Author: Mohsin Aqee