Improving JWT Security Using .NET Core C#

Waqas Ahmed
7 min readSep 29, 2023

--

Improving JWT Security Using .NET Core C#
Improving JWT Security Using .NET Core C#

Securing JWT (JSON Web Tokens) in a .NET Core C# application involves various steps, including token generation, validation, and handling.

To create and validate a JWT token using both a Public Key and a Private Key, you need a certificate that contains both keys. You will use the Private Key to sign the token during creation and the Public Key to validate the token during validation. Here’s how you can do it in C#:

  1. Install the required NuGet packages:
Install-Package System.IdentityModel.Tokens.Jwt

2. Create an Interface for token handling:

It looks like you have defined an interface called ITokenService in C#. This interface appears to be related to token generation and validation for authentication and authorization purposes. Here's a breakdown of the methods in this interface:

  1. GenerateAccessToken(IEnumerable<Claim> claims, IConfiguration _config): This method generates an access token based on a collection of claims and a configuration object (_config). Access tokens are typically used for authorizing and authenticating users or applications.
  2. GenerateRefreshToken(): This method generates a refresh token. Refresh tokens are often used in token-based authentication systems to obtain new access tokens without requiring the user to log in again.
  3. GetPrincipalFromExpiredToken(string token, IConfiguration _config): This method extracts a ClaimsPrincipal from an expired token. A ClaimsPrincipal represents the claims of a user or entity and is commonly used for identity and authorization purposes.
  4. ValidateJwtToken(string token): This method validates a JWT (JSON Web Token). JWTs are a commonly used token format in authentication systems. This method likely checks the signature, expiration, and other aspects of the token to ensure its validity.
public interface ITokenService
{
string GenerateAccessToken(IEnumerable<Claim> claims, IConfiguration _config);
string GenerateRefreshToken();
ClaimsPrincipal GetPrincipalFromExpiredToken(string token, IConfiguration _config);
JwtSecurityToken ValidateJwtToken(string token);
}

3. Create a class for implement Interface:

 public class TokenService: ITokenService
{
private readonly IAzureConfigurationService _azureConfigurationService;

public TokenService(IAzureConfigurationService azureConfigurationService)
{
_azureConfigurationService = azureConfigurationService;
}

public string GenerateAccessToken(IEnumerable < Claim > claims, IConfiguration _config)
{
var privateKey = RSAKeyConverterService.CreateRsaFromPrivateKey(_azureConfigurationService.AzureConfig.Jwt.PrivateKey);
var signingCredentials = new SigningCredentials(new RsaSecurityKey(privateKey), SecurityAlgorithms.RsaSha256);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(claims),
Expires = DateTime.UtcNow.AddMinutes(Convert.ToDouble(_azureConfigurationService.AzureConfig.Jwt.TokenExpiry)),
Audience = _azureConfigurationService.AzureConfig.Jwt.Audience,
Issuer = _azureConfigurationService.AzureConfig.Jwt.Issuer,
SigningCredentials = signingCredentials
};
var tokenHandler = new JwtSecurityTokenHandler();
var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}

public string GenerateRefreshToken()
{
var secureRandomBytes = new byte[128];
using
var randomNumberGenerator = RandomNumberGenerator.Create();
randomNumberGenerator.GetBytes(secureRandomBytes);
var refreshToken = Convert.ToBase64String(secureRandomBytes);
return refreshToken;
}

public ClaimsPrincipal GetPrincipalFromExpiredToken(string token, IConfiguration _config)
{
var tokenHandler = new JwtSecurityTokenHandler();
var _publicKey = RSAKeyConverterService.CreateRsaFromPrivateKey(_azureConfigurationService.AzureConfig.Jwt.PublicKey);
var tokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(_publicKey),
ValidateIssuer = true, // Set to true if you want to validate the Issuer
ValidateAudience = true, // Set to true if you want to validate the Audience
ClockSkew = TimeSpan.Zero // No clock skew for simplicity (adjust as needed)
};
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if(jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase)) throw new SecurityTokenException("Invalid token");
return principal;
}

public JwtSecurityToken ValidateJwtToken(string token)
{
if(token == null) return null;
var tokenHandler = new JwtSecurityTokenHandler();
var PrivateKey = RSAKeyConverterService.CreateRsaFromPrivateKey(_azureConfigurationService.AzureConfig.Jwt.PrivateKey);
try
{
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new RsaSecurityKey(PrivateKey),
ValidateIssuer = false,
ValidateAudience = false,
// set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken) validatedToken;
return jwtToken;
}
catch
{
return null;
}
}
}

4. If you need the RSA keys as plain strings (PEM format) instead of XML, you can convert the keys to PEM format using the PemUtils class from the Org.BouncyCastle.OpenSsl namespace, which is part of the BouncyCastle library. You can install the BouncyCastle library from NuGet:

Install-Package BouncyCastle

Here’s the updated code to generate RSA public and private keys as PEM strings:

