Token Based Authentication In ASP.NET Core
In this chapter, we will focus on implementing token based authentication and authorization in asp.net core from scratch. This is a continuation of authentication and authorization series, check out previous blog about the theory behind it.
Let’s familiarize ourself with core tokens.
Access Token: An access token is a credential that is used to authorize and access protected resources in a network. It serves as a proof of the authentication and authorization process and allows a client (such as a user or an application) to make authenticated requests to a secured API or service.
Refresh Token: A refresh token is a long-lived credential used to obtain a new access token without requiring the user to re-enter their credentials. It is typically issued along with the access token during the initial authentication and authorization process. While access tokens have a shorter lifespan, refresh tokens are designed to have a longer expiration period, allowing clients to obtain new access tokens over an extended time.
In this chapter, we are gonna break the implementation in three parts.
- Create a web application in asp.net core.
- Add token based authorization and testing API
- Add Refresh Token and retesting API
Creating Web Application
We are gonna start by creating a new asp.net core web application through the command line.
dotnet new web -n Backend
cd Backend
This creates a simple web application, with weatherforecast
api, to fetch weather information. This saves us time for creating api’s, which is not our main goal in this chapter. To run the project, run the following command, which will build the application and start the server on localhost and defined port. (ex: http://localhost:5168)
dotnet run
On your postman, fetching http://localhost:5168/WeatherForecast with HTTP GET method should return 200 OK the responses similar to following format.
[
{
"date": "2024-03-03",
"temperatureC": 11,
"temperatureF": 51,
"summary": "Balmy"
}
]
Adding token based authorization
In this chapter we will focus on implementing our own, and not use any third party authentication server for simplicity. We also won’t use db to store user information, to avoid unintended contents in this chapter.
We are gonna start by implementing login
endpoint: ‘url/api/Auth/login’. Since we will be using JWT token, let’s start by adding necessary packages.
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
We are gonna AuthController.cs
under Controller
section, to handle authentication and creating access tokens for the user.
// login
[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel model)
{
// Check user credentials (in a real application, you'd authenticate against a database)
if (model is { Username: "demo", Password: "password" })
{
// generate token for user
var token = GenerateAccessToken(model.Username);
// return access token for user's use
return Ok(new { AccessToken = new JwtSecurityTokenHandler().WriteToken(token)});
}
// unauthorized user
return Unauthorized("Invalid credentials");
}
// Generating token based on user information
private JwtSecurityToken GenerateAccessToken(string userName)
{
// Create user claims
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userName),
// Add additional claims as needed (e.g., roles, etc.)
};
// Create a JWT
var token = new JwtSecurityToken(
issuer: _configuration["JwtSettings:Issuer"],
audience: _configuration["JwtSettings:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(1), // Token expiration time
signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecretKey"])),
SecurityAlgorithms.HmacSha256)
);
return token;
}
The login model:-
public class LoginModel
{
[Required(ErrorMessage = "Username is required.")]
public string Username { get; set; }
[Required(ErrorMessage = "Password is required.")]
[DataType(DataType.Password)]
public string Password { get; set; }
}
Add JWTAuthentication
in the services in Program.cs
file for middleware.
// Load configuration from appsettings.json
var configuration = new ConfigurationBuilder()
.SetBasePath(builder.Environment.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
//Add JWT authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = configuration["JwtSettings:Issuer"],
ValidAudience = configuration["JwtSettings:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JwtSettings:SecretKey"]))
};
});
app.UseAuthorization();
app.UseAuthorization();
Make sure to add required variables in appsettings.json
file. This is an example, and not intended for prod use.
"JwtSettings": {
"Issuer": "localhost",
"Audience": "localhost",
"SecretKey": "averylongsecretkeythatisrequiredtobeused"
}
Last but not least, don’t forget to add authorize
attribute in WeatherForeCastController.cs
.
[HttpGet(Name = "GetWeatherForecast")]
[Authorize]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
Let’s build and run the application and test endpoint in postman. A HTTP POST method ‘http://localhost:5168/api/Auth/login’ with the following payload results in 200 OK response.
{
"username": "demo",
"password": "password"
}
Similar to the following example access token in can be found in response.
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiZGVtbyIsImV4cCI6MTcwOTYyMjAwMiwiaXNzIjoibG9jYWxob3N0IiwiYXVkIjoibG9jYWxob3N0In0.PhlEPVUbj-gukE_H080OasHfBrJ38iD_dYcYSAMNo-0",
}
Let’s test our ‘http://localhost:5168/WeatherForecast’ endpoint. A GET method should result in 401 Unauthorized response. On the authorization tab, select Bearer token, and paste the accessToken we just received earlier. The result should be 200 OK the responses similar to following format.
[
{
"date": "2024-03-03",
"temperatureC": 11,
"temperatureF": 51,
"summary": "Balmy"
}
]
That is nice!! We have successfully obtained accessToken to be used with our API to authenticate and authorize our user to access resources as appropriate.
As discussed in our previous chapter, JWT tokens are stateless, and there is not east way to logout users from server side. The common practice is store access token in client side, and when user logs out, clear/delete the token from storage.
It is also important to reduce the lifespan of an accessToken, to reduce the probability of unintended use of accessToken of a user by a malicious user. There is a catch, this results in bad user experience of logging in over and over again. For a solution, it is common practice to use refresh tokens.
Adding Refresh Token
Refresh token is a long lived token, providing new access token to a user when an old one expires. This avoid re-logging a user over and over again. Refresh tokens are unique in the authentication db, so no two user’s can use the same refresh token at the same time for security purpose.
We are gonna focus on implementing simple refresh token without using db and in memory solution. Let’s add a dictionary for storing refresh tokens and modify login method a bit.
// dictionary to store refresh tokens
private static Dictionary<string, string> _refreshTokens = new Dictionary<string, string>();
[HttpPost("login")]
public IActionResult Login([FromBody] LoginModel model)
{
// Check user credentials (in a real application, you'd authenticate against a database)
if (model is { Username: "demo", Password: "password" })
{
var token = GenerateAccessToken(model.Username);
// Generate refresh token
var refreshToken = Guid.NewGuid().ToString();
// Store the refresh token (in-memory for simplicity)
_refreshTokens[refreshToken] = model.Username;
//return access token and refresh token
return Ok(new { AccessToken = new JwtSecurityTokenHandler().WriteToken(token),
RefreshToken = refreshToken });
}
// unauthorized
return Unauthorized("Invalid credentials");
}
Let’s create a refresh endpoint, to refresh access token when one expires.
[HttpPost("refresh")]
public IActionResult Refresh([FromBody] RefreshRequest request)
{
if (_refreshTokens.TryGetValue(request.RefreshToken, out var userId))
{
// Generate a new access token
var token = GenerateAccessToken(userId);
// Return the new access token to the client
return Ok(new { AccessToken = new JwtSecurityTokenHandler().WriteToken(token) });
}
return BadRequest("Invalid refresh token");
}
public class RefreshRequest
{
public string RefreshToken { get; set; }
}
This will create a new access token, with the help of refresh token.
(Optional) Token Expiry and Revocation:
If you want to implement token expiry and revocation, you can manually manage token expiration and maintain a list of revoked tokens, which is essentially removing the refresh token key from dictionary.
// Example code to revoke a refresh token
public IActionResult Revoke([FromBody] RevokeRequest request)
{
if (RefreshTokens.ContainsKey(request.RefreshToken))
{
// Remove the refresh token to revoke it
RefreshTokens.Remove(request.RefreshToken);
return Ok("Token revoked successfully");
}
return BadRequest("Invalid refresh token");
}
Keep in mind that using an in-memory approach for refresh tokens has limitations, especially in a distributed or scaled environment. If your application needs to scale across multiple servers or instances, you might need a more advanced solution, like a distributed cache or a database. For production scenarios, consider using a database to store refresh tokens securely.
Conclusion
JSON Web Token (JWT) based authentication is a widely adopted approach for securing web applications and APIs. JWTs provide a stateless and scalable solution, offering several advantages such as simplicity, flexibility, and ease of implementation. However, it’s crucial to consider both the strengths and potential challenges associated with JWT-based authentication.
The codebase for this project can be found here.
We are covering all authentication and authorization process from scratch for web application. Have a look on other authentication and authorization process, when to use them, and how to use them.