.NET Core  

Authenticated Requests in .NET 9 Web API [GamesCatalog] 21

Previous part: Encrypting User Passwords [GamesCatalog] 20

Step 1. When the user wants to use the API, we will authenticate them using a bearer token. To do this, we will create a route where they can authenticate and generate a JWT token.
As a starting point, let’s create the function that will generate the token:

Infra

Code

using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;

namespace Services.Functions
{
    public interface IJwtTokenService
    {
        string GenerateToken(int uid, string email, DateTime expireDt);
        int? GetUidFromToken(string token);
    }

    public class JwtTokenService(string jwtKey) : IJwtTokenService
    {
        public string GenerateToken(int uid, string email, DateTime expireDt)
        {
            JwtSecurityTokenHandler tokenHandler = new();
            byte[] key = Encoding.ASCII.GetBytes(jwtKey);
            SecurityTokenDescriptor tokenDescriptor = new()
            {
                Subject = new ClaimsIdentity([new Claim("uid", uid.ToString()), new Claim(ClaimTypes.Email, email)]),
                Expires = expireDt,
                SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
            };
            SecurityToken token = tokenHandler.CreateToken(tokenDescriptor);

            return tokenHandler.WriteToken(token);
        }

        public int? GetUidFromToken(string token)
        {
            if (token == null)
                return null;

            JwtSecurityTokenHandler tokenHandler = new();
            byte[] key = Encoding.ASCII.GetBytes(jwtKey);

            try
            {
                tokenHandler.ValidateToken(token, new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ClockSkew = TimeSpan.Zero
                }, out SecurityToken validatedToken);

                JwtSecurityToken jwtToken = (JwtSecurityToken)validatedToken;
                int uid = int.Parse(jwtToken.Claims.First(x => x.Type == "uid").Value);
                return uid;
            }
            catch
            {
                throw;
            }
        }
    }
}

With this code, we generate a token with the user’s ID and later retrieve it from the token that will be passed.

Step 2. Right-click on the class and generate a test class:

Generate a test class

Code

using Services.Functions;

namespace ServicesTests.Functions
{
    [TestClass()]
    public class JwtTokenServiceTests
    {
        //https://generate-random.org/encryption-key-generator?count=1&bytes=32&cipher=aes-256-cbc&string=&password=
        private const string JwtKey = "iezfxheYc3rduxaqQ+OXQNkbp0MAfZs4jU/8nU+c3isVuvcOdFPV1TzLDIy9X6oe";
        private readonly JwtTokenService _jwtTokenService;

        public JwtTokenServiceTests()
        {
            _jwtTokenService = new JwtTokenService(JwtKey);
        }

        [TestMethod()]
        public void GetUidFromToken_ShouldReturnCorrectUid()
        {
            int uid = 123;
            string email = "[email protected]";
            DateTime expireDt = DateTime.UtcNow.AddHours(1);
            string token = _jwtTokenService.GenerateToken(uid, email, expireDt);

            int? extractedUid = _jwtTokenService.GetUidFromToken(token);

            Assert.IsNotNull(extractedUid);
            Assert.AreEqual(uid, extractedUid);
        }
    }
}

Generate any test key—you can use this site to generate a test key and another for use in configuration. With this test created, check if the key is being correctly generated.

Step 3. Insert your key into appsettings.json:

Appsetting.json

Step 4. In Program.cs, configure the JwtTokenService via DI.

Program.cs configure JwtTokenService via DI

Code

builder.Services.AddScoped<IJwtTokenService, JwtTokenService>(p 
    => new JwtTokenService(builder.Configuration["JwtKey"]));

Step 5. After creating our function to generate the token, let's go to UserRepo and create the function that will retrieve the user from the database if they exist:

Userrepo

Code

    public async Task<UserDTO?> GetByEmailAndPasswordAsync(string email, string encryptedPassword)
    {
        using var context = DbCtx.CreateDbContext();
        return await context.User.FirstOrDefaultAsync(x 
            => x.Email == email && x.Password == encryptedPassword);
    }

Right-click and pull this function to the interface.

Step 6. Let's create a return model for the token:

Solution Explorer

Code

namespace Models.Resps;

public record ResToken(string? Token);

Step 7. In UserService.cs, let's create the function that will generate and return the token:

Step 7.1. Add the use of IJwtTokenService to UserService:

JWT Token Service

