< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Jwt.JwtTokenBuilder
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Jwt/JwtTokenBuilder.cs
Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
66%
Covered lines: 232
Uncovered lines: 118
Coverable lines: 350
Total lines: 1032
Line coverage: 66.2%
Branch coverage
38%
Covered branches: 59
Total branches: 152
Branch coverage: 38.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 14:53:17 Line coverage: 80% (232/290) Branch coverage: 50% (58/116) Total lines: 869 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743111/19/2025 - 02:25:56 Line coverage: 66.2% (232/350) Branch coverage: 38.8% (59/152) Total lines: 1032 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d 08/26/2025 - 14:53:17 Line coverage: 80% (232/290) Branch coverage: 50% (58/116) Total lines: 869 Tag: Kestrun/Kestrun@78d1e497d8ba989d121b57aa39aa3c6b22de743111/19/2025 - 02:25:56 Line coverage: 66.2% (232/350) Branch coverage: 38.8% (59/152) Total lines: 1032 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
New()100%11100%
WithIssuer(...)100%11100%
WithAudience(...)100%11100%
WithSubject(...)100%11100%
AddClaim(...)100%11100%
ValidFor(...)100%11100%
NotBefore(...)100%11100%
AddHeader(...)100%11100%
get_Issuer()100%22100%
get_Audience()100%22100%
get_Algorithm()100%11100%
get_B64u()100%11100%
get_Pem()100%11100%
get_Cert()100%11100%
get_Jwk()100%210%
get_B64u()100%11100%
get_Pem()100%11100%
get_Cert()100%210%
get_JwkJson()100%210%
SignWithSecret(...)100%22100%
CloneBuilder()100%11100%
SignWithSecretHex(...)100%11100%
SignWithSecretPassphrase(...)100%11100%
SignWithRsaPem(...)50%22100%
SignWithCertificate(...)83.33%66100%
SignWithJwkJson(...)0%272160%
EncryptWithCertificate(...)100%210%
EncryptWithPemPublic(...)100%11100%
EncryptWithSecretHex(...)100%210%
EncryptWithSecretB64(...)100%210%
EncryptWithSecret(...)100%11100%
EncryptWithJwkJson(...)0%620%
EncryptWithJwkPath(...)0%620%
BuildToken()83.33%66100%
BuildToken(...)100%11100%
Build()100%11100%
BuildSigningCredentials(...)62.5%9877.77%
CreateHsCreds(...)100%11100%
CreateRsaCreds(...)100%11100%
CreateCertCreds(...)100%11100%
CreateJwkCreds(...)0%156120%
BuildEncryptingCredentials()75%10866.66%
.ctor()100%11100%
.cctor()100%11100%
.ctor(...)100%210%
get_Key()100%210%
ToSigningCreds()100%210%
get_Pem()100%210%
ToSigningCreds()0%620%
get_Cert()100%210%
ToSigningCreds()0%272160%
get_KeyAlg()100%11100%
get_KeyAlgMapped()100%11100%
get_EncAlgMapped()100%11100%
get_Cert()100%210%
ToEncryptingCreds()100%210%
get_Pem()100%11100%
ToEncryptingCreds()100%11100%
get_JwkJson()100%210%
ToEncryptingCreds()100%210%
get_B64()100%11100%
.ctor(...)100%11100%
ToEncryptingCreds()41.17%1113459.45%
Require()100%22100%
RenewJwt(...)75%88100%
RenewJwt(...)40.9%292275.75%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Jwt/JwtTokenBuilder.cs

