| | 1 | | using System.IdentityModel.Tokens.Jwt; |
| | 2 | | using System.Security.Claims; |
| | 3 | | using System.Security.Cryptography; |
| | 4 | | using System.Security.Cryptography.X509Certificates; |
| | 5 | | using Microsoft.IdentityModel.Tokens; |
| | 6 | | using Serilog; |
| | 7 | | using Serilog.Events; |
| | 8 | | using System.Text; |
| | 9 | | using System.Security; |
| | 10 | | using System.Runtime.InteropServices; |
| | 11 | | using Kestrun.Hosting; // For Base64UrlEncoder |
| | 12 | |
|
| | 13 | | namespace Kestrun.Jwt; |
| | 14 | |
|
| | 15 | | /// <summary> |
| | 16 | | /// Fluent utility to create any flavour of JWS/JWE in one line. |
| | 17 | | /// </summary> |
| | 18 | | /// <example> |
| | 19 | | /// // PowerShell usage: |
| | 20 | | /// $builder = [Kestrun.Security.JwtTokenBuilder]::New() |
| | 21 | | /// $token = $builder |
| | 22 | | /// .WithSubject('admin') |
| | 23 | | /// .WithIssuer('https://issuer') |
| | 24 | | /// .WithAudience('api') |
| | 25 | | /// .SignWithSecret('uZ6zDP3CGK3rktmVOXQk8A') # base64url |
| | 26 | | /// .EncryptWithCertificate($cert,'RSA-OAEP','A256GCM') |
| | 27 | | /// .Build() |
| | 28 | | /// Write-Output $token |
| | 29 | | /// </example> |
| | 30 | | public sealed class JwtTokenBuilder |
| | 31 | | { |
| | 32 | | // ───── Public fluent API ────────────────────────────────────────── |
| | 33 | | /// <summary> |
| | 34 | | /// Creates a new instance of <see cref="JwtTokenBuilder"/>. |
| | 35 | | /// </summary> |
| | 36 | | /// <returns>A new <see cref="JwtTokenBuilder"/> instance.</returns> |
| 29 | 37 | | public static JwtTokenBuilder New() => new(); |
| | 38 | |
|
| | 39 | | /// <summary> |
| | 40 | | /// Sets the issuer of the JWT token. |
| | 41 | | /// </summary> |
| | 42 | | /// <param name="issuer">The issuer to set.</param> |
| | 43 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 52 | 44 | | public JwtTokenBuilder WithIssuer(string issuer) { _issuer = issuer; return this; } |
| | 45 | | /// <summary> |
| | 46 | | /// Sets the audience of the JWT token. |
| | 47 | | /// </summary> |
| | 48 | | /// <param name="audience">The audience to set.</param> |
| | 49 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 52 | 50 | | public JwtTokenBuilder WithAudience(string audience) { _aud = audience; return this; } |
| | 51 | | /// <summary> |
| | 52 | | /// Sets the subject ('sub' claim) of the JWT token. |
| | 53 | | /// </summary> |
| | 54 | | /// <param name="sub">The subject to set.</param> |
| | 55 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 50 | 56 | | public JwtTokenBuilder WithSubject(string sub) { _claims.Add(new Claim(Microsoft.IdentityModel.JsonWebTokens.JwtRegi |
| | 57 | | /// <summary> |
| | 58 | | /// Adds a claim to the JWT token. |
| | 59 | | /// </summary> |
| | 60 | | /// <param name="type">The claim type.</param> |
| | 61 | | /// <param name="value">The claim value.</param> |
| | 62 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 12 | 63 | | public JwtTokenBuilder AddClaim(string type, string value) { _claims.Add(new Claim(type, value)); return this; } |
| | 64 | | /// <summary> |
| | 65 | | /// Sets the lifetime (validity period) of the JWT token. |
| | 66 | | /// </summary> |
| | 67 | | /// <param name="ttl">The time span for which the token is valid.</param> |
| | 68 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 14 | 69 | | public JwtTokenBuilder ValidFor(TimeSpan ttl) { _lifetime = ttl; return this; } |
| | 70 | | /// <summary> |
| | 71 | | /// Sets the 'not before' (nbf) claim for the JWT token. |
| | 72 | | /// </summary> |
| | 73 | | /// <param name="utc">The UTC date and time before which the token is not valid.</param> |
| | 74 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 2 | 75 | | public JwtTokenBuilder NotBefore(DateTime utc) { _nbf = utc; return this; } |
| | 76 | | /// <summary> |
| | 77 | | /// Adds a custom header to the JWT token. |
| | 78 | | /// </summary> |
| | 79 | | /// <param name="k">The header key.</param> |
| | 80 | | /// <param name="v">The header value.</param> |
| | 81 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 8 | 82 | | public JwtTokenBuilder AddHeader(string k, object v) { _header[k] = v; return this; } |
| | 83 | |
|
| | 84 | | /// <summary> |
| | 85 | | /// Gets the issuer of the JWT token. |
| | 86 | | /// </summary> |
| 20 | 87 | | public string Issuer => _issuer ?? string.Empty; |
| | 88 | | /// <summary> |
| | 89 | | /// Gets the audience of the JWT token. |
| | 90 | | /// </summary> |
| 20 | 91 | | public string Audience => _aud ?? string.Empty; |
| | 92 | | /// <summary> |
| | 93 | | /// Gets the algorithm used for signing the JWT token. |
| | 94 | | /// </summary> |
| 53 | 95 | | public string? Algorithm { get; private set; } |
| | 96 | |
|
| | 97 | | // ── pending-config “envelopes” (built later) ───────── |
| 56 | 98 | | private sealed record PendingSymmetricSign(string B64u, string Alg /*auto/HS256…*/); |
| 9 | 99 | | private sealed record PendingRsaSign(string Pem, string Alg); |
| 15 | 100 | | private sealed record PendingCertSign(X509Certificate2 Cert, string Alg); |
| | 101 | |
|
| 12 | 102 | | private sealed record PendingSymmetricEnc(string B64u, string KeyAlg, string EncAlg); |
| 4 | 103 | | private sealed record PendingRsaEnc(string Pem, string KeyAlg, string EncAlg); |
| 0 | 104 | | private sealed record PendingCertEnc(X509Certificate2 Cert, string KeyAlg, string EncAlg); |
| | 105 | |
|
| | 106 | | private object? _pendingSign; // will be one of the above |
| | 107 | | private object? _pendingEnc; |
| | 108 | | private SymmetricSecurityKey? _issuerSigningKey; |
| | 109 | |
|
| | 110 | | // ── signing helpers (store only) ───────────────────── |
| | 111 | | /// <summary> |
| | 112 | | /// Signs the JWT using a symmetric key provided as a Base64Url-encoded string. |
| | 113 | | /// </summary> |
| | 114 | | /// <param name="b64Url">The symmetric key as a Base64Url-encoded string.</param> |
| | 115 | | /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param> |
| | 116 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 117 | | public JwtTokenBuilder SignWithSecret( |
| | 118 | | string b64Url, |
| | 119 | | JwtAlgorithm alg = JwtAlgorithm.Auto) |
| | 120 | | { |
| 20 | 121 | | if (string.IsNullOrWhiteSpace(b64Url)) |
| | 122 | | { |
| 2 | 123 | | throw new ArgumentNullException(nameof(b64Url)); |
| | 124 | | } |
| | 125 | |
|
| | 126 | | // 1) Decode the incoming Base64Url to bytes |
| 18 | 127 | | var raw = Base64UrlEncoder.DecodeBytes(b64Url); |
| | 128 | |
|
| | 129 | | // 2) Create (and remember) the SymmetricSecurityKey |
| 18 | 130 | | var key = new SymmetricSecurityKey(raw) |
| 18 | 131 | | { |
| 18 | 132 | | KeyId = Guid.NewGuid().ToString("N") |
| 18 | 133 | | }; |
| 18 | 134 | | _issuerSigningKey = key; |
| | 135 | |
|
| | 136 | | // 3) Resolve "Auto" or map the enum to the exact JWS alg string |
| 18 | 137 | | var resolvedAlg = alg.ToJwtString(raw.Length); |
| | 138 | |
|
| | 139 | | // 4) Store the pending sign using the resolved algorithm |
| 18 | 140 | | _pendingSign = new PendingSymmetricSign(b64Url, resolvedAlg); |
| | 141 | |
|
| 18 | 142 | | return this; |
| | 143 | | } |
| | 144 | |
|
| | 145 | |
|
| | 146 | | /// <summary> |
| | 147 | | /// Creates a new token builder instance by cloning the current configuration. |
| | 148 | | /// </summary> |
| | 149 | | /// <returns>A new <see cref="JwtTokenBuilder"/> instance with the same configuration.</returns> |
| | 150 | | public JwtTokenBuilder CloneBuilder() |
| | 151 | | { |
| 2 | 152 | | var clone = (JwtTokenBuilder)MemberwiseClone(); |
| 2 | 153 | | clone._claims = [.. _claims]; |
| 2 | 154 | | return clone; |
| | 155 | | } |
| | 156 | |
|
| | 157 | | /// <summary> |
| | 158 | | /// Signs the JWT using a symmetric key provided as a hexadecimal string. |
| | 159 | | /// </summary> |
| | 160 | | /// <param name="hex">The symmetric key as a hexadecimal string.</param> |
| | 161 | | /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param> |
| | 162 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| 2 | 163 | | public JwtTokenBuilder SignWithSecretHex(string hex, JwtAlgorithm alg = JwtAlgorithm.Auto) => SignWithSecret(Base64U |
| | 164 | |
|
| | 165 | | /// <summary> |
| | 166 | | /// Signs the JWT using a symmetric key derived from the provided passphrase. |
| | 167 | | /// </summary> |
| | 168 | | /// <param name="passPhrase">The passphrase to use as the symmetric key.</param> |
| | 169 | | /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param> |
| | 170 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 171 | | public JwtTokenBuilder SignWithSecretPassphrase( |
| | 172 | | SecureString passPhrase, |
| | 173 | | JwtAlgorithm alg = JwtAlgorithm.Auto) |
| | 174 | | { |
| 1 | 175 | | ArgumentNullException.ThrowIfNull(passPhrase); |
| | 176 | |
|
| | 177 | | // Marshal to unmanaged Unicode (UTF-16) buffer |
| 1 | 178 | | var unicodePtr = Marshal.SecureStringToGlobalAllocUnicode(passPhrase); |
| | 179 | | try |
| | 180 | | { |
| 1 | 181 | | var charCount = passPhrase.Length; |
| 1 | 182 | | var unicodeBytes = new byte[charCount * sizeof(char)]; |
| | 183 | | // copy from unmanaged -> managed |
| 1 | 184 | | Marshal.Copy(unicodePtr, unicodeBytes, 0, unicodeBytes.Length); |
| | 185 | |
|
| | 186 | | // convert UTF-16 bytes directly to UTF-8 |
| 1 | 187 | | var utf8Bytes = Encoding.Convert(Encoding.Unicode, Encoding.UTF8, unicodeBytes); |
| | 188 | | // zero-out the intermediate Unicode bytes |
| 1 | 189 | | Array.Clear(unicodeBytes, 0, unicodeBytes.Length); |
| | 190 | |
|
| 1 | 191 | | var b64url = Base64UrlEncoder.Encode(utf8Bytes); |
| | 192 | | // zero-out the UTF-8 bytes too |
| 1 | 193 | | Array.Clear(utf8Bytes, 0, utf8Bytes.Length); |
| | 194 | |
|
| 1 | 195 | | return SignWithSecret(b64url, alg); |
| | 196 | | } |
| | 197 | | finally |
| | 198 | | { |
| | 199 | | // zero-free the unmanaged buffer |
| 1 | 200 | | Marshal.ZeroFreeGlobalAllocUnicode(unicodePtr); |
| 1 | 201 | | } |
| 1 | 202 | | } |
| | 203 | |
|
| | 204 | | // ── inside JwtTokenBuilder ───────────────────────────────────────── |
| | 205 | |
|
| | 206 | | /// <summary> |
| | 207 | | /// Signs the JWT using an RSA private key provided in PEM format. |
| | 208 | | /// </summary> |
| | 209 | | /// <param name="pemPath">The file path to the RSA private key in PEM format.</param> |
| | 210 | | /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param> |
| | 211 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 212 | | public JwtTokenBuilder SignWithRsaPem( |
| | 213 | | string pemPath, |
| | 214 | | JwtAlgorithm alg = JwtAlgorithm.Auto) |
| | 215 | | { |
| 3 | 216 | | var pem = File.ReadAllText(pemPath); |
| | 217 | |
|
| | 218 | | // Auto ⇒ default RS256; otherwise map enum to the exact string |
| 3 | 219 | | var resolvedAlg = alg == JwtAlgorithm.Auto |
| 3 | 220 | | ? SecurityAlgorithms.RsaSha256 |
| 3 | 221 | | : alg.ToJwtString(0); |
| | 222 | |
|
| 3 | 223 | | _pendingSign = new PendingRsaSign(pem, resolvedAlg); |
| 3 | 224 | | return this; |
| | 225 | | } |
| | 226 | |
|
| | 227 | | /// <summary> |
| | 228 | | /// Sign with an X.509 certificate (must have private key). |
| | 229 | | /// </summary> |
| | 230 | | public JwtTokenBuilder SignWithCertificate( |
| | 231 | | X509Certificate2 cert, |
| | 232 | | JwtAlgorithm alg = JwtAlgorithm.Auto) |
| | 233 | | { |
| 6 | 234 | | if (!cert.HasPrivateKey) |
| | 235 | | { |
| 1 | 236 | | throw new ArgumentException( |
| 1 | 237 | | "Certificate must contain a private key.", nameof(cert)); |
| | 238 | | } |
| | 239 | |
|
| | 240 | | // Auto ⇒ ES256 for ECDSA keys, RS256 for RSA keys |
| 5 | 241 | | var resolvedAlg = alg == JwtAlgorithm.Auto |
| 5 | 242 | | ? (cert.GetECDsaPublicKey() is not null |
| 5 | 243 | | ? SecurityAlgorithms.EcdsaSha256 |
| 5 | 244 | | : SecurityAlgorithms.RsaSha256) |
| 5 | 245 | | : alg.ToJwtString(0); |
| | 246 | |
|
| 5 | 247 | | _pendingSign = new PendingCertSign(cert, resolvedAlg); |
| 5 | 248 | | return this; |
| | 249 | | } |
| | 250 | |
|
| | 251 | |
|
| | 252 | |
|
| | 253 | | // ── encryption helpers (lazy) ─────────────────────────────────── |
| | 254 | |
|
| | 255 | | // 1️⃣ X.509 certificate (RSA or EC public key) |
| | 256 | | /// <summary> |
| | 257 | | /// Encrypts the JWT using the provided X.509 certificate. |
| | 258 | | /// </summary> |
| | 259 | | /// <param name="cert">The X.509 certificate to use for encryption.</param> |
| | 260 | | /// <param name="keyAlg">The key encryption algorithm (default is "RSA-OAEP").</param> |
| | 261 | | /// <param name="encAlg">The content encryption algorithm (default is "A256GCM").</param> |
| | 262 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 263 | | public JwtTokenBuilder EncryptWithCertificate( |
| | 264 | | X509Certificate2 cert, |
| | 265 | | string keyAlg = "RSA-OAEP", |
| | 266 | | string encAlg = "A256GCM") |
| | 267 | | { |
| 0 | 268 | | _pendingEnc = new PendingCertEnc(cert, keyAlg, encAlg); |
| 0 | 269 | | return this; |
| | 270 | | } |
| | 271 | |
|
| | 272 | | /// <summary> |
| | 273 | | /// Encrypts the JWT using a PEM-encoded RSA public key. |
| | 274 | | /// </summary> |
| | 275 | | /// <param name="pemPath">The file path to the PEM-encoded RSA public key.</param> |
| | 276 | | /// <param name="keyAlg">The key encryption algorithm (default is "RSA-OAEP").</param> |
| | 277 | | /// <param name="encAlg">The content encryption algorithm (default is "A256GCM").</param> |
| | 278 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 279 | | public JwtTokenBuilder EncryptWithPemPublic( |
| | 280 | | string pemPath, |
| | 281 | | string keyAlg = "RSA-OAEP", |
| | 282 | | string encAlg = "A256GCM") |
| | 283 | | { |
| 1 | 284 | | _pendingEnc = new PendingRsaEnc(File.ReadAllText(pemPath), keyAlg, encAlg); |
| 1 | 285 | | return this; |
| | 286 | | } |
| | 287 | |
|
| | 288 | | /// <summary> |
| | 289 | | /// Encrypts the JWT using a symmetric key provided as a hexadecimal string. |
| | 290 | | /// </summary> |
| | 291 | | /// <param name="hex">The symmetric key as a hexadecimal string.</param> |
| | 292 | | /// <param name="keyAlg">The key encryption algorithm (default is "dir").</param> |
| | 293 | | /// <param name="encAlg">The content encryption algorithm (default is "A256CBC-HS512").</param> |
| | 294 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 295 | | public JwtTokenBuilder EncryptWithSecretHex( |
| | 296 | | string hex, |
| | 297 | | string keyAlg = "dir", |
| 0 | 298 | | string encAlg = "A256CBC-HS512") => EncryptWithSecret(Convert.FromHexString(hex), keyAlg, encAlg); |
| | 299 | |
|
| | 300 | | /// <summary> |
| | 301 | | /// Encrypts the JWT using a symmetric key provided as a Base64Url-encoded string. |
| | 302 | | /// </summary> |
| | 303 | | /// <param name="b64Url">The symmetric key as a Base64Url-encoded string.</param> |
| | 304 | | /// <param name="keyAlg">The key encryption algorithm (default is "dir").</param> |
| | 305 | | /// <param name="encAlg">The content encryption algorithm (default is "A256CBC-HS512").</param> |
| | 306 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 307 | | public JwtTokenBuilder EncryptWithSecretB64( |
| | 308 | | string b64Url, |
| | 309 | | string keyAlg = "dir", |
| 0 | 310 | | string encAlg = "A256CBC-HS512") => EncryptWithSecret(Base64UrlEncoder.DecodeBytes(b64Url), keyAlg, encAlg); |
| | 311 | |
|
| | 312 | | /// <summary> |
| | 313 | | /// Encrypts the JWT using a symmetric key provided as a byte array. |
| | 314 | | /// </summary> |
| | 315 | | /// <param name="keyBytes">The symmetric key as a byte array.</param> |
| | 316 | | /// <param name="keyAlg">The key encryption algorithm (default is "dir").</param> |
| | 317 | | /// <param name="encAlg">The content encryption algorithm (default is "A256CBC-HS512").</param> |
| | 318 | | /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns> |
| | 319 | | public JwtTokenBuilder EncryptWithSecret( |
| | 320 | | byte[] keyBytes, |
| | 321 | | string keyAlg = "dir", |
| | 322 | | string encAlg = "A256CBC-HS512") |
| | 323 | | { |
| 3 | 324 | | var b64u = Base64UrlEncoder.Encode(keyBytes); |
| 3 | 325 | | _pendingEnc = new PendingSymmetricEnc(b64u, keyAlg, encAlg); |
| 3 | 326 | | return this; |
| | 327 | | } |
| | 328 | |
|
| | 329 | |
|
| | 330 | | // ───── Build the compact JWT ────────────────────────────────────── |
| | 331 | |
|
| | 332 | | /// <summary> |
| | 333 | | /// Builds the JWT token. |
| | 334 | | /// This method constructs the JWT token using the configured parameters and returns it as a compact string. |
| | 335 | | /// </summary> |
| | 336 | | /// <returns>The JWT token as a compact string.</returns> |
| | 337 | | /// <exception cref="InvalidOperationException">Thrown if no signing credentials are configured.</exception> |
| | 338 | | /// <remarks> |
| | 339 | | /// This method constructs the JWT token using the configured parameters and returns it as a compact string. |
| | 340 | | /// </remarks> |
| | 341 | | private string BuildToken() |
| | 342 | | { |
| 25 | 343 | | var handler = new JwtSecurityTokenHandler(); |
| | 344 | | // ── build creds lazily now ─────────────────────────── |
| 25 | 345 | | var signCreds = BuildSigningCredentials(out _issuerSigningKey) ?? throw new InvalidOperationException("No signin |
| 25 | 346 | | Algorithm = signCreds.Algorithm; |
| 25 | 347 | | var encCreds = BuildEncryptingCredentials(); |
| 24 | 348 | | if (_nbf < DateTime.UtcNow) |
| | 349 | | { |
| 24 | 350 | | _nbf = DateTime.UtcNow; |
| | 351 | | } |
| 24 | 352 | | var token = handler.CreateJwtSecurityToken( |
| 24 | 353 | | issuer: _issuer, |
| 24 | 354 | | audience: _aud, |
| 24 | 355 | | subject: new ClaimsIdentity(_claims), |
| 24 | 356 | | notBefore: _nbf, |
| 24 | 357 | | expires: _nbf.Add(_lifetime), |
| 24 | 358 | | issuedAt: DateTime.UtcNow, |
| 24 | 359 | | signingCredentials: signCreds, |
| 24 | 360 | | encryptingCredentials: encCreds); |
| | 361 | |
|
| | 362 | |
|
| 54 | 363 | | foreach (var kv in _header) |
| | 364 | | { |
| 4 | 365 | | token.Header[kv.Key] = kv.Value; |
| | 366 | | } |
| | 367 | |
|
| 23 | 368 | | return handler.WriteToken(token); |
| | 369 | | } |
| | 370 | |
|
| | 371 | | /// <summary> |
| | 372 | | /// Builds the JWT token. |
| | 373 | | /// This method constructs the JWT token using the configured parameters and returns it as a compact string. |
| | 374 | | /// </summary> |
| | 375 | | /// <param name="signingKey">The signing key used to sign the JWT.</param> |
| | 376 | | /// <returns>The JWT token as a compact string.</returns> |
| | 377 | | /// <exception cref="InvalidOperationException">Thrown if no signing credentials are configured.</exception> |
| | 378 | | /// <remarks> |
| | 379 | | /// This method constructs the JWT token using the configured parameters and returns it as a compact string. |
| | 380 | | /// </remarks> |
| | 381 | | private string BuildToken(out SymmetricSecurityKey? signingKey) |
| | 382 | | { |
| 25 | 383 | | var jwt = BuildToken(); // call the original Build() |
| 23 | 384 | | signingKey = _issuerSigningKey; // may be null for unsigned / RSA / cert |
| 23 | 385 | | return jwt; |
| | 386 | | } |
| | 387 | |
|
| | 388 | |
|
| | 389 | | /// <summary> |
| | 390 | | /// Builds the JWT token and returns a <see cref="JwtBuilderResult"/> containing the token, signing key, and validit |
| | 391 | | /// </summary> |
| | 392 | | /// <returns>A <see cref="JwtBuilderResult"/> containing the JWT token, signing key, and validity period.</returns> |
| | 393 | | public JwtBuilderResult Build() |
| | 394 | | { |
| | 395 | | // ① produce the raw token + signing key |
| 25 | 396 | | var token = BuildToken(out var key); |
| | 397 | | // ② Parse it immediately to pull out the valid-from / valid-to |
| 23 | 398 | | var handler = new JwtSecurityTokenHandler(); |
| 23 | 399 | | var jwtToken = handler.ReadJwtToken(token); |
| 23 | 400 | | var issuedAt = jwtToken.ValidFrom.ToUniversalTime(); |
| 23 | 401 | | var expires = jwtToken.ValidTo.ToUniversalTime(); |
| | 402 | | // ③ return the helper object |
| 23 | 403 | | return new JwtBuilderResult(token, key, this, issuedAt, expires); |
| | 404 | | } |
| | 405 | |
|
| | 406 | | // ────────── helpers that materialise creds ─────────── |
| | 407 | |
|
| | 408 | | /// <summary> |
| | 409 | | /// Builds the signing credentials. |
| | 410 | | /// This method constructs the signing credentials based on the pending signing configuration. |
| | 411 | | /// If no signing configuration is set, it returns null. |
| | 412 | | /// </summary> |
| | 413 | | /// <param name="key">The symmetric security key to use for signing.</param> |
| | 414 | | /// <returns>The signing credentials, or null if not configured.</returns> |
| | 415 | | /// <exception cref="InvalidOperationException">Thrown if no signing configuration is set.</exception> |
| | 416 | | /// <remarks> |
| | 417 | | /// This method constructs the signing credentials based on the pending signing configuration. |
| | 418 | | /// If no signing configuration is set, it returns null. |
| | 419 | | /// </remarks> |
| | 420 | | private SigningCredentials? BuildSigningCredentials(out SymmetricSecurityKey? key) |
| | 421 | | { |
| 27 | 422 | | key = null; |
| | 423 | |
|
| 27 | 424 | | return _pendingSign switch |
| 27 | 425 | | { |
| 19 | 426 | | PendingSymmetricSign ps => CreateHsCreds(ps, out key), |
| 3 | 427 | | PendingRsaSign pr => CreateRsaCreds(pr), |
| 5 | 428 | | PendingCertSign pc => CreateCertCreds(pc), |
| 0 | 429 | | _ => null |
| 27 | 430 | | }; |
| | 431 | | } |
| | 432 | |
|
| | 433 | | /// <summary> |
| | 434 | | /// Builds the signing credentials. |
| | 435 | | /// This method constructs the signing credentials based on the pending signing configuration. |
| | 436 | | /// If no signing configuration is set, it returns null. |
| | 437 | | /// </summary> |
| | 438 | | /// <param name="ps">The pending symmetric signing configuration.</param> |
| | 439 | | /// <param name="key">The symmetric security key to use for signing.</param> |
| | 440 | | /// <returns>The signing credentials, or null if not configured.</returns> |
| | 441 | | /// <exception cref="InvalidOperationException">Thrown if no signing configuration is set.</exception> |
| | 442 | | /// <remarks> |
| | 443 | | /// This method constructs the signing credentials based on the pending symmetric signing configuration. |
| | 444 | | /// If no signing configuration is set, it returns null. |
| | 445 | | /// </remarks> |
| | 446 | | private static SigningCredentials CreateHsCreds( |
| | 447 | | PendingSymmetricSign ps, |
| | 448 | | out SymmetricSecurityKey key) |
| | 449 | | { |
| | 450 | | // 1) decode the Base64Url secret |
| 19 | 451 | | var raw = Base64UrlEncoder.DecodeBytes(ps.B64u); |
| | 452 | |
|
| | 453 | | // 2) create the SymmetricSecurityKey (and record its KeyId) |
| 19 | 454 | | key = new SymmetricSecurityKey(raw) |
| 19 | 455 | | { |
| 19 | 456 | | KeyId = Guid.NewGuid().ToString("N") |
| 19 | 457 | | }; |
| | 458 | |
|
| | 459 | | // 3) ps.Alg is *already* the exact SecurityAlgorithms.* constant |
| 19 | 460 | | return new SigningCredentials(key, ps.Alg); |
| | 461 | | } |
| | 462 | |
|
| | 463 | | /// <summary> |
| | 464 | | /// Creates signing credentials for RSA using the provided PEM string. |
| | 465 | | /// This method imports the RSA key from the PEM string and returns the signing credentials. |
| | 466 | | /// </summary> |
| | 467 | | /// <param name="pr">The pending RSA signing configuration.</param> |
| | 468 | | /// <returns>The signing credentials for RSA.</returns> |
| | 469 | | /// <exception cref="InvalidOperationException">Thrown if the PEM string is invalid or cannot be imported.</exceptio |
| | 470 | | /// <remarks> |
| | 471 | | /// This method imports the RSA key from the PEM string and returns the signing credentials. |
| | 472 | | /// </remarks> |
| | 473 | | private static SigningCredentials CreateRsaCreds(PendingRsaSign pr) |
| | 474 | | { |
| 3 | 475 | | var rsa = RSA.Create(); |
| 3 | 476 | | rsa.ImportFromPem(pr.Pem); |
| 3 | 477 | | var key = new RsaSecurityKey(rsa) |
| 3 | 478 | | { |
| 3 | 479 | | KeyId = Guid.NewGuid().ToString("N") |
| 3 | 480 | | }; |
| 3 | 481 | | return new SigningCredentials(key, pr.Alg); |
| | 482 | | } |
| | 483 | | /// <summary> |
| | 484 | | /// Creates signing credentials for a certificate. |
| | 485 | | /// </summary> |
| | 486 | | /// <param name="pc">The pending certificate signing configuration.</param> |
| | 487 | | /// <returns>The signing credentials for the certificate.</returns> |
| | 488 | | /// <exception cref="InvalidOperationException">Thrown if the certificate does not have a private key.</exception> |
| | 489 | | /// <remarks> |
| | 490 | | /// This method creates signing credentials for a certificate using the provided certificate. |
| | 491 | | /// </remarks> |
| | 492 | | private static SigningCredentials CreateCertCreds(PendingCertSign pc) |
| | 493 | | { |
| 5 | 494 | | var cert = pc.Cert; |
| 5 | 495 | | var key = new X509SecurityKey(cert); // thumbprint becomes kid |
| 5 | 496 | | return new SigningCredentials(key, pc.Alg); |
| | 497 | | } |
| | 498 | | /// <summary> |
| | 499 | | /// Builds the encrypting credentials. |
| | 500 | | /// This method constructs the encrypting credentials based on the pending encryption configuration. |
| | 501 | | /// If no encryption configuration is set, it returns null. |
| | 502 | | /// </summary> |
| | 503 | | /// <returns>The encrypting credentials, or null if not set.</returns> |
| | 504 | | /// <remarks> |
| | 505 | | /// This method constructs the encrypting credentials based on the pending encryption configuration. |
| | 506 | | /// If no encryption configuration is set, it returns null. |
| | 507 | | /// </remarks> |
| | 508 | | private EncryptingCredentials? BuildEncryptingCredentials() |
| 27 | 509 | | => _pendingEnc switch |
| 27 | 510 | | { |
| 3 | 511 | | PendingSymmetricEnc se => new SymmetricEncrypt( |
| 3 | 512 | | se.B64u, se.KeyAlg, se.EncAlg).ToEncryptingCreds(), |
| 1 | 513 | | PendingRsaEnc re => new RsaEncrypt( |
| 1 | 514 | | re.Pem, re.KeyAlg, re.EncAlg).ToEncryptingCreds(), |
| 0 | 515 | | PendingCertEnc ce => new CertEncrypt( |
| 0 | 516 | | ce.Cert, ce.KeyAlg, ce.EncAlg).ToEncryptingCreds(), |
| 23 | 517 | | _ => null |
| 27 | 518 | | }; |
| | 519 | |
|
| | 520 | |
|
| | 521 | |
|
| | 522 | | // ───── Internals ────────────────────────────────────────────────── |
| | 523 | | /// <summary> |
| | 524 | | /// Gets the claims to be included in the JWT token. |
| | 525 | | /// </summary> |
| 29 | 526 | | private List<Claim> _claims = []; |
| | 527 | | /// <summary> |
| | 528 | | /// Gets the headers to be included in the JWT token. |
| | 529 | | /// </summary> |
| 29 | 530 | | private readonly Dictionary<string, object> _header = new(StringComparer.OrdinalIgnoreCase); |
| | 531 | | /// <summary> |
| | 532 | | /// Gets the not before (nbf) claim for the JWT token. |
| | 533 | | /// </summary> |
| 29 | 534 | | private DateTime _nbf = DateTime.UtcNow; |
| | 535 | | /// <summary> |
| | 536 | | /// Gets the lifetime of the JWT token. |
| | 537 | | /// </summary> |
| 29 | 538 | | private TimeSpan _lifetime = TimeSpan.FromHours(1); |
| | 539 | | private string? _issuer, _aud; |
| | 540 | |
|
| | 541 | |
|
| | 542 | | // ── Strategy interfaces & concrete configs ─────────────────────── |
| | 543 | | private interface ISignConfig { SigningCredentials ToSigningCreds(); } |
| | 544 | | private interface IEncConfig { EncryptingCredentials ToEncryptingCreds(); } |
| | 545 | |
|
| | 546 | | private static class Map |
| | 547 | | { |
| | 548 | | // maps short names (HS256, RSA-OAEP, …) to SecurityAlgorithms |
| 1 | 549 | | public static readonly IReadOnlyDictionary<string, string> Jws = new Dictionary<string, string>(StringComparer.O |
| 1 | 550 | | { |
| 1 | 551 | | ["HS256"] = SecurityAlgorithms.HmacSha256, |
| 1 | 552 | | ["HS384"] = SecurityAlgorithms.HmacSha384, |
| 1 | 553 | | ["HS512"] = SecurityAlgorithms.HmacSha512, |
| 1 | 554 | | ["RS256"] = SecurityAlgorithms.RsaSha256, |
| 1 | 555 | | ["RS384"] = SecurityAlgorithms.RsaSha384, |
| 1 | 556 | | ["RS512"] = SecurityAlgorithms.RsaSha512, |
| 1 | 557 | | ["PS256"] = SecurityAlgorithms.RsaSsaPssSha256, |
| 1 | 558 | | ["PS384"] = SecurityAlgorithms.RsaSsaPssSha384, |
| 1 | 559 | | ["PS512"] = SecurityAlgorithms.RsaSsaPssSha512, |
| 1 | 560 | | ["ES256"] = SecurityAlgorithms.EcdsaSha256, |
| 1 | 561 | | ["ES384"] = SecurityAlgorithms.EcdsaSha384, |
| 1 | 562 | | ["ES512"] = SecurityAlgorithms.EcdsaSha512 |
| 1 | 563 | | }; |
| 1 | 564 | | public static readonly IReadOnlyDictionary<string, string> KeyAlg = new Dictionary<string, string>(StringCompare |
| 1 | 565 | | { |
| 1 | 566 | | ["RSA-OAEP"] = SecurityAlgorithms.RsaOAEP, |
| 1 | 567 | | ["RSA-OAEP-256"] = "RSA-OAEP-256", |
| 1 | 568 | | ["RSA-OAEP-384"] = "RSA-OAEP-384", |
| 1 | 569 | | ["RSA-OAEP-512"] = "RSA-OAEP-512", |
| 1 | 570 | | ["RSA1_5"] = SecurityAlgorithms.RsaPKCS1, |
| 1 | 571 | | ["A128KW"] = SecurityAlgorithms.Aes128KW, |
| 1 | 572 | | ["A192KW"] = SecurityAlgorithms.Aes192KW, |
| 1 | 573 | | ["A256KW"] = SecurityAlgorithms.Aes256KW, |
| 1 | 574 | | ["ECDH-ES"] = SecurityAlgorithms.EcdhEs, |
| 1 | 575 | | ["ECDH-ESA128KW"] = SecurityAlgorithms.EcdhEsA128kw, |
| 1 | 576 | | ["ECDH-ESA192KW"] = SecurityAlgorithms.EcdhEsA192kw, |
| 1 | 577 | | ["ECDH-ESA256KW"] = SecurityAlgorithms.EcdhEsA256kw, |
| 1 | 578 | | ["dir"] = "dir" |
| 1 | 579 | | }; |
| 1 | 580 | | public static readonly IReadOnlyDictionary<string, string> EncAlg = new Dictionary<string, string>(StringCompare |
| 1 | 581 | | { |
| 1 | 582 | | ["A128GCM"] = SecurityAlgorithms.Aes128Gcm, |
| 1 | 583 | | ["A192GCM"] = SecurityAlgorithms.Aes192Gcm, |
| 1 | 584 | | ["A256GCM"] = SecurityAlgorithms.Aes256Gcm, |
| 1 | 585 | | ["A128CBC-HS256"] = SecurityAlgorithms.Aes128CbcHmacSha256, |
| 1 | 586 | | ["A192CBC-HS384"] = SecurityAlgorithms.Aes192CbcHmacSha384, |
| 1 | 587 | | ["A256CBC-HS512"] = SecurityAlgorithms.Aes256CbcHmacSha512 |
| 1 | 588 | | }; |
| | 589 | | } |
| | 590 | |
|
| | 591 | | // ── Signing configs ─────────────────────────────────────────────── |
| 0 | 592 | | private sealed record SymmetricSign( |
| 0 | 593 | | SecurityKey Key, string ResolvedAlg) : ISignConfig |
| | 594 | | { |
| | 595 | | public SigningCredentials ToSigningCreds() |
| 0 | 596 | | => new(Key, ResolvedAlg); |
| | 597 | | } |
| | 598 | |
|
| 0 | 599 | | private sealed record RsaSign(string Pem, string Alg) : ISignConfig |
| | 600 | | { |
| | 601 | | public SigningCredentials ToSigningCreds() |
| | 602 | | { |
| 0 | 603 | | var rsa = RSA.Create(); rsa.ImportFromPem(Pem); |
| 0 | 604 | | var key = new RsaSecurityKey(rsa); |
| 0 | 605 | | var algo = Alg.Equals("auto", StringComparison.OrdinalIgnoreCase) ? Map.Jws["RS256"] : Map.Jws[Alg]; |
| 0 | 606 | | return new SigningCredentials(key, algo); |
| | 607 | | } |
| | 608 | | } |
| | 609 | |
|
| | 610 | |
|
| 0 | 611 | | private sealed record CertSign(X509Certificate2 Cert, string Alg) : ISignConfig |
| | 612 | | { |
| | 613 | | public SigningCredentials ToSigningCreds() |
| | 614 | | { |
| 0 | 615 | | if (!Cert.HasPrivateKey) |
| | 616 | | { |
| 0 | 617 | | throw new ArgumentException("Certificate must contain a private key."); |
| | 618 | | } |
| | 619 | |
|
| 0 | 620 | | var key = new X509SecurityKey(Cert); |
| | 621 | |
|
| | 622 | | // Pick default alg if caller passed "auto" |
| | 623 | | string resolvedAlg; |
| 0 | 624 | | if (!Alg.Equals("auto", StringComparison.OrdinalIgnoreCase)) |
| | 625 | | { |
| 0 | 626 | | resolvedAlg = Map.Jws[Alg]; |
| | 627 | | } |
| | 628 | | else |
| | 629 | | { |
| 0 | 630 | | if (Cert.GetECDsaPublicKey() is not null) |
| | 631 | | { |
| 0 | 632 | | resolvedAlg = Map.Jws["ES256"]; // ECDSA → ES256 by default |
| | 633 | | } |
| 0 | 634 | | else if (Cert.GetRSAPublicKey() is not null) |
| | 635 | | { |
| 0 | 636 | | resolvedAlg = Map.Jws["RS256"]; // RSA → RS256 by default |
| | 637 | | } |
| | 638 | | else |
| | 639 | | { |
| 0 | 640 | | var keyType = "unknown"; |
| 0 | 641 | | if (Cert.PublicKey != null && Cert.PublicKey.EncodedKeyValue != null && Cert.PublicKey.EncodedKeyVal |
| | 642 | | { |
| 0 | 643 | | keyType = Cert.PublicKey.EncodedKeyValue.Oid.FriendlyName ?? "unknown"; |
| | 644 | | } |
| | 645 | |
|
| 0 | 646 | | throw new NotSupportedException( |
| 0 | 647 | | $"Unsupported key type: {keyType}"); |
| | 648 | | } |
| | 649 | | } |
| | 650 | |
|
| 0 | 651 | | return new SigningCredentials(key, resolvedAlg); |
| | 652 | | } |
| | 653 | | } |
| | 654 | |
|
| | 655 | | // ── Encryption configs ──────────────────────────────────────────── |
| 27 | 656 | | private abstract record BaseEnc(string KeyAlg, string EncAlg) : IEncConfig |
| | 657 | | { |
| 1 | 658 | | protected string KeyAlgMapped => Map.KeyAlg[KeyAlg]; |
| 1 | 659 | | protected string EncAlgMapped => Map.EncAlg[EncAlg]; |
| | 660 | | public abstract EncryptingCredentials ToEncryptingCreds(); |
| | 661 | | } |
| | 662 | |
|
| 0 | 663 | | private sealed record CertEncrypt(X509Certificate2 Cert, string KeyAlg, string EncAlg) : BaseEnc(KeyAlg, EncAlg) |
| | 664 | | { |
| | 665 | | public override EncryptingCredentials ToEncryptingCreds() |
| | 666 | | { |
| 0 | 667 | | var key = new X509SecurityKey(Cert); |
| 0 | 668 | | return new EncryptingCredentials(key, KeyAlgMapped, EncAlgMapped); |
| | 669 | | } |
| | 670 | | } |
| | 671 | |
|
| 2 | 672 | | private sealed record RsaEncrypt(string Pem, string KeyAlg, string EncAlg) : BaseEnc(KeyAlg, EncAlg) |
| | 673 | | { |
| | 674 | | public override EncryptingCredentials ToEncryptingCreds() |
| | 675 | | { |
| 2 | 676 | | var rsa = RSA.Create(); rsa.ImportFromPem(Pem); |
| 1 | 677 | | var key = new RsaSecurityKey(rsa); |
| 1 | 678 | | return new EncryptingCredentials(key, KeyAlgMapped, EncAlgMapped); |
| | 679 | | } |
| | 680 | | } |
| | 681 | |
|
| | 682 | | private sealed record SymmetricEncrypt( |
| 6 | 683 | | string B64, |
| | 684 | | string KeyAlg, |
| 3 | 685 | | string EncAlg) : BaseEnc(KeyAlg, EncAlg) |
| | 686 | | { |
| | 687 | | public override EncryptingCredentials ToEncryptingCreds() |
| | 688 | | { |
| 3 | 689 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 690 | | { |
| 3 | 691 | | Log.Debug( |
| 3 | 692 | | "Encrypting with {KeyAlg} and {EncAlg} ({Bits} bits)", |
| 3 | 693 | | KeyAlg, EncAlg, Base64UrlEncoder.DecodeBytes(B64).Length * 8); |
| | 694 | | } |
| | 695 | | // ────────── shared-secret → SymmetricSecurityKey ────────── |
| 3 | 696 | | if (!Map.KeyAlg.ContainsKey(KeyAlg)) |
| | 697 | | { |
| 0 | 698 | | throw new ArgumentException($"Unknown key algorithm: {KeyAlg}"); |
| | 699 | | } |
| | 700 | |
|
| 3 | 701 | | var key = new SymmetricSecurityKey(Base64UrlEncoder.DecodeBytes(B64)); |
| 3 | 702 | | var bits = key.KeySize; // 128 / 192 / 256 / 384 / 512 … |
| | 703 | |
|
| | 704 | | // ────────── auto-pick encAlg for 'dir' default case ─────── |
| 3 | 705 | | var encEff = EncAlg; |
| | 706 | |
|
| 3 | 707 | | if (KeyAlg.Equals("dir", StringComparison.OrdinalIgnoreCase) && |
| 3 | 708 | | EncAlg.Equals("A256CBC-HS512", StringComparison.OrdinalIgnoreCase)) |
| | 709 | | { |
| 0 | 710 | | encEff = bits switch |
| 0 | 711 | | { |
| 0 | 712 | | 128 => "A128GCM", |
| 0 | 713 | | 192 => "A192GCM", |
| 0 | 714 | | 256 => "A256GCM", |
| 0 | 715 | | 384 => "A192CBC-HS384", |
| 0 | 716 | | 512 => "A256CBC-HS512", |
| 0 | 717 | | _ => throw new ArgumentException( |
| 0 | 718 | | $"Unsupported key size {bits} bits for direct encryption.") |
| 0 | 719 | | }; |
| | 720 | | } |
| | 721 | |
|
| | 722 | | // ────────── hard validation (caller may specify any enc) ── |
| | 723 | | static void Require(int actualBits, int requiredBits, string alg) |
| | 724 | | { |
| 3 | 725 | | _ = actualBits == requiredBits |
| 3 | 726 | | ? true |
| 3 | 727 | | : throw new ArgumentException($"{alg} requires a {requiredBits}-bit key."); |
| 2 | 728 | | } |
| | 729 | |
|
| 3 | 730 | | switch (encEff.ToUpperInvariant()) |
| | 731 | | { |
| 2 | 732 | | case "A128GCM": Require(bits, 128, encEff); break; |
| 0 | 733 | | case "A192GCM": Require(bits, 192, encEff); break; |
| 1 | 734 | | case "A256GCM": Require(bits, 256, encEff); break; |
| 2 | 735 | | case "A128CBC-HS256": Require(bits, 256, encEff); break; |
| 0 | 736 | | case "A192CBC-HS384": Require(bits, 384, encEff); break; |
| 0 | 737 | | case "A256CBC-HS512": Require(bits, 512, encEff); break; |
| | 738 | | default: |
| 0 | 739 | | throw new ArgumentException($"Unknown or unsupported enc algorithm: {encEff}"); |
| | 740 | | } |
| 2 | 741 | | if (Log.IsEnabled(LogEventLevel.Debug)) |
| | 742 | | { |
| 2 | 743 | | Log.Debug( |
| 2 | 744 | | "Encrypting with {KeyAlg} and {EncAlg} ({Bits} bits)", |
| 2 | 745 | | KeyAlg, encEff, bits); |
| | 746 | | } |
| | 747 | | // ────────── build EncryptingCredentials ─────────────────── |
| 2 | 748 | | return new EncryptingCredentials( |
| 2 | 749 | | key, |
| 2 | 750 | | Map.KeyAlg[KeyAlg.ToUpperInvariant()], // 'dir', 'A256KW', … |
| 2 | 751 | | Map.EncAlg[encEff.ToUpperInvariant()]); // validated / auto-picked enc |
| | 752 | | } |
| | 753 | | } |
| | 754 | |
|
| | 755 | | /// <summary> |
| | 756 | | /// Renews a JWT token from the current request context, optionally extending its lifetime. |
| | 757 | | /// </summary> |
| | 758 | | /// <param name="ctx">The Kestrun context containing the request and authorization header.</param> |
| | 759 | | /// <param name="lifetime">The new lifetime for the renewed token. If null, uses the builder's default lifetime.</pa |
| | 760 | |
|
| | 761 | | /// <returns>The renewed JWT token as a compact string.</returns> |
| | 762 | | /// <exception cref="UnauthorizedAccessException">Thrown if no Bearer token is provided in the request.</exception> |
| | 763 | | public string RenewJwt( |
| | 764 | | KestrunContext ctx, |
| | 765 | | TimeSpan? lifetime = null) |
| | 766 | | { |
| 2 | 767 | | if (ctx.Request.Authorization == null || (!ctx.Request.Authorization?.StartsWith("Bearer ") ?? true)) |
| | 768 | | { |
| 1 | 769 | | return string.Empty; |
| | 770 | | } |
| 1 | 771 | | var authHeader = ctx.Request.Authorization; |
| 1 | 772 | | var strToken = authHeader != null ? authHeader["Bearer ".Length..].Trim() : throw new UnauthorizedAccessExceptio |
| 1 | 773 | | return RenewJwt(jwt: strToken, lifetime: lifetime); |
| | 774 | | } |
| | 775 | |
|
| | 776 | | /// <summary> |
| | 777 | | /// Extends the validity period of an existing JWT token by creating a new token with updated lifetime. |
| | 778 | | /// </summary> |
| | 779 | | /// <param name="jwt">The original JWT token to extend.</param> |
| | 780 | | /// <param name="lifetime">The new lifetime for the extended token. If null, uses the builder's default lifetime.</p |
| | 781 | | /// <returns>The extended JWT token as a compact string.</returns> |
| | 782 | | public string RenewJwt( |
| | 783 | | string jwt, |
| | 784 | | TimeSpan? lifetime = null) |
| | 785 | | { |
| 2 | 786 | | var handler = new JwtSecurityTokenHandler(); |
| | 787 | |
|
| | 788 | | // Read raw token (no mapping, no validation) |
| 2 | 789 | | var old = handler.ReadJwtToken(jwt); |
| 2 | 790 | | var _builder = CloneBuilder(); |
| | 791 | | // Copy all non-time claims |
| 2 | 792 | | var reserved = new HashSet<string>(StringComparer.OrdinalIgnoreCase) |
| 2 | 793 | | { "exp", "nbf", "iat" }; |
| | 794 | |
|
| 15 | 795 | | var claims = old.Claims.Where(c => !reserved.Contains(c.Type)).ToList(); |
| | 796 | |
|
| | 797 | | // If you rely on "sub", make sure it’s there (some libs put it into NameIdentifier) |
| 4 | 798 | | if (!claims.Any(c => c.Type == JwtRegisteredClaimNames.Sub)) |
| | 799 | | { |
| 0 | 800 | | var sub = old.Claims.FirstOrDefault(c => |
| 0 | 801 | | c.Type is JwtRegisteredClaimNames.Sub or |
| 0 | 802 | | ClaimTypes.NameIdentifier)?.Value; |
| 0 | 803 | | if (!string.IsNullOrEmpty(sub)) |
| | 804 | | { |
| 0 | 805 | | claims.Add(new Claim(JwtRegisteredClaimNames.Sub, sub)); |
| | 806 | | } |
| | 807 | | } |
| | 808 | |
|
| 2 | 809 | | var signCreds = BuildSigningCredentials(out _issuerSigningKey) ?? throw new InvalidOperationException("No signin |
| 2 | 810 | | Algorithm = signCreds.Algorithm; |
| 2 | 811 | | var encCreds = BuildEncryptingCredentials(); |
| | 812 | |
|
| | 813 | | // Keep the same kid if present by setting it on the signing key |
| | 814 | | // signing.Key.KeyId = old.Header.Kid; // uncomment if you must mirror the old 'kid' |
| 2 | 815 | | if (_nbf < DateTime.UtcNow) |
| | 816 | | { |
| 2 | 817 | | _nbf = DateTime.UtcNow; |
| | 818 | | } |
| 2 | 819 | | if (lifetime is null) |
| | 820 | | { |
| 0 | 821 | | lifetime = _lifetime; |
| | 822 | | } |
| 2 | 823 | | else if (lifetime < TimeSpan.Zero) |
| | 824 | | { |
| 0 | 825 | | throw new ArgumentOutOfRangeException(nameof(lifetime), "Lifetime must be a positive TimeSpan."); |
| | 826 | | } |
| 2 | 827 | | var token = handler.CreateJwtSecurityToken( |
| 2 | 828 | | issuer: _issuer, |
| 2 | 829 | | audience: _aud, |
| 2 | 830 | | subject: new ClaimsIdentity(claims), |
| 2 | 831 | | notBefore: _nbf, |
| 2 | 832 | | expires: _nbf.Add((TimeSpan)lifetime), |
| 2 | 833 | | issuedAt: DateTime.UtcNow, |
| 2 | 834 | | signingCredentials: signCreds, |
| 2 | 835 | | encryptingCredentials: encCreds); |
| | 836 | |
|
| 4 | 837 | | foreach (var kv in _header) |
| | 838 | | { |
| 0 | 839 | | token.Header[kv.Key] = kv.Value; |
| | 840 | | } |
| | 841 | |
|
| 2 | 842 | | return handler.WriteToken(token); |
| | 843 | | } |
| | 844 | | /* |
| | 845 | | public JwtTokenPackage BuildPackage() |
| | 846 | | { |
| | 847 | | string jwt = BuildToken(out var key); // your existing overload |
| | 848 | |
|
| | 849 | | var tvp = new TokenValidationParameters |
| | 850 | | { |
| | 851 | | ValidateIssuer = true, |
| | 852 | | ValidIssuer = _issuer, // private fields in builder |
| | 853 | | ValidateAudience = true, |
| | 854 | | ValidAudience = _aud, |
| | 855 | | ValidateLifetime = true, |
| | 856 | | ClockSkew = TimeSpan.FromMinutes(1), |
| | 857 | |
|
| | 858 | | RequireSignedTokens = key is not null, |
| | 859 | | ValidateIssuerSigningKey = key is not null, |
| | 860 | | IssuerSigningKey = key, |
| | 861 | | ValidAlgorithms = key is not null |
| | 862 | | ? new[] { SecurityAlgorithms.HmacSha256 } |
| | 863 | | : Array.Empty<string>() |
| | 864 | | }; |
| | 865 | |
|
| | 866 | | return new JwtTokenPackage(jwt, key, tvp); |
| | 867 | | } |
| | 868 | | */ |
| | 869 | | } |