Skip to content

Instantly share code, notes, and snippets.

@ddjerqq
Last active April 6, 2025 20:21
Show Gist options
  • Save ddjerqq/00aa18ac2d9d3fb8361d1eaa315110e1 to your computer and use it in GitHub Desktop.
Save ddjerqq/00aa18ac2d9d3fb8361d1eaa315110e1 to your computer and use it in GitHub Desktop.
asp.net core JWT with RSA JWE payload encryption and ECDSA nistP256 for signature generation, all loaded from keys and env variables
JWT__ISSUER=me
JWT__AUDIENCE=you
JWT__EXPIRATION=01:00:00
JWT__ENCRYPTION_KEY_PASSWORD=private key password
JWT__ENCRYPTION_KEY_PATH=/path/to/jwt_encryption_key.pem
JWT__SIGNING_KEY_PATH=/path/to/jwt_signing_key.pem
eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJ0eXAiOiJKV1QiLCJjdHkiOiJKV1QifQ.q5DrcSnq7srP5_CC8rl97DTPQaR74fhYu6qQN4KuE4DDBFjC__HROppadLGt1lthuqMkGMmsYXQoX-uUWvkuJ0p2VVt8L5eB1dwwY1cwIDKuxbIapnysDgMMWGRzClfFKqSYizvLnYldbrBTom8YC9RKkhcNUpmYCfwV_GueBFir6lLUojr7royw0y9RwjP9c6GuS_4dJwu-CxjWjtHmECT9-O8dgE1M2lEOPyukpExkPwR0jOyJGbjgO8jyThApxVn5nYD6yAVhuqLsmrfcIRZweoiiIEUvUoe8QXLf3LK2pgKjHupHeYk1L0qF5y3x04hPAWN1jTIlqdZ-0gZ4HGeJ62by4MGJotu4IN2Wn_52n76DZlFDGtDvhhzDeJiuaQsUcp1tlWcqayyjys-GSvOTq01ketr1e7dLKS9j5sq3AI-Auh_4D0F6FoprQyyuNXQM7etVdQItuo9G4f5TkOJ3LZUsu7QxKDFtsJNLEEvnznawsn-U7-Ia1N9Hns7DndSNHFtwkSm5AbXWHU8rP3HxE-jPHxj8TakQij8cqGWCWINocaAhxMTcwuTWIefz7aDu_uNPxgMcPpYmxk39vYx0dcRdP-gh2CX4JGezLm_5qyvqapLQCtRwIy8PxdKRU0w2M1DwvKSc_Ktmo5iHvPvh0KQnL9VSdggbTRXrCJI.0rRkpdH3En-b2DUpXQbGXQ.Ai6OsddKL3eY32X8Z-1upeE9X6-gmRMPr_eaCyhWFpyew_rdpE_3zLUv0oQmeZnBD6qO0Ees0Y_tD_HHRsjFgz5Y_9RTe2qMmOV4NtixXu0GT-2XjN3C2tyv51y7SuBsVmC5cBZFXPRMlqAZ-eZarkjGkUCgc7L_HunmvjwdVtKaaIMRkHGzWFku8cZBckZ2N1YCSZbGAXU_8nWH2SWiBQfmI9PvgLUlGJfedBaDy3e0rXRaQ5l5Pv9ZdIiQKi43RChWda7QeiPgy1TY_xsl78wdgdgnVaWS-_GR9aUhpwxjVFvVly1_GSoSCxT1O4obju44uwWzfGSb6Qne0HJmuvDE24y38Skocem98_r1HYCjNwE_MeJ4JwOLZbtkOjRDUg80BbqT8toBjmBTKx7i_puSCin3mpX3oSKowFfirj8p7OeEUC4FSZjUmUKe5zui8tROK6YwTZHI4gFvLuNxnSXIgH41Jibdeqd2eVZLEE9hVE9_6VlBWmee3qH70sOOpyOEtv6hVqNn816aBr0fqpzWRtPXR_j3RBCc6OoGQUxNgb24xs05q9OmO3stchIx9s-MSdc-AR4IuVPxkdv71gvn8bhvBu_C8U7IYXCBpF_S-brZUSZeFBgfW-yU5SADBOX9U-BAzOyJfn0MOCjy-pjHA7OmAP72xSo1h3XGBB1GUaOPyF-VcaWj8sxok_L7SIjRDuCRb42Etr1FdDp_VpYdzBZyAAaIRdrftwCXwnG7gQ3ky38hjSG5y9WQW6zFeOwGFWssVwmm6ahV6yNhysQUev_F5_gxYJnC6ElbMEs.FVoiab0nStvo9g_6Bn5DJ-PkjwYLcpdldlBYnH6gWf0
/// <summary>
/// Represents a service that manages JWT tokens.
/// </summary>
public interface IJwtGenerator
{
/// <summary>
/// Generates a JWT token with the specified claims, expiration, and date time provider.
/// </summary>
public string GenerateToken(IEnumerable<Claim> claims, TimeSpan? expiration = null);
/// <summary>
/// Tries to validate the specified token and returns the claims if the token is valid.
/// </summary>
public Task<IEnumerable<Claim>> TryValidateTokenAsync(string token);
}
public sealed class JwtGenerator : IJwtGenerator
{
public const string CookieName = "authorization";
private static readonly string[] ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512];
public static readonly string ClaimsIssuer = "JWT__ISSUER".FromEnvRequired();
public static readonly string ClaimsAudience = "JWT__AUDIENCE".FromEnvRequired();
public static readonly TimeSpan Expiration = TimeSpan.Parse("JWT__EXPIRATION".FromEnvRequired());
public static readonly string EncryptionKeyPassword = "JWT__ENCRYPTION_KEY_PASSWORD".FromEnvRequired();
public static readonly string EncryptionKeyPath = "JWT__ENCRYPTION_KEY_PATH".FromEnvRequired();
public static readonly string SigningKeyPath = "JWT__SIGNING_KEY_PATH".FromEnvRequired();
private readonly JsonWebTokenHandler _handler;
private readonly RsaSecurityKey _privateEncryptionKey;
private readonly RsaSecurityKey _publicEncryptionKey;
private readonly ECDsaSecurityKey _privateSigningKey;
private readonly ECDsaSecurityKey _publicSigningKey;
public readonly TokenValidationParameters TokenValidationParameters;
public JwtGenerator()
{
var encryptionKey = RSA.Create();
var encryptionKeyText = File.ReadAllText(EncryptionKeyPath);
encryptionKey.ImportFromEncryptedPem(encryptionKeyText, Encoding.UTF8.GetBytes(EncryptionKeyPassword));
var signingKeyText = File.ReadAllText(SigningKeyPath);
var signingKey = ECDsa.Create();
signingKey.ImportFromPem(signingKeyText);
_handler = new JsonWebTokenHandler();
_privateEncryptionKey = new RsaSecurityKey(encryptionKey);
_publicEncryptionKey = new RsaSecurityKey(encryptionKey.ExportParameters(false));
_privateSigningKey = new ECDsaSecurityKey(signingKey);
_publicSigningKey = new ECDsaSecurityKey(ECDsa.Create(signingKey.ExportParameters(false)));
TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = ClaimsIssuer,
ValidAudience = ClaimsAudience,
IssuerSigningKey = _publicSigningKey,
TokenDecryptionKey = _privateEncryptionKey,
RequireSignedTokens = true,
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAlgorithms = ValidAlgorithms,
NameClaimType = ClaimsPrincipalExt.IdClaimType,
RoleClaimType = ClaimsPrincipalExt.RoleClaimType,
};
}
public string GenerateToken(IEnumerable<Claim> claims, TimeSpan? expiration = null)
{
expiration ??= Expiration;
var tokenDescriptor = new SecurityTokenDescriptor
{
Issuer = ClaimsIssuer,
Audience = ClaimsAudience,
Claims = claims.ToDictionary(claim => claim.Type, object (claim) => claim.Value),
Expires = DateTime.UtcNow.Add(expiration.Value),
SigningCredentials = new SigningCredentials(_privateSigningKey, SecurityAlgorithms.EcdsaSha256),
EncryptingCredentials = new EncryptingCredentials(_publicEncryptionKey, SecurityAlgorithms.RsaOAEP, SecurityAlgorithms.Aes256CbcHmacSha512),
};
return _handler.CreateToken(tokenDescriptor);
}
public async Task<IEnumerable<Claim>> TryValidateTokenAsync(string token)
{
try
{
var result = await _handler.ValidateTokenAsync(token, TokenValidationParameters);
if (!result.IsValid)
throw new SecurityTokenInvalidSignatureException("Invalid token signature");
return result.ClaimsIdentity.Claims;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to validate token");
return [];
}
}
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IJwtGenerator, JwtGenerator>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var serviceProvider = services.BuildServiceProvider();
var jwtGenerator = (serviceProvider.GetRequiredService<IJwtGenerator>() as JwtGenerator)!;
options.MapInboundClaims = false;
options.RequireHttpsMetadata = true;
options.SaveToken = true;
options.Audience = JwtGenerator.ClaimsAudience;
options.ClaimsIssuer = JwtGenerator.ClaimsIssuer;
options.Events = Events;
options.TokenValidationParameters = jwtGenerator.TokenValidationParameters;
});
private static readonly JwtBearerEvents Events = new()
{
OnMessageReceived = ctx =>
{
ctx.Request.Query.TryGetValue(JwtGenerator.CookieName, out var query);
ctx.Request.Headers.TryGetValue(JwtGenerator.CookieName, out var header);
ctx.Request.Cookies.TryGetValue(JwtGenerator.CookieName, out var cookie);
ctx.Token = (string?)query ?? (string?)header ?? cookie;
return Task.CompletedTask;
},
OnForbidden = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.StatusCode = 403;
return Task.CompletedTask;
}
ctx.Response.Redirect("auth/denied");
return Task.CompletedTask;
},
OnChallenge = ctx =>
{
if (ctx.Request.Path.StartsWithSegments("/api"))
{
ctx.Response.StatusCode = 401;
return Task.CompletedTask;
}
ctx.Response.Redirect("auth/login");
ctx.HandleResponse();
return Task.CompletedTask;
},
};
public sealed class Service(IJwtGenerator jwt)
{
public string GenerateToken(User user, string purpose)
{
Claim[] claims =
[
new("purpose", purpose),
new("security_stamp", user.SecurityStamp),
new("sid", user.Id.ToString()),
];
return jwt.GenerateToken(claims);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment