2 min read

Modifications of the JWT token generated by the application

If the word JWT is new to you, I would suggesting taking a glance here https://jwt.io/introduction/ .

There are quite a few ways in which you can pass the TenantId in your app. We have decided to go with this architecture for a number of reasons:

  • It is a standard way to pass parameters in calls and it is accepted in the community
  • Security wise, the TenantId it is embedded in the JWT, so you have to decode the token to see it
  • Minimal modifications required in the application ( redesign mode on ? )

After doing the update on the DB part and scaffolding the structure ( we are in a DB first scenario ) you can apply the following updates in your .NET Core 3.1 app.

Below you will find the modifications needed so that AuthService.cs can accommodate the changes in the JWT token claims:

public class AuthService : IAuthService
{

	public async Task<AuthInfoViewModel.Result> Login(AuthInfoViewModel.Login viewModel)
	{
		...
		
		try
		{
			...
			
			using (transaction = Context.Database.BeginTransaction())
			{
			// find all the users in the context of the authentication
				var user = await Context.Set<User>()
					.Include(x => x.UserEntity).ThenInclude(x => x.Entity)
					.Where(x => x.Username.Equals(viewModel.Username) && !x.IsDeleted)
					.FirstOrDefaultAsync();
			// find all the tenants attached to the user via the bridge table
				var tenantAccountIds = await Context.Set<TenantUser>()
				   .Where(x => x.UserId == user.Id && !x.IsDeleted)
				   .Select(x => x.TenantId)
				   .ToListAsync();
			// find all the tenantId guids so you can embed them into the token
				var tenantAccountGuids = await Context.Set<Tenant>()
			   .Where(x => tenantAccountIds.Contains(x.Id) && !x.IsDeleted)
			   .Select(x => x.TenantId)
			   .ToListAsync();

				...

				#region Create a User Login record.
				var granted = new DateTime();
				var expiry = new DateTime();
				//generate the token with the tenants attached
				token = viewModel.IsRelogin ? viewModel.Token : GetToken(user, tenantAccountGuids, out granted, out expiry).ToString();

			   ...
			}
		}
		catch (Exception e)
		{
			if (transaction != null)
				transaction.Rollback();

			return new AuthInfoViewModel.Result { Success = false, Message = e.ToString() };
		}
	}


	object GetToken(Users user,List<Guid> tenantAccountGuids, out DateTime granted, out DateTime expiry)
	{
		var utcNow = DateTime.UtcNow;

		var claims = new List<Claim>();
		 // all you claims that you already have in your JWT token
		 …
		// plus every tenant you need to be present in the token
		foreach (var tenantAccountGuid in tenantAccountGuids)
		{
			claims.Add(new Claim("tenant_ids", tenantAccountGuid.ToString()));
		}


		var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.Config.GetValue<string>("Tokens:Key")));
		var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
		var jwt = new JwtSecurityToken(
			signingCredentials: signingCredentials,
			claims: claims,
			notBefore: utcNow,
			expires: utcNow.AddSeconds(this.Config.GetValue<int>("Tokens:Lifetime")),
			audience: this.Config.GetValue<String>("Tokens:Audience"),
			issuer: this.Config.GetValue<String>("Tokens:Issuer"));

		granted = jwt.ValidFrom;
		expiry = jwt.ValidTo;
		return new JwtSecurityTokenHandler().WriteToken(jwt);
	}
}

After applying the code presented you will have a JWT that will contain all the information you had in your token, plus a new tag that will look like this:

"tenant_ids": [
    "89110290-7ede-472a-813e-448740d5a171",
    "2113afce-7574-4de6-aa8c-ff22c73339a7"
  ]

You can decode yours using https://jwt.io/ , that’s if you aren’t already doing that.

Now, you have a token that contains all the tenants that the user has access to.

Chapter 3 : Modifications and Extensions of the DBContext class to accommodate EF Global Query Filters in a DB first project