And create a function that generates and returns the token in UserService:

     public async Task<BaseResp> GenerateTokenAsync(ReqUserSession reqUserSession)
     {
         string? validateError = reqUserSession.Validate();

         if (!string.IsNullOrEmpty(validateError)) return new BaseResp(null, validateError);

         UserDTO? userResp = await userRepo.GetByEmailAndPasswordAsync(reqUserSession.Email, encryptionService.Encrypt(reqUserSession.Password));

         if (userResp is null) return new BaseResp(null, "User/Password incorrect");

         string userJwt = jwtTokenService.GenerateToken(userResp.Id, userResp.Email, DateTime.UtcNow.AddDays(5));

         ResToken resToken = new(userJwt);

         return new BaseResp(resToken);
     }

Pull this function to the interface.

Step 8. Use it in UserController:

        [Route("Session")]
        [HttpPost]
        public async Task<IActionResult> SignIn(ReqUserSession reqUserSession) => BuildResponse(await userService.GenerateTokenAsync(reqUserSession));

Step 9. Testing it in GamesCatalogAPI.http We receive the token as a response:

API

Code

POST {{GamesCatalogAPI_HostAddress}}/User/Session
Content-Type: application/json
{
  "email": "[email protected]",
  "password": "pass123"
}
###

Step 9.1. If an invalid email or password is provided, we get a failure response:

Headers

Step 10. To use authentication, in Program.cs, we will include key validation to use it in routes where it is required. We will also disable HTTPS redirection:

Testing

Code

#region Auth

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false,
        ValidateIssuer = false,
        ValidateIssuerSigningKey = true,
        ValidateLifetime = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtKey"]))
    };
    options.SaveToken = true;
});

#endregion

Step 11. Let's create a route where authentication is required.

Step 11.1. First in UserRepo, create a function that retrieves the user by their ID:

    public async Task<UserDTO?> GetByIdAsync(int uid)
    {
        using var context = DbCtx.CreateDbContext();
        return  await context.User.FirstOrDefaultAsync(x => x.Id.Equals(uid));
    }

Pull it to the interface.

Step 12. In UserService.cs, create the function that returns the recovered user to the API:

        public async Task<BaseResp> GetByIdAsync(int uid)
        {
            UserDTO? userResp = await userRepo.GetByIdAsync(uid);

            if (userResp == null)
                return new BaseResp(null, "User not found");

            return new BaseResp(new ResUser() { Id = userResp.Id, Name = userResp.Name, Email = userResp.Email, CreatedAt = userResp.CreatedAt });
        }

Pull it to the interface.

Step 13. In BaseController, we’ll add a variable to store the user ID retrieved by the RecoverUidSession function, which will be executed during API calls.

Base controller

Code for declaring the Uid variable:

      protected int Uid { get; set; }

Code for RecoverUidSession and the override of OnActionExecuting:

        protected int? RecoverUidSession()
        {
            string? uid = null;

            if (HttpContext.User.Identity is ClaimsIdentity identity)
                uid = identity.Claims.FirstOrDefault(x => x.Type == "uid")?.Value;

            return uid != null ? Convert.ToInt32(uid) : null;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            Microsoft.Extensions.Primitives.StringValues auth = context.HttpContext.Request.Headers.Authorization;
            if (!string.IsNullOrEmpty(auth))
            {
                int? uid = RecoverUidSession();

                if (uid is null)
                {
                    context.Result = new UnauthorizedObjectResult("unauthorized user");
                    return;
                }

                Uid = uid.Value;
            }

            base.OnActionExecuting(context);
        }

Step 14. With this in place, in UserController, we can create a route that retrieves the user’s name and email using the authentication token:

        [Route("")]
        [HttpGet]
        [Authorize]
        public async Task<IActionResult> GetUser() => BuildResponse(await userService.GetByIdAsync(Uid));

Step 15. In GamesCatalogAPI.http, we will create a test for our request that requires token authentication:

Post

Code

#Auths
@Token = 

GET {{GamesCatalogAPI_HostAddress}}/User
Authorization: Bearer {{Token}}
###

Step 16. Generate a token.

Token

Step 17. Use it to perform a GET request to retrieve the user from this token:

GET request

Note. Make sure to run the system using HTTP. If you run it using HTTPS, UseHttpsRedirection() will cause the Authorization to be lost, resulting in a 401.

In the next part, we will create the password recovery option.

Code on git: GamesCatalogBackEnd