public static class RSAKeyConverterService
{
public static RSA CreateRsaFromPublicKey(string publicKey)
{
// Convert the PEM public key to PKCS#1 format
publicKey = ConvertPemPublicKeyToPkcs1(publicKey);
var publicKeyBytes = Convert.FromBase64String(publicKey);
var rsa = RSA.Create();
rsa.ImportRSAPublicKey(publicKeyBytes, out _);
return rsa;
}

public static RSA CreateRsaFromPrivateKey(string privateKey)
{
// Convert the PEM private key to PKCS#1 format
privateKey = ConvertPemPrivateKeyToPkcs1(privateKey);
var privateKeyBytes = Convert.FromBase64String(privateKey);
var rsa = RSA.Create();
rsa.ImportRSAPrivateKey(privateKeyBytes, out _);
return rsa;
}

private static string ConvertPemPublicKeyToPkcs1(string publicKey)
{
var rsaKeyParameters = (RsaKeyParameters) new PemReader(new StringReader(publicKey)).ReadObject();
var rsaParameters = new RSAParameters
{
Modulus = rsaKeyParameters.Modulus.ToByteArrayUnsigned(),
Exponent = rsaKeyParameters.Exponent.ToByteArrayUnsigned(),
};
using(var rsa = RSA.Create())
{
rsa.ImportParameters(rsaParameters);
return Convert.ToBase64String(rsa.ExportRSAPublicKey());
}
}

private static string ConvertPemPrivateKeyToPkcs1(string privateKey)
{
var rsaKeyPair = (AsymmetricCipherKeyPair) new PemReader(new StringReader(privateKey)).ReadObject();
var rsaParams = ToRSAParameters((RsaPrivateCrtKeyParameters) rsaKeyPair.Private);
using(var rsa = RSA.Create())
{
rsa.ImportParameters(rsaParams);
return Convert.ToBase64String(rsa.ExportRSAPrivateKey());
}
}

private static RSAParameters ToRSAParameters(RsaPrivateCrtKeyParameters privKey)
{
var rsaParams = new RSAParameters
{
Modulus = privKey.Modulus.ToByteArrayUnsigned(),
Exponent = privKey.PublicExponent.ToByteArrayUnsigned(),
D = privKey.Exponent.ToByteArrayUnsigned(),
P = privKey.P.ToByteArrayUnsigned(),
Q = privKey.Q.ToByteArrayUnsigned(),
DP = privKey.DP.ToByteArrayUnsigned(),
DQ = privKey.DQ.ToByteArrayUnsigned(),
InverseQ = privKey.QInv.ToByteArrayUnsigned(),
};
return rsaParams;
}
}

5. Centralized Configuration:

public interface IAzureConfigurationService
{
AzureConfigInfo AzureConfig
{
get;
set;
}
}
 public class AzureConfigurationService: IAzureConfigurationService
{
public AzureConfigurationService(IConfiguration configuration)
{
AzureConfig = JsonConvert.DeserializeObject <AzureConfigInfo> (configuration.GetSection("AzureConfig").Value);
}
public AzureConfigInfo AzureConfig
{
get;
set;
}
}
public class AzureConfigInfo
{
public Jwt Jwt
{
get;
set;
}
}
public class Jwt
{
public string Issuer
{
get;
set;
}
public string Audience
{
get;
set;
}
public string PublicKey
{
get;
set;
}
public string PrivateKey
{
get;
set;
}
public string TokenExpiry
{
get;
set;
}
}

6. Here is the interface where you can generate Public and Private key:

public interface IRSAKeyGenerator
{
(string publicKey, string privateKey) GenerateKeys();
}

7. Implementation of IRSAKeyGenerator interface:

using Drive.Services.Abstractions;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl;
using System.Security.Cryptography;
namespace RSAKeyGeneratorService
{
public class RSAKeyGeneratorService: IRSAKeyGenerator
{
/// <summary>
/// Usage of this method var (publicKey, privateKey) = GenerateKeys();
/// </summary>
/// <returns></returns>
public(string publicKey, string privateKey) GenerateKeys()
{
using(var rsa = new RSACryptoServiceProvider(2048))
{
var publicKey = ExportPublicKey(rsa);
var privateKey = ExportPrivateKey(rsa);
return (publicKey, privateKey);
}
}

private string ExportPublicKey(RSACryptoServiceProvider csp)
{
var parameters = csp.ExportParameters(false);
using(var writer = new StringWriter())
{
var pemWriter = new PemWriter(writer);
pemWriter.WriteObject(new RsaKeyParameters(false, new Org.BouncyCastle.Math.BigInteger(1, parameters.Modulus), new Org.BouncyCastle.Math.BigInteger(1, parameters.Exponent)));
return writer.ToString();
}
}

private string ExportPrivateKey(RSACryptoServiceProvider csp)
{
var parameters = csp.ExportParameters(true);
using(var writer = new StringWriter())
{
var pemWriter = new PemWriter(writer);
pemWriter.WriteObject(new RsaPrivateCrtKeyParameters(
new Org.BouncyCastle.Math.BigInteger(1, parameters.Modulus),
new Org.BouncyCastle.Math.BigInteger(1, parameters.Exponent),
new Org.BouncyCastle.Math.BigInteger(1, parameters.D),
new Org.BouncyCastle.Math.BigInteger(1, parameters.P),
new Org.BouncyCastle.Math.BigInteger(1, parameters.Q),
new Org.BouncyCastle.Math.BigInteger(1, parameters.DP),
new Org.BouncyCastle.Math.BigInteger(1, parameters.DQ),
new Org.BouncyCastle.Math.BigInteger(1, parameters.InverseQ)
));
return writer.ToString();
}
}

}
}