#LineLine coverage
 1using System.IdentityModel.Tokens.Jwt;
 2using System.Security.Claims;
 3using System.Security.Cryptography;
 4using System.Security.Cryptography.X509Certificates;
 5using Microsoft.IdentityModel.Tokens;
 6using Serilog;
 7using Serilog.Events;
 8using System.Text;
 9using System.Security;
 10using System.Runtime.InteropServices;
 11using Kestrun.Models;  // For Base64UrlEncoder
 12
 13namespace 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>
 30public 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>
 2937    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>
 5244    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>
 5250    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>
 5056    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>
 1263    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>
 1469    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>
 275    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>
 882    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>
 2087    public string Issuer => _issuer ?? string.Empty;
 88    /// <summary>
 89    /// Gets the audience of the JWT token.
 90    /// </summary>
 2091    public string Audience => _aud ?? string.Empty;
 92    /// <summary>
 93    /// Gets the algorithm used for signing the JWT token.
 94    /// </summary>
 5395    public string? Algorithm { get; private set; }
 96
 97    // ── pending-config “envelopes” (built later) ─────────
 5698    private sealed record PendingSymmetricSign(string B64u, string Alg /*auto/HS256…*/);
 999    private sealed record PendingRsaSign(string Pem, string Alg);
 15100    private sealed record PendingCertSign(X509Certificate2 Cert, string Alg);
 0101    private sealed record PendingJwkSign(JsonWebKey Jwk, string Alg);
 102
 12103    private sealed record PendingSymmetricEnc(string B64u, string KeyAlg, string EncAlg);
 4104    private sealed record PendingRsaEnc(string Pem, string KeyAlg, string EncAlg);
 0105    private sealed record PendingCertEnc(X509Certificate2 Cert, string KeyAlg, string EncAlg);
 0106    private sealed record PendingJwkEnc(string JwkJson, string KeyAlg, string EncAlg);
 107
 108    private object? _pendingSign;     // will be one of the above
 109    private object? _pendingEnc;
 110    private SymmetricSecurityKey? _issuerSigningKey;
 111
 112    // ── signing helpers (store only) ─────────────────────
 113    /// <summary>
 114    /// Signs the JWT using a symmetric key provided as a Base64Url-encoded string.
 115    /// </summary>
 116    /// <param name="b64Url">The symmetric key as a Base64Url-encoded string.</param>
 117    /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param>
 118    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 119    public JwtTokenBuilder SignWithSecret(
 120       string b64Url,
 121       JwtAlgorithm alg = JwtAlgorithm.Auto)
 122    {
 20123        if (string.IsNullOrWhiteSpace(b64Url))
 124        {
 2125            throw new ArgumentNullException(nameof(b64Url));
 126        }
 127
 128        // 1) Decode the incoming Base64Url to bytes
 18129        var raw = Base64UrlEncoder.DecodeBytes(b64Url);
 130
 131        // 2) Create (and remember) the SymmetricSecurityKey
 18132        var key = new SymmetricSecurityKey(raw)
 18133        {
 18134            KeyId = Guid.NewGuid().ToString("N")
 18135        };
 18136        _issuerSigningKey = key;
 137
 138        // 3) Resolve "Auto" or map the enum to the exact JWS alg string
 18139        var resolvedAlg = alg.ToJwtString(raw.Length);
 140
 141        // 4) Store the pending sign using the resolved algorithm
 18142        _pendingSign = new PendingSymmetricSign(b64Url, resolvedAlg);
 143
 18144        return this;
 145    }
 146
 147
 148    /// <summary>
 149    /// Creates a new token builder instance by cloning the current configuration.
 150    /// </summary>
 151    /// <returns>A new <see cref="JwtTokenBuilder"/> instance with the same configuration.</returns>
 152    public JwtTokenBuilder CloneBuilder()
 153    {
 2154        var clone = (JwtTokenBuilder)MemberwiseClone();
 2155        clone._claims = [.. _claims];
 2156        return clone;
 157    }
 158
 159    /// <summary>
 160    /// Signs the JWT using a symmetric key provided as a hexadecimal string.
 161    /// </summary>
 162    /// <param name="hex">The symmetric key as a hexadecimal string.</param>
 163    /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param>
 164    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 2165    public JwtTokenBuilder SignWithSecretHex(string hex, JwtAlgorithm alg = JwtAlgorithm.Auto) => SignWithSecret(Base64U
 166
 167    /// <summary>
 168    /// Signs the JWT using a symmetric key derived from the provided passphrase.
 169    /// </summary>
 170    /// <param name="passPhrase">The passphrase to use as the symmetric key.</param>
 171    /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param>
 172    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 173    public JwtTokenBuilder SignWithSecretPassphrase(
 174       SecureString passPhrase,
 175       JwtAlgorithm alg = JwtAlgorithm.Auto)
 176    {
 1177        ArgumentNullException.ThrowIfNull(passPhrase);
 178
 179        // Marshal to unmanaged Unicode (UTF-16) buffer
 1180        var unicodePtr = Marshal.SecureStringToGlobalAllocUnicode(passPhrase);
 181        try
 182        {
 1183            var charCount = passPhrase.Length;
 1184            var unicodeBytes = new byte[charCount * sizeof(char)];
 185            // copy from unmanaged -> managed
 1186            Marshal.Copy(unicodePtr, unicodeBytes, 0, unicodeBytes.Length);
 187
 188            // convert UTF-16 bytes directly to UTF-8
 1189            var utf8Bytes = Encoding.Convert(Encoding.Unicode, Encoding.UTF8, unicodeBytes);
 190            // zero-out the intermediate Unicode bytes
 1191            Array.Clear(unicodeBytes, 0, unicodeBytes.Length);
 192
 1193            var b64url = Base64UrlEncoder.Encode(utf8Bytes);
 194            // zero-out the UTF-8 bytes too
 1195            Array.Clear(utf8Bytes, 0, utf8Bytes.Length);
 196
 1197            return SignWithSecret(b64url, alg);
 198        }
 199        finally
 200        {
 201            // zero-free the unmanaged buffer
 1202            Marshal.ZeroFreeGlobalAllocUnicode(unicodePtr);
 1203        }
 1204    }
 205
 206    // ── inside JwtTokenBuilder ─────────────────────────────────────────
 207
 208    /// <summary>
 209    /// Signs the JWT using an RSA private key provided in PEM format.
 210    /// </summary>
 211    /// <param name="pemPath">The file path to the RSA private key in PEM format.</param>
 212    /// <param name="alg">The JWT algorithm to use for signing (default is Auto).</param>
 213    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 214    public JwtTokenBuilder SignWithRsaPem(
 215        string pemPath,
 216        JwtAlgorithm alg = JwtAlgorithm.Auto)
 217    {
 3218        var pem = File.ReadAllText(pemPath);
 219
 220        // Auto ⇒ default RS256; otherwise map enum to the exact string
 3221        var resolvedAlg = alg == JwtAlgorithm.Auto
 3222            ? SecurityAlgorithms.RsaSha256
 3223            : alg.ToJwtString(0);
 224
 3225        _pendingSign = new PendingRsaSign(pem, resolvedAlg);
 3226        return this;
 227    }
 228
 229    /// <summary>
 230    /// Sign with an X.509 certificate (must have private key).
 231    /// </summary>
 232    public JwtTokenBuilder SignWithCertificate(
 233        X509Certificate2 cert,
 234        JwtAlgorithm alg = JwtAlgorithm.Auto)
 235    {
 6236        if (!cert.HasPrivateKey)
 237        {
 1238            throw new ArgumentException(
 1239                "Certificate must contain a private key.", nameof(cert));
 240        }
 241
 242        // Auto ⇒ ES256 for ECDSA keys, RS256 for RSA keys
 5243        var resolvedAlg = alg == JwtAlgorithm.Auto
 5244            ? (cert.GetECDsaPublicKey() is not null
 5245                ? SecurityAlgorithms.EcdsaSha256
 5246                : SecurityAlgorithms.RsaSha256)
 5247            : alg.ToJwtString(0);
 248
 5249        _pendingSign = new PendingCertSign(cert, resolvedAlg);
 5250        return this;
 251    }
 252
 253    /// <summary>
 254    /// Signs the JWT using a JWK JSON string (RSA / EC / oct).
 255    /// </summary>
 256    /// <param name="jwkJson">The JWK JSON string.</param>
 257    /// <param name="alg">
 258    /// The algorithm to use. If <see cref="JwtAlgorithm.Auto"/>, a default is chosen
 259    /// based on JWK key type (RS256 for RSA, ES256 for EC, HS256 for symmetric).
 260    /// </param>
 261    public JwtTokenBuilder SignWithJwkJson(
 262        string jwkJson,
 263        JwtAlgorithm alg = JwtAlgorithm.Auto)
 264    {
 0265        if (string.IsNullOrWhiteSpace(jwkJson))
 266        {
 0267            throw new ArgumentNullException(nameof(jwkJson));
 268        }
 269
 0270        var jwk = new JsonWebKey(jwkJson);
 271
 272        // Determine algorithm
 273        string resolvedAlg;
 0274        resolvedAlg = (alg == JwtAlgorithm.Auto) ?
 0275          jwk.Kty switch
 0276          {
 0277              "RSA" => SecurityAlgorithms.RsaSha256,
 0278              "EC" => SecurityAlgorithms.EcdsaSha256,
 0279              "oct" => SecurityAlgorithms.HmacSha256,
 0280              _ => throw new NotSupportedException(
 0281                         $"Unsupported JWK key type '{jwk.Kty}'.")
 0282          }
 0283          : alg.ToJwtString(0);        // You already map JwtAlgorithm → SecurityAlgorithms via ToJwtString
 284
 285
 286
 287        // For symmetric JWKs, we can treat it like other HMAC keys for validation helpers
 0288        if (string.Equals(jwk.Kty, "oct", StringComparison.OrdinalIgnoreCase))
 289        {
 290            // JWK 'k' is the base64url-encoded raw key material
 0291            if (string.IsNullOrEmpty(jwk.K))
 292            {
 0293                throw new InvalidOperationException("Symmetric JWK is missing 'k' value.");
 294            }
 295
 0296            var raw = Base64UrlEncoder.DecodeBytes(jwk.K);
 0297            _issuerSigningKey = new SymmetricSecurityKey(raw)
 0298            {
 0299                KeyId = jwk.Kid ?? Guid.NewGuid().ToString("N")
 0300            };
 301        }
 302
 0303        _pendingSign = new PendingJwkSign(jwk, resolvedAlg);
 0304        return this;
 305    }
 306
 307
 308    // ── encryption helpers (lazy) ───────────────────────────────────
 309
 310    // 1️⃣  X.509 certificate (RSA or EC public key)
 311    /// <summary>
 312    /// Encrypts the JWT using the provided X.509 certificate.
 313    /// </summary>
 314    /// <param name="cert">The X.509 certificate to use for encryption.</param>
 315    /// <param name="keyAlg">The key encryption algorithm (default is "RSA-OAEP").</param>
 316    /// <param name="encAlg">The content encryption algorithm (default is "A256GCM").</param>
 317    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 318    public JwtTokenBuilder EncryptWithCertificate(
 319        X509Certificate2 cert,
 320        string keyAlg = "RSA-OAEP",
 321        string encAlg = "A256GCM")
 322    {
 0323        _pendingEnc = new PendingCertEnc(cert, keyAlg, encAlg);
 0324        return this;
 325    }
 326
 327    /// <summary>
 328    /// Encrypts the JWT using a PEM-encoded RSA public key.
 329    /// </summary>
 330    /// <param name="pemPath">The file path to the PEM-encoded RSA public key.</param>
 331    /// <param name="keyAlg">The key encryption algorithm (default is "RSA-OAEP").</param>
 332    /// <param name="encAlg">The content encryption algorithm (default is "A256GCM").</param>
 333    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 334    public JwtTokenBuilder EncryptWithPemPublic(
 335        string pemPath,
 336        string keyAlg = "RSA-OAEP",
 337        string encAlg = "A256GCM")
 338    {
 1339        _pendingEnc = new PendingRsaEnc(File.ReadAllText(pemPath), keyAlg, encAlg);
 1340        return this;
 341    }
 342
 343    /// <summary>
 344    /// Encrypts the JWT using a symmetric key provided as a hexadecimal string.
 345    /// </summary>
 346    /// <param name="hex">The symmetric key as a hexadecimal string.</param>
 347    /// <param name="keyAlg">The key encryption algorithm (default is "dir").</param>
 348    /// <param name="encAlg">The content encryption algorithm (default is "A256CBC-HS512").</param>
 349    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 350    public JwtTokenBuilder EncryptWithSecretHex(
 351        string hex,
 352        string keyAlg = "dir",
 0353        string encAlg = "A256CBC-HS512") => EncryptWithSecret(Convert.FromHexString(hex), keyAlg, encAlg);
 354
 355    /// <summary>
 356    /// Encrypts the JWT using a symmetric key provided as a Base64Url-encoded string.
 357    /// </summary>
 358    /// <param name="b64Url">The symmetric key as a Base64Url-encoded string.</param>
 359    /// <param name="keyAlg">The key encryption algorithm (default is "dir").</param>
 360    /// <param name="encAlg">The content encryption algorithm (default is "A256CBC-HS512").</param>
 361    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 362    public JwtTokenBuilder EncryptWithSecretB64(
 363        string b64Url,
 364        string keyAlg = "dir",
 0365        string encAlg = "A256CBC-HS512") => EncryptWithSecret(Base64UrlEncoder.DecodeBytes(b64Url), keyAlg, encAlg);
 366
 367    /// <summary>
 368    /// Encrypts the JWT using a symmetric key provided as a byte array.
 369    /// </summary>
 370    /// <param name="keyBytes">The symmetric key as a byte array.</param>
 371    /// <param name="keyAlg">The key encryption algorithm (default is "dir").</param>
 372    /// <param name="encAlg">The content encryption algorithm (default is "A256CBC-HS512").</param>
 373    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 374    public JwtTokenBuilder EncryptWithSecret(
 375        byte[] keyBytes,
 376        string keyAlg = "dir",
 377        string encAlg = "A256CBC-HS512")
 378    {
 3379        var b64u = Base64UrlEncoder.Encode(keyBytes);
 3380        _pendingEnc = new PendingSymmetricEnc(b64u, keyAlg, encAlg);
 3381        return this;
 382    }
 383
 384
 385    /// <summary>
 386    /// Encrypts the JWT payload using a JWK (public key) in JSON format.
 387    /// </summary>
 388    /// <param name="jwkJson">The JWK JSON string (typically an RSA or EC public key).</param>
 389    /// <param name="keyAlg">The JWE key management algorithm (default: "RSA-OAEP").</param>
 390    /// <param name="encAlg">The JWE content encryption algorithm (default: "A256GCM").</param>
 391    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 392    public JwtTokenBuilder EncryptWithJwkJson(
 393        string jwkJson,
 394        string keyAlg = "RSA-OAEP",
 395        string encAlg = "A256GCM")
 396    {
 0397        if (string.IsNullOrWhiteSpace(jwkJson))
 398        {
 0399            throw new ArgumentException("JWK JSON cannot be null or empty.", nameof(jwkJson));
 400        }
 401
 0402        _pendingEnc = new PendingJwkEnc(jwkJson, keyAlg, encAlg);
 0403        return this;
 404    }
 405
 406    /// <summary>
 407    /// Encrypts the JWT payload using a JWK read from a file.
 408    /// </summary>
 409    /// <param name="jwkPath">Path to the JWK JSON file.</param>
 410    /// <param name="keyAlg">The JWE key management algorithm (default: "RSA-OAEP").</param>
 411    /// <param name="encAlg">The JWE content encryption algorithm (default: "A256GCM").</param>
 412    /// <returns>The current <see cref="JwtTokenBuilder"/> instance.</returns>
 413    public JwtTokenBuilder EncryptWithJwkPath(
 414        string jwkPath,
 415        string keyAlg = "RSA-OAEP",
 416        string encAlg = "A256GCM")
 417    {
 0418        if (string.IsNullOrWhiteSpace(jwkPath))
 419        {
 0420            throw new ArgumentException("JWK path cannot be null or empty.", nameof(jwkPath));
 421        }
 422
 0423        var fullPath = Path.GetFullPath(jwkPath);
 0424        var jwkJson = File.ReadAllText(fullPath);
 425
 0426        _pendingEnc = new PendingJwkEnc(jwkJson, keyAlg, encAlg);
 0427        return this;
 428    }
 429
 430
 431
 432    // ───── Build the compact JWT ──────────────────────────────────────
 433
 434    /// <summary>
 435    /// Builds the JWT token.
 436    /// This method constructs the JWT token using the configured parameters and returns it as a compact string.
 437    /// </summary>
 438    /// <returns>The JWT token as a compact string.</returns>
 439    /// <exception cref="InvalidOperationException">Thrown if no signing credentials are configured.</exception>
 440    /// <remarks>
 441    /// This method constructs the JWT token using the configured parameters and returns it as a compact string.
 442    /// </remarks>
 443    private string BuildToken()
 444    {
 25445        var handler = new JwtSecurityTokenHandler();
 446        // ── build creds lazily now ───────────────────────────
 25447        var signCreds = BuildSigningCredentials(out _issuerSigningKey) ?? throw new InvalidOperationException("No signin
 25448        Algorithm = signCreds.Algorithm;
 25449        var encCreds = BuildEncryptingCredentials();
 24450        if (_nbf < DateTime.UtcNow)
 451        {
 24452            _nbf = DateTime.UtcNow;
 453        }
 24454        var token = handler.CreateJwtSecurityToken(
 24455            issuer: _issuer,
 24456            audience: _aud,
 24457            subject: new ClaimsIdentity(_claims),
 24458            notBefore: _nbf,
 24459            expires: _nbf.Add(_lifetime),
 24460            issuedAt: DateTime.UtcNow,
 24461            signingCredentials: signCreds,
 24462            encryptingCredentials: encCreds);
 463
 464
 54465        foreach (var kv in _header)
 466        {
 4467            token.Header[kv.Key] = kv.Value;
 468        }
 469
 23470        return handler.WriteToken(token);
 471    }
 472
 473    /// <summary>
 474    /// Builds the JWT token.
 475    /// This method constructs the JWT token using the configured parameters and returns it as a compact string.
 476    /// </summary>
 477    /// <param name="signingKey">The signing key used to sign the JWT.</param>
 478    /// <returns>The JWT token as a compact string.</returns>
 479    /// <exception cref="InvalidOperationException">Thrown if no signing credentials are configured.</exception>
 480    /// <remarks>
 481    /// This method constructs the JWT token using the configured parameters and returns it as a compact string.
 482    /// </remarks>
 483    private string BuildToken(out SymmetricSecurityKey? signingKey)
 484    {
 25485        var jwt = BuildToken();          // call the original Build()
 23486        signingKey = _issuerSigningKey;  // may be null for unsigned / RSA / cert
 23487        return jwt;
 488    }
 489
 490
 491    /// <summary>
 492    /// Builds the JWT token and returns a <see cref="JwtBuilderResult"/> containing the token, signing key, and validit
 493    /// </summary>
 494    /// <returns>A <see cref="JwtBuilderResult"/> containing the JWT token, signing key, and validity period.</returns>
 495    public JwtBuilderResult Build()
 496    {
 497        // ① produce the raw token + signing key
 25498        var token = BuildToken(out var key);
 499        // ② Parse it immediately to pull out the valid-from / valid-to
 23500        var handler = new JwtSecurityTokenHandler();
 23501        var jwtToken = handler.ReadJwtToken(token);
 23502        var issuedAt = jwtToken.ValidFrom.ToUniversalTime();
 23503        var expires = jwtToken.ValidTo.ToUniversalTime();
 504        // ③ return the helper object
 23505        return new JwtBuilderResult(token, key, this, issuedAt, expires);
 506    }
 507
 508    // ────────── helpers that materialise creds ───────────
 509
 510    /// <summary>
 511    /// Builds the signing credentials.
 512    /// This method constructs the signing credentials based on the pending signing configuration.
 513    /// If no signing configuration is set, it returns null.
 514    /// </summary>
 515    /// <param name="key">The symmetric security key to use for signing.</param>
 516    /// <returns>The signing credentials, or null if not configured.</returns>
 517    /// <exception cref="InvalidOperationException">Thrown if no signing configuration is set.</exception>
 518    /// <remarks>
 519    /// This method constructs the signing credentials based on the pending signing configuration.
 520    /// If no signing configuration is set, it returns null.
 521    /// </remarks>
 522    private SigningCredentials? BuildSigningCredentials(out SymmetricSecurityKey? key)
 523    {
 27524        key = null;
 525
 27526        return _pendingSign switch
 27527        {
 19528            PendingSymmetricSign ps => CreateHsCreds(ps, out key),
 3529            PendingRsaSign pr => CreateRsaCreds(pr),
 5530            PendingCertSign pc => CreateCertCreds(pc),
 0531            PendingJwkSign pj => CreateJwkCreds(pj),
 0532            _ => null
 27533        };
 534    }
 535
 536    /// <summary>
 537    /// Builds the signing credentials.
 538    /// This method constructs the signing credentials based on the pending signing configuration.
 539    /// If no signing configuration is set, it returns null.
 540    /// </summary>
 541    /// <param name="ps">The pending symmetric signing configuration.</param>
 542    /// <param name="key">The symmetric security key to use for signing.</param>
 543    /// <returns>The signing credentials, or null if not configured.</returns>
 544    /// <exception cref="InvalidOperationException">Thrown if no signing configuration is set.</exception>
 545    /// <remarks>
 546    /// This method constructs the signing credentials based on the pending symmetric signing configuration.
 547    /// If no signing configuration is set, it returns null.
 548    /// </remarks>
 549    private static SigningCredentials CreateHsCreds(
 550     PendingSymmetricSign ps,
 551     out SymmetricSecurityKey key)
 552    {
 553        // 1) decode the Base64Url secret
 19554        var raw = Base64UrlEncoder.DecodeBytes(ps.B64u);
 555
 556        // 2) create the SymmetricSecurityKey (and record its KeyId)
 19557        key = new SymmetricSecurityKey(raw)
 19558        {
 19559            KeyId = Guid.NewGuid().ToString("N")
 19560        };
 561
 562        // 3) ps.Alg is *already* the exact SecurityAlgorithms.* constant
 19563        return new SigningCredentials(key, ps.Alg);
 564    }
 565
 566    /// <summary>
 567    /// Creates signing credentials for RSA using the provided PEM string.
 568    /// This method imports the RSA key from the PEM string and returns the signing credentials.
 569    /// </summary>
 570    /// <param name="pr">The pending RSA signing configuration.</param>
 571    /// <returns>The signing credentials for RSA.</returns>
 572    /// <exception cref="InvalidOperationException">Thrown if the PEM string is invalid or cannot be imported.</exceptio
 573    /// <remarks>
 574    /// This method imports the RSA key from the PEM string and returns the signing credentials.
 575    /// </remarks>
 576    private static SigningCredentials CreateRsaCreds(PendingRsaSign pr)
 577    {
 3578        var rsa = RSA.Create();
 3579        rsa.ImportFromPem(pr.Pem);
 3580        var key = new RsaSecurityKey(rsa)
 3581        {
 3582            KeyId = Guid.NewGuid().ToString("N")
 3583        };
 3584        return new SigningCredentials(key, pr.Alg);
 585    }
 586    /// <summary>
 587    /// Creates signing credentials for a certificate.
 588    /// </summary>
 589    /// <param name="pc">The pending certificate signing configuration.</param>
 590    /// <returns>The signing credentials for the certificate.</returns>
 591    /// <exception cref="InvalidOperationException">Thrown if the certificate does not have a private key.</exception>
 592    /// <remarks>
 593    /// This method creates signing credentials for a certificate using the provided certificate.
 594    /// </remarks>
 595    private static SigningCredentials CreateCertCreds(PendingCertSign pc)
 596    {
 5597        var cert = pc.Cert;
 5598        var key = new X509SecurityKey(cert);  // thumbprint becomes kid
 5599        return new SigningCredentials(key, pc.Alg);
 600    }
 601
 602    /// <summary>
 603    /// Creates signing credentials for a JsonWebKey.
 604    /// </summary>
 605    /// <param name="pj">The pending JWK signing configuration.</param>
 606    /// <returns>The signing credentials for the JWK.</returns>
 607    /// <exception cref="NotSupportedException">Thrown if the JWK key type is incompatible with the specified algorithm.
 608    /// <remarks>
 609    /// This method creates signing credentials for a JsonWebKey using the provided JWK.
 610    /// </remarks>
 611    private static SigningCredentials CreateJwkCreds(
 612        PendingJwkSign pj)
 613    {
 0614        var jwk = pj.Jwk;
 615
 616        // Optional sanity checks: ensure alg matches key type
 0617        if (string.Equals(jwk.Kty, "RSA", StringComparison.OrdinalIgnoreCase) &&
 0618            !pj.Alg.StartsWith("RS", StringComparison.OrdinalIgnoreCase))
 619        {
 0620            throw new NotSupportedException(
 0621                $"Incompatible algorithm '{pj.Alg}' for RSA JWK (kty='RSA').");
 622        }
 623
 0624        if (string.Equals(jwk.Kty, "EC", StringComparison.OrdinalIgnoreCase) &&
 0625            !pj.Alg.StartsWith("ES", StringComparison.OrdinalIgnoreCase))
 626        {
 0627            throw new NotSupportedException(
 0628                $"Incompatible algorithm '{pj.Alg}' for EC JWK (kty='EC').");
 629        }
 630
 0631        if (string.Equals(jwk.Kty, "oct", StringComparison.OrdinalIgnoreCase) &&
 0632            !pj.Alg.StartsWith("HS", StringComparison.OrdinalIgnoreCase))
 633        {
 0634            throw new NotSupportedException(
 0635                $"Incompatible algorithm '{pj.Alg}' for symmetric JWK (kty='oct').");
 636        }
 637
 638        // JsonWebKey is already a SecurityKey
 0639        return new SigningCredentials(jwk, pj.Alg);
 640    }
 641
 642
 643    /// <summary>
 644    /// Builds the encrypting credentials.
 645    /// This method constructs the encrypting credentials based on the pending encryption configuration.
 646    /// If no encryption configuration is set, it returns null.
 647    /// </summary>
 648    /// <returns>The encrypting credentials, or null if not set.</returns>
 649    /// <remarks>
 650    /// This method constructs the encrypting credentials based on the pending encryption configuration.
 651    /// If no encryption configuration is set, it returns null.
 652    /// </remarks>
 653    private EncryptingCredentials? BuildEncryptingCredentials()
 27654        => _pendingEnc switch
 27655        {
 3656            PendingSymmetricEnc se => new SymmetricEncrypt(
 3657                                          se.B64u, se.KeyAlg, se.EncAlg).ToEncryptingCreds(),
 1658            PendingRsaEnc re => new RsaEncrypt(
 1659                                          re.Pem, re.KeyAlg, re.EncAlg).ToEncryptingCreds(),
 0660            PendingCertEnc ce => new CertEncrypt(
 0661                                          ce.Cert, ce.KeyAlg, ce.EncAlg).ToEncryptingCreds(),
 0662            PendingJwkEnc je => new JwkEncrypt(
 0663                                    je.JwkJson, je.KeyAlg, je.EncAlg).ToEncryptingCreds(),
 23664            _ => null
 27665        };
 666
 667
 668
 669    // ───── Internals ──────────────────────────────────────────────────
 670    /// <summary>
 671    /// Gets the claims to be included in the JWT token.
 672    /// </summary>
 29673    private List<Claim> _claims = [];
 674    /// <summary>
 675    /// Gets the headers to be included in the JWT token.
 676    /// </summary>
 29677    private readonly Dictionary<string, object> _header = new(StringComparer.OrdinalIgnoreCase);
 678    /// <summary>
 679    /// Gets the not before (nbf) claim for the JWT token.
 680    /// </summary>
 29681    private DateTime _nbf = DateTime.UtcNow;
 682    /// <summary>
 683    /// Gets the lifetime of the JWT token.
 684    /// </summary>
 29685    private TimeSpan _lifetime = TimeSpan.FromHours(1);
 686    private string? _issuer, _aud;
 687
 688
 689    // ── Strategy interfaces & concrete configs ───────────────────────
 690    private interface ISignConfig { SigningCredentials ToSigningCreds(); }
 691    private interface IEncConfig { EncryptingCredentials ToEncryptingCreds(); }
 692
 693    private static class Map
 694    {
 695        // maps short names (HS256, RSA-OAEP, …) to SecurityAlgorithms
 1696        public static readonly IReadOnlyDictionary<string, string> Jws = new Dictionary<string, string>(StringComparer.O
 1697        {
 1698            ["HS256"] = SecurityAlgorithms.HmacSha256,
 1699            ["HS384"] = SecurityAlgorithms.HmacSha384,
 1700            ["HS512"] = SecurityAlgorithms.HmacSha512,
 1701            ["RS256"] = SecurityAlgorithms.RsaSha256,
 1702            ["RS384"] = SecurityAlgorithms.RsaSha384,
 1703            ["RS512"] = SecurityAlgorithms.RsaSha512,
 1704            ["PS256"] = SecurityAlgorithms.RsaSsaPssSha256,
 1705            ["PS384"] = SecurityAlgorithms.RsaSsaPssSha384,
 1706            ["PS512"] = SecurityAlgorithms.RsaSsaPssSha512,
 1707            ["ES256"] = SecurityAlgorithms.EcdsaSha256,
 1708            ["ES384"] = SecurityAlgorithms.EcdsaSha384,
 1709            ["ES512"] = SecurityAlgorithms.EcdsaSha512
 1710        };
 1711        public static readonly IReadOnlyDictionary<string, string> KeyAlg = new Dictionary<string, string>(StringCompare
 1712        {
 1713            ["RSA-OAEP"] = SecurityAlgorithms.RsaOAEP,
 1714            ["RSA-OAEP-256"] = "RSA-OAEP-256",
 1715            ["RSA-OAEP-384"] = "RSA-OAEP-384",
 1716            ["RSA-OAEP-512"] = "RSA-OAEP-512",
 1717            ["RSA1_5"] = SecurityAlgorithms.RsaPKCS1,
 1718            ["A128KW"] = SecurityAlgorithms.Aes128KW,
 1719            ["A192KW"] = SecurityAlgorithms.Aes192KW,
 1720            ["A256KW"] = SecurityAlgorithms.Aes256KW,
 1721            ["ECDH-ES"] = SecurityAlgorithms.EcdhEs,
 1722            ["ECDH-ESA128KW"] = SecurityAlgorithms.EcdhEsA128kw,
 1723            ["ECDH-ESA192KW"] = SecurityAlgorithms.EcdhEsA192kw,
 1724            ["ECDH-ESA256KW"] = SecurityAlgorithms.EcdhEsA256kw,
 1725            ["dir"] = "dir"
 1726        };
 1727        public static readonly IReadOnlyDictionary<string, string> EncAlg = new Dictionary<string, string>(StringCompare
 1728        {
 1729            ["A128GCM"] = SecurityAlgorithms.Aes128Gcm,
 1730            ["A192GCM"] = SecurityAlgorithms.Aes192Gcm,
 1731            ["A256GCM"] = SecurityAlgorithms.Aes256Gcm,
 1732            ["A128CBC-HS256"] = SecurityAlgorithms.Aes128CbcHmacSha256,
 1733            ["A192CBC-HS384"] = SecurityAlgorithms.Aes192CbcHmacSha384,
 1734            ["A256CBC-HS512"] = SecurityAlgorithms.Aes256CbcHmacSha512
 1735        };
 736    }
 737
 738    // ── Signing configs ───────────────────────────────────────────────
 0739    private sealed record SymmetricSign(
 0740         SecurityKey Key, string ResolvedAlg) : ISignConfig
 741    {
 742        public SigningCredentials ToSigningCreds()
 0743            => new(Key, ResolvedAlg);
 744    }
 745
 0746    private sealed record RsaSign(string Pem, string Alg) : ISignConfig
 747    {
 748        public SigningCredentials ToSigningCreds()
 749        {
 0750            var rsa = RSA.Create(); rsa.ImportFromPem(Pem);
 0751            var key = new RsaSecurityKey(rsa);
 0752            var algo = Alg.Equals("auto", StringComparison.OrdinalIgnoreCase) ? Map.Jws["RS256"] : Map.Jws[Alg];
 0753            return new SigningCredentials(key, algo);
 754        }
 755    }
 756
 757
 0758    private sealed record CertSign(X509Certificate2 Cert, string Alg) : ISignConfig
 759    {
 760        public SigningCredentials ToSigningCreds()
 761        {
 0762            if (!Cert.HasPrivateKey)
 763            {
 0764                throw new ArgumentException("Certificate must contain a private key.");
 765            }
 766
 0767            var key = new X509SecurityKey(Cert);
 768
 769            // Pick default alg if caller passed "auto"
 770            string resolvedAlg;
 0771            if (!Alg.Equals("auto", StringComparison.OrdinalIgnoreCase))
 772            {
 0773                resolvedAlg = Map.Jws[Alg];
 774            }
 775            else
 776            {
 0777                if (Cert.GetECDsaPublicKey() is not null)
 778                {
 0779                    resolvedAlg = Map.Jws["ES256"];   // ECDSA → ES256 by default
 780                }
 0781                else if (Cert.GetRSAPublicKey() is not null)
 782                {
 0783                    resolvedAlg = Map.Jws["RS256"];   // RSA   → RS256 by default
 784                }
 785                else
 786                {
 0787                    var keyType = "unknown";
 0788                    if (Cert.PublicKey != null && Cert.PublicKey.EncodedKeyValue != null && Cert.PublicKey.EncodedKeyVal
 789                    {
 0790                        keyType = Cert.PublicKey.EncodedKeyValue.Oid.FriendlyName ?? "unknown";
 791                    }
 792
 0793                    throw new NotSupportedException(
 0794                        $"Unsupported key type: {keyType}");
 795                }
 796            }
 797
 0798            return new SigningCredentials(key, resolvedAlg);
 799        }
 800    }
 801
 802    // ── Encryption configs ────────────────────────────────────────────
 27803    private abstract record BaseEnc(string KeyAlg, string EncAlg) : IEncConfig
 804    {
 1805        protected string KeyAlgMapped => Map.KeyAlg[KeyAlg];
 1806        protected string EncAlgMapped => Map.EncAlg[EncAlg];
 807        public abstract EncryptingCredentials ToEncryptingCreds();
 808    }
 809
 0810    private sealed record CertEncrypt(X509Certificate2 Cert, string KeyAlg, string EncAlg) : BaseEnc(KeyAlg, EncAlg)
 811    {
 812        public override EncryptingCredentials ToEncryptingCreds()
 813        {
 0814            var key = new X509SecurityKey(Cert);
 0815            return new EncryptingCredentials(key, KeyAlgMapped, EncAlgMapped);
 816        }
 817    }
 818
 2819    private sealed record RsaEncrypt(string Pem, string KeyAlg, string EncAlg) : BaseEnc(KeyAlg, EncAlg)
 820    {
 821        public override EncryptingCredentials ToEncryptingCreds()
 822        {
 2823            var rsa = RSA.Create(); rsa.ImportFromPem(Pem);
 1824            var key = new RsaSecurityKey(rsa);
 1825            return new EncryptingCredentials(key, KeyAlgMapped, EncAlgMapped);
 826        }
 827    }
 828
 0829    private sealed record JwkEncrypt(string JwkJson, string KeyAlg, string EncAlg) : BaseEnc(KeyAlg, EncAlg)
 830    {
 831        public override EncryptingCredentials ToEncryptingCreds()
 832        {
 833            // JsonWebKey is a SecurityKey
 0834            var jwk = new JsonWebKey(JwkJson);
 0835            var key = (SecurityKey)jwk;
 836
 0837            return new EncryptingCredentials(
 0838                key,
 0839                KeyAlgMapped,   // mapped from Map.KeyAlg
 0840                EncAlgMapped    // mapped from Map.EncAlg
 0841            );
 842        }
 843    }
 844
 845    private sealed record SymmetricEncrypt(
 6846     string B64,
 847     string KeyAlg,
 3848     string EncAlg) : BaseEnc(KeyAlg, EncAlg)
 849    {
 850        public override EncryptingCredentials ToEncryptingCreds()
 851        {
 3852            if (Log.IsEnabled(LogEventLevel.Debug))
 853            {
 3854                Log.Debug(
 3855                    "Encrypting with {KeyAlg} and {EncAlg} ({Bits} bits)",
 3856                    KeyAlg, EncAlg, Base64UrlEncoder.DecodeBytes(B64).Length * 8);
 857            }
 858            // ────────── shared-secret → SymmetricSecurityKey ──────────
 3859            if (!Map.KeyAlg.ContainsKey(KeyAlg))
 860            {
 0861                throw new ArgumentException($"Unknown key algorithm: {KeyAlg}");
 862            }
 863
 3864            var key = new SymmetricSecurityKey(Base64UrlEncoder.DecodeBytes(B64));
 3865            var bits = key.KeySize;                        // 128 / 192 / 256 / 384 / 512 …
 866
 867            // ────────── auto-pick encAlg for 'dir' default case ───────
 3868            var encEff = EncAlg;
 869
 3870            if (KeyAlg.Equals("dir", StringComparison.OrdinalIgnoreCase) &&
 3871                EncAlg.Equals("A256CBC-HS512", StringComparison.OrdinalIgnoreCase))
 872            {
 0873                encEff = bits switch
 0874                {
 0875                    128 => "A128GCM",
 0876                    192 => "A192GCM",
 0877                    256 => "A256GCM",
 0878                    384 => "A192CBC-HS384",
 0879                    512 => "A256CBC-HS512",
 0880                    _ => throw new ArgumentException(
 0881                               $"Unsupported key size {bits} bits for direct encryption.")
 0882                };
 883            }
 884
 885            // ────────── hard validation (caller may specify any enc) ──
 886            static void Require(int actualBits, int requiredBits, string alg)
 887            {
 3888                _ = actualBits == requiredBits
 3889                    ? true
 3890                    : throw new ArgumentException($"{alg} requires a {requiredBits}-bit key.");
 2891            }
 892
 3893            switch (encEff.ToUpperInvariant())
 894            {
 2895                case "A128GCM": Require(bits, 128, encEff); break;
 0896                case "A192GCM": Require(bits, 192, encEff); break;
 1897                case "A256GCM": Require(bits, 256, encEff); break;
 2898                case "A128CBC-HS256": Require(bits, 256, encEff); break;
 0899                case "A192CBC-HS384": Require(bits, 384, encEff); break;
 0900                case "A256CBC-HS512": Require(bits, 512, encEff); break;
 901                default:
 0902                    throw new ArgumentException($"Unknown or unsupported enc algorithm: {encEff}");
 903            }
 2904            if (Log.IsEnabled(LogEventLevel.Debug))
 905            {
 2906                Log.Debug(
 2907                    "Encrypting with {KeyAlg} and {EncAlg} ({Bits} bits)",
 2908                    KeyAlg, encEff, bits);
 909            }
 910            // ────────── build EncryptingCredentials ───────────────────
 2911            return new EncryptingCredentials(
 2912                key,
 2913                Map.KeyAlg[KeyAlg.ToUpperInvariant()],          // 'dir', 'A256KW', …
 2914                Map.EncAlg[encEff.ToUpperInvariant()]);         // validated / auto-picked enc
 915        }
 916    }
 917
 918    /// <summary>
 919    /// Renews a JWT token from the current request context, optionally extending its lifetime.
 920    /// </summary>
 921    /// <param name="ctx">The Kestrun context containing the request and authorization header.</param>
 922    /// <param name="lifetime">The new lifetime for the renewed token. If null, uses the builder's default lifetime.</pa
 923
 924    /// <returns>The renewed JWT token as a compact string.</returns>
 925    /// <exception cref="UnauthorizedAccessException">Thrown if no Bearer token is provided in the request.</exception>
 926    public string RenewJwt(
 927            KestrunContext ctx,
 928            TimeSpan? lifetime = null)
 929    {
 2930        if (ctx.Request.Authorization == null || (!ctx.Request.Authorization?.StartsWith("Bearer ") ?? true))
 931        {
 1932            return string.Empty;
 933        }
 1934        var authHeader = ctx.Request.Authorization;
 1935        var strToken = authHeader != null ? authHeader["Bearer ".Length..].Trim() : throw new UnauthorizedAccessExceptio
 1936        return RenewJwt(jwt: strToken, lifetime: lifetime);
 937    }
 938
 939    /// <summary>
 940    /// Extends the validity period of an existing JWT token by creating a new token with updated lifetime.
 941    /// </summary>
 942    /// <param name="jwt">The original JWT token to extend.</param>
 943    /// <param name="lifetime">The new lifetime for the extended token. If null, uses the builder's default lifetime.</p
 944    /// <returns>The extended JWT token as a compact string.</returns>
 945    public string RenewJwt(
 946        string jwt,
 947        TimeSpan? lifetime = null)
 948    {
 2949        var handler = new JwtSecurityTokenHandler();
 950
 951        // Read raw token (no mapping, no validation)
 2952        var old = handler.ReadJwtToken(jwt);
 2953        var _builder = CloneBuilder();
 954        // Copy all non-time claims
 2955        var reserved = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 2956        { "exp", "nbf", "iat"  };
 957
 15958        var claims = old.Claims.Where(c => !reserved.Contains(c.Type)).ToList();
 959
 960        // If you rely on "sub", make sure it’s there (some libs put it into NameIdentifier)
 4961        if (!claims.Any(c => c.Type == JwtRegisteredClaimNames.Sub))
 962        {
 0963            var sub = old.Claims.FirstOrDefault(c =>
 0964                         c.Type is JwtRegisteredClaimNames.Sub or
 0965                         ClaimTypes.NameIdentifier)?.Value;
 0966            if (!string.IsNullOrEmpty(sub))
 967            {
 0968                claims.Add(new Claim(JwtRegisteredClaimNames.Sub, sub));
 969            }
 970        }
 971
 2972        var signCreds = BuildSigningCredentials(out _issuerSigningKey) ?? throw new InvalidOperationException("No signin
 2973        Algorithm = signCreds.Algorithm;
 2974        var encCreds = BuildEncryptingCredentials();
 975
 976        // Keep the same kid if present by setting it on the signing key
 977        // signing.Key.KeyId = old.Header.Kid; // uncomment if you must mirror the old 'kid'
 2978        if (_nbf < DateTime.UtcNow)
 979        {
 2980            _nbf = DateTime.UtcNow;
 981        }
 2982        if (lifetime is null)
 983        {
 0984            lifetime = _lifetime;
 985        }
 2986        else if (lifetime < TimeSpan.Zero)
 987        {
 0988            throw new ArgumentOutOfRangeException(nameof(lifetime), "Lifetime must be a positive TimeSpan.");
 989        }
 2990        var token = handler.CreateJwtSecurityToken(
 2991                issuer: _issuer,
 2992                audience: _aud,
 2993                subject: new ClaimsIdentity(claims),
 2994                notBefore: _nbf,
 2995                expires: _nbf.Add((TimeSpan)lifetime),
 2996                issuedAt: DateTime.UtcNow,
 2997                signingCredentials: signCreds,
 2998                encryptingCredentials: encCreds);
 999
 41000        foreach (var kv in _header)
 1001        {
 01002            token.Header[kv.Key] = kv.Value;
 1003        }
 1004
 21005        return handler.WriteToken(token);
 1006    }
 1007    /*
 1008        public JwtTokenPackage BuildPackage()
 1009        {
 1010            string jwt = BuildToken(out var key);   // your existing overload
 1011
 1012            var tvp = new TokenValidationParameters
 1013            {
 1014                ValidateIssuer = true,
 1015                ValidIssuer = _issuer,      // private fields in builder
 1016                ValidateAudience = true,
 1017                ValidAudience = _aud,
 1018                ValidateLifetime = true,
 1019                ClockSkew = TimeSpan.FromMinutes(1),
 1020
 1021                RequireSignedTokens = key is not null,
 1022                ValidateIssuerSigningKey = key is not null,
 1023                IssuerSigningKey = key,
 1024                ValidAlgorithms = key is not null
 1025                    ? new[] { SecurityAlgorithms.HmacSha256 }
 1026                    : Array.Empty<string>()
 1027            };
 1028
 1029            return new JwtTokenPackage(jwt, key, tvp);
 1030        }
 1031    */
 1032}

Methods/Properties

New()
WithIssuer(System.String)
WithAudience(System.String)
WithSubject(System.String)
AddClaim(System.String,System.String)
ValidFor(System.TimeSpan)
NotBefore(System.DateTime)
AddHeader(System.String,System.Object)
get_Issuer()
get_Audience()
get_Algorithm()
get_B64u()
get_Pem()
get_Cert()
get_Jwk()
get_B64u()
get_Pem()
get_Cert()
get_JwkJson()
SignWithSecret(System.String,Kestrun.Jwt.JwtAlgorithm)
CloneBuilder()
SignWithSecretHex(System.String,Kestrun.Jwt.JwtAlgorithm)
SignWithSecretPassphrase(System.Security.SecureString,Kestrun.Jwt.JwtAlgorithm)
SignWithRsaPem(System.String,Kestrun.Jwt.JwtAlgorithm)
SignWithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2,Kestrun.Jwt.JwtAlgorithm)
SignWithJwkJson(System.String,Kestrun.Jwt.JwtAlgorithm)
EncryptWithCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2,System.String,System.String)
EncryptWithPemPublic(System.String,System.String,System.String)
EncryptWithSecretHex(System.String,System.String,System.String)
EncryptWithSecretB64(System.String,System.String,System.String)
EncryptWithSecret(System.Byte[],System.String,System.String)
EncryptWithJwkJson(System.String,System.String,System.String)
EncryptWithJwkPath(System.String,System.String,System.String)
BuildToken()
BuildToken(Microsoft.IdentityModel.Tokens.SymmetricSecurityKey&)
Build()
BuildSigningCredentials(Microsoft.IdentityModel.Tokens.SymmetricSecurityKey&)
CreateHsCreds(Kestrun.Jwt.JwtTokenBuilder/PendingSymmetricSign,Microsoft.IdentityModel.Tokens.SymmetricSecurityKey&)
CreateRsaCreds(Kestrun.Jwt.JwtTokenBuilder/PendingRsaSign)
CreateCertCreds(Kestrun.Jwt.JwtTokenBuilder/PendingCertSign)
CreateJwkCreds(Kestrun.Jwt.JwtTokenBuilder/PendingJwkSign)
BuildEncryptingCredentials()
.ctor()
.cctor()
.ctor(Microsoft.IdentityModel.Tokens.SecurityKey,System.String)
get_Key()
ToSigningCreds()
get_Pem()
ToSigningCreds()
get_Cert()
ToSigningCreds()
get_KeyAlg()
get_KeyAlgMapped()
get_EncAlgMapped()
get_Cert()
ToEncryptingCreds()
get_Pem()
ToEncryptingCreds()
get_JwkJson()
ToEncryptingCreds()
get_B64()
.ctor(System.String,System.String,System.String)
ToEncryptingCreds()
Require()
RenewJwt(Kestrun.Models.KestrunContext,System.Nullable`1<System.TimeSpan>)
RenewJwt(System.String,System.Nullable`1<System.TimeSpan>)