Last active
April 6, 2025 20:21
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 []; | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}, | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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