Implementing a Simple Authentication Mechanism in ASP.NET Core Step-by-Step

Mustafa Bektaş
8 min readOct 9, 2023

--

Photo by Onur Binay on Unsplash

In today’s blog, we’re diving into the realm of authentication, exploring the implementation of a simple yet effective authentication mechanism using JSON Web Tokens (JWT). I’ll guide you step by step, from setting up the project structure to integrating Swagger for easy API documentation and testing. By the end of this post, you’ll not only understand how JWT works but also have a fully functional authentication system in your ASP.NET Core application, ready to safeguard your data and control access to your APIs.

Creating the Project

To initiate our project, we start by creating an empty Web API project. Using the terminal, navigate to an empty folder and execute the following commands:

dotnet new webapi -n SimpleAuthenticationProject
cd SimpleAuthenticationProject

Here is the folder structure for our created project. Locate and remove the WeatherForecast.cs and WeatherForecastController.cs files. These are unnecessary for our current implementation.

Don’t forget to delete the WeatherForecast.cs and WeatherForecastController.cs files that came with the template. We won’t need them.

Login Model

In the Models folder, create a file named LoginModel.cs. This class acts as a template for the login information. Here’s the code:

using System.ComponentModel.DataAnnotations;

namespace SimpleAuthenticationProject.Models;

public class LoginModel
{
[Required(ErrorMessage = "Username is required")]
public string? Username { get; set; }

[Required(ErrorMessage = "Password is required")]
public string? Password { get; set; }
}

ValuesController

Now let’s create a simple controller named ValuesControllerwith aGet() method with a simple list inside to mock the process of retrieving some data from a database. Add the [Authorize] attribute to the Get() method to indicate that we require authorization to access the method.

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace SimpleAuthenticationProject.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
private ILogger<ValuesController> _logger;

public ValuesController(ILogger<ValuesController> logger)
{
_logger = logger;
}

[HttpGet]
[Authorize]
public IActionResult Get()
{

_logger.LogInformation("Fetching all elements from the list.");

var tList = new List<string>() { "John", "Doe", "123", "ABC123" };

_logger.LogInformation($"Returning {tList.Count} elements.");

return Ok(tList);

}
}
}

AccountController for Handling the Authentication Process

Now let’s create another controller named AccountController to handle the authentication process and log in as an authenticated user. Don’t forget to install theMicrosoft.AspNetCore.Authentication.JwtBearer NuGet package to your project.

using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using SimpleAuthenticationProject.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;

namespace SimpleAuthenticationProject.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AccountController : ControllerBase
{
[HttpPost, Route("login")]
public IActionResult Login(LoginModel model)
{
if (model.Username == "user" && model.Password == "admin")
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secretkey@0506"));
var loginCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokenOptions = new JwtSecurityToken(
issuer: "Echonos",
audience: "https://localhost:5001",
claims: new List<Claim>(),
expires: DateTime.Now.AddMinutes(5),
signingCredentials: loginCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return Ok(new { Token = tokenString });
}
else
{
return Unauthorized();
}
}
}
}

Before I explain the code, let’s briefly explore JSON Web Tokens (JWT). JWT is a compact and self-contained way of representing information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs are commonly used for authentication and information exchange in web development. They consist of three parts: a header, a payload, and a signature. The header typically consists of the type of the token (which is JWT) and the signing algorithm being used, such as HMAC SHA256 or RSA. The payload contains claims, which are statements about an entity (typically, the user) and additional data. The signature is created by encoding the header, encoding the payload, combining them with a secret key, and then applying the signing algorithm. This signature can be used to verify that the sender of the JWT is who it says it is and to ensure that the message wasn’t changed along the way.

With this understanding, let’s walk through the code implementation step by step:

Step 1: Login Action Method

We define a Login action method that is triggered when a POST request is made to the /api/account/login endpoint. The method expects a JSON object containing Username and Password properties sent in the request body.

Step 2: Authentication Logic

Within the Login method, we validate the provided Username and Password. For the sake of simplicity, we've hardcoded the expected values. If the provided credentials match the hardcoded values, the code proceeds to generate a JWT token. If not, an Unauthorized response is returned.

Step 3: Generating JWT Token

In this step, we generate the JWT token using the JwtSecurityToken class. Let's go through the code line by line to understand each part:

1. Symmetric Security Key:

var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secretkeyfortesting@0506"));

Here, we create a symmetric security key by encoding a secret string (“secretkey@0506”) into bytes. This key will be used to sign the JWT token and later to verify its authenticity.

2. Signing Credentials:

var loginCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);

The SigningCredentials class holds the key and the algorithm used for signing the token. In this case, we're using HMAC SHA256 algorithm for signing.

3. JWT Token Options:

var tokenOptions = new JwtSecurityToken(
issuer: "Echonos",
audience: "https://localhost:5001",
claims: new List<Claim>(),
expires: DateTime.Now.AddMinutes(5),
signingCredentials: loginCredentials
);

Here, we create a new instance of JwtSecurityToken and specify various parameters:

  • Issuer (issuer): Indicates the entity that issued the token. In this case, it's "Echonos."
  • Audience (audience): Specifies the intended recipient of the token. It's set to the URL where the token is intended to be used, for example, "https://localhost:5001."
  • Claims (claims): Claims represent statements about an entity and additional data. For instance, if your application has different user roles like “Admin” and “User,” you could include a claim indicating the user’s role. When the client receives the JWT, it can extract and read the claims to understand the user’s role and grant appropriate access or permissions within the application. In this example, no specific claims are included (new List<Claim>() is empty).
  • Expiration (expires): Sets the expiration time of the token. Here, it's set to expire 5 minutes from the current time (DateTime.Now.AddMinutes(5)).
  • Signing Credentials (signingCredentials): Specifies the key and algorithm used for signing the token.

4. Writing and Returning the Token:

var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return Ok(new { Token = tokenString });

In this last part, the JwtSecurityTokenHandler().WriteToken() method is used to write the token into a string representation. This string representation of the JWT token is returned in the HTTP response body. The client can then use this token for subsequent authenticated requests by including it in the Authorization header of the HTTP request.

Our authentication handling mechanism is complete. Now add the required services to the pipeline in the Program.cs :

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

builder.Services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,

ValidIssuer = "Echonos",
ValidAudience = "https://localhost:5001",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secretkeyfortesting@0506"))
};
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(opt =>
{
opt.SwaggerDoc("v1", new OpenApiInfo { Title = "SimpleAuthenticationProject", Version = "v1" });
opt.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "bearer"
});

opt.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type=ReferenceType.SecurityScheme,
Id="Bearer"
}
},
new string[]{}
}
});
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

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

app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});

app.Run();

Let’s also explain the modifications to this file:

Setting up JWT Authentication

builder.Services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,

ValidIssuer = "Echonos",
ValidAudience = "https://localhost:5001",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("secretkeyfortesting@0506"))
};
});

In this block of code, we configure JWT (JSON Web Token) authentication. We specify that our application should use JWT as the default authentication and challenge scheme. The AddJwtBearer method sets up the JWT bearer authentication, where we define parameters for token validation. We ensure the JWT's issuer, audience, lifetime, and the signing key are all validated to enhance the security of our authentication mechanism.

Integrating Swagger for API Documentation and Testing

builder.Services.AddSwaggerGen(opt =>
{
opt.SwaggerDoc("v1", new OpenApiInfo { Title = "SimpleAuthenticationProject", Version = "v1" });
opt.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Description = "Please enter token",
Name = "Authorization",
Type = SecuritySchemeType.Http,
BearerFormat = "JWT",
Scheme = "bearer"
});

opt.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type=ReferenceType.SecurityScheme,
Id="Bearer"
}
},
new string[]{}
}
});
});

This segment of code integrates Swagger, a powerful tool for API documentation and testing. We define the API’s version and title, ensuring clarity in the documentation. Additionally, we specify how authentication is handled in Swagger. The AddSecurityDefinition method configures the security scheme as a bearer token, expecting the JWT to be included in the request header. The AddSecurityRequirement method enforces the presence of this bearer token in Swagger requests, ensuring that API interactions in the documentation are authenticated correctly.

Now that our implementation is complete, let’s test our program using Swagger. After you run the project, you will be greeted by the Swagger interface.

Swagger allows us to test our controllers easily

Let’s first try accessing the Get() method of our ValuesController before authorizing.

As expected, we received a 401 Error: Unauthorized response. Now let’s try authorizing ourselves to gain access to this method.

We provided our hardcoded login information in the request body and clicked Execute. As you can see, our controller returned an encrypted token. We can use this token in our future requests’ header to access restricted controller actions. Let’s tell Swagger to use this token for future requests by clicking Authorize in the top right corner and pasting our token.

Click authorize and log in with your token.

Now that we are logged in, we can access the Get() method of our ValuesController :

We are successfully authorized and we accessed the contents of our mock database list.

Conclusion

In this tutorial, we’ve implemented a simple authentication mechanism in ASP.NET Core, breaking down the process into digestible steps. From configuring JWT tokens to integrating Swagger for seamless API testing, we’ve covered the essentials of building a secure authentication mechanism. Armed with this knowledge, I hope you’re well-equipped to enhance the security of your ASP.NET Core applications and provide users with a robust, authenticated experience. Thank you for reading.

--

--