In this updated example, the RSAKeyGenerator class uses the BouncyCastle library to convert the RSA keys to PEM format. The ExportPublicKey and ExportPrivateKey methods generate the public and private keys as PEM strings, respectively.

Make sure you’ve added the BouncyCastle library to your project to use this code.

8. Finally, use the SecurityProvider class to Generate Token and Refresh Token :

public class SecurityProvider : ISecurityProvider
{
private readonly ITokenService _tokenService;
private readonly IConfiguration _configuration;
public SecurityProvider(IConfiguration config, ITokenService tokenService)
{
this._tokenService = tokenService;
this._configuration = config;
}

public async Task <TokenRequest> CreateToken(SignInRequest signInRequest)
{
var _userinfo = await _securityRepository.GetUserInfoByNumber(signInRequest.username);
if (_userinfo == null) throw new Exception("Invalid Credentails");

var roles = await GetUserRoleList(_userinfo.Id);
var claims = new []
{
new Claim(ClaimTypes.Sid, _userinfo.Id.ToString()),
new Claim(ClaimTypes.Email, _userinfo.email),
new Claim(ClaimTypes.Upn, _userinfo.email),
new Claim(ClaimTypes.Name, _userinfo.firstName),
new Claim("UserType", _userinfo.userType),
new Claim(ClaimTypes.Role, String.Join(",", roles)),
};
return new TokenRequest()
{
AccessToken = _tokenService.GenerateAccessToken(claims, _configuration),
RefreshToken = _tokenService.GenerateRefreshToken()
};
}

public TokenRequest RefreshToken(TokenRequest tokenRequest)
{
ClaimsPrincipal claimPrincipal = _tokenService.GetPrincipalFromExpiredToken(tokenRequest.AccessToken, _configuration);
if (claimPrincipal is null) throw new Exception("Invalid client request");
var jwtToken = _tokenService.ValidateJwtToken(tokenRequest.AccessToken);
if (jwtToken is null) throw new Exception("Invalid client request");
var exp = jwtToken.Claims.Where(x => x.Type == "exp").FirstOrDefault();
var token_TimeStamp = DateTimeHelper.UnixTimeStampToDateTime(Convert.ToDouble(exp.Value));
if (token_TimeStamp >= DateTime.Now)
{
return tokenRequest;
}

return new TokenRequest()
{
AccessToken = _tokenService.GenerateAccessToken(claimPrincipal.Claims, _configuration),
RefreshToken = _tokenService.GenerateRefreshToken()
};
}
}
public async Task<TokenRequest> CreateToken(SignInRequest signInRequest)

This method is used to create an access token for a user when they sign in. It performs the following steps:

  1. Get User Info: It calls _securityRepository.GetUserInfoByNumber to retrieve user information based on the provided username. If no user is found, it throws an "Invalid Credentials" exception.
  2. Get User Roles: It calls GetUserRoleList to retrieve a list of roles associated with the user.
  3. Claims Generation: It creates a list of claims using user information, such as user ID, email, name, user type, and roles. These claims are essential for authorization and can be embedded in the access token.
  4. Return TokenRequest: It returns a TokenRequest object containing an access token generated by _tokenService.GenerateAccessToken and a refresh token generated by _tokenService.GenerateRefreshToken.

RefreshToken Method:

public TokenRequest RefreshToken(TokenRequest tokenRequest)

This method is used to refresh an access token. It verifies the validity of the existing access token and provides a new one if the existing one is expired or invalid. Here’s what it does:

  1. Decode Claims: It decodes the claims from the provided access token using _tokenService.GetPrincipalFromExpiredToken.
  2. Token Validation: It validates the JWT token using _tokenService.ValidateJwtToken. If the token is invalid or cannot be validated, it throws an "Invalid client request" exception.
  3. Check Token Expiry: It checks the expiration time (exp) of the JWT token and compares it to the current time. If the token is still valid, it returns the provided tokenRequest as no refresh is needed.
  4. Generate New Token: If the token has expired, it generates a new access token using the claims from the original token and a new refresh token using _tokenService.GenerateAccessToken and _tokenService.GenerateRefreshToken, respectively.

Overall, these methods are essential for managing user authentication and authorization, ensuring that users have valid access tokens to use protected resources within your application. The TokenRequest objects likely contain the access and refresh tokens necessary for subsequent API requests and maintaining user sessions.

--

--

Waqas Ahmed

Microsoft Azure Enthusiast ☁ | Principal Software Engineer | Angular | React.Js | .Net Core | .Net | Azure | Micro Services 👉 https://bit.ly/3AhEyOz