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