< 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@9d3a582b2d63930269564a7591aa77ef297cadeb
Line coverage
80%
Covered lines: 232
Uncovered lines: 58
Coverable lines: 290
Total lines: 869
Line coverage: 80%
Branch coverage
50%
Covered branches: 58
Total branches: 116
Branch coverage: 50%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

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_B64u()100%11100%
get_Pem()100%11100%
get_Cert()100%210%
SignWithSecret(...)100%22100%
CloneBuilder()100%11100%
SignWithSecretHex(...)100%11100%
SignWithSecretPassphrase(...)100%11100%
SignWithRsaPem(...)50%22100%
SignWithCertificate(...)83.33%66100%
EncryptWithCertificate(...)100%210%
EncryptWithPemPublic(...)100%11100%
EncryptWithSecretHex(...)100%210%
EncryptWithSecretB64(...)100%210%
EncryptWithSecret(...)100%11100%
BuildToken()83.33%66100%
BuildToken(...)100%11100%
Build()100%11100%
BuildSigningCredentials(...)83.33%6687.5%
CreateHsCreds(...)100%11100%
CreateRsaCreds(...)100%11100%
CreateCertCreds(...)100%11100%
BuildEncryptingCredentials()83.33%6680%
.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_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.Hosting;  // 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);
 101
 12102    private sealed record PendingSymmetricEnc(string B64u, string KeyAlg, string EncAlg);
 4103    private sealed record PendingRsaEnc(string Pem, string KeyAlg, string EncAlg);
 0104    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    {
 20121        if (string.IsNullOrWhiteSpace(b64Url))
 122        {
 2123            throw new ArgumentNullException(nameof(b64Url));
 124        }
 125
 126        // 1) Decode the incoming Base64Url to bytes
 18127        var raw = Base64UrlEncoder.DecodeBytes(b64Url);
 128
 129        // 2) Create (and remember) the SymmetricSecurityKey
 18130        var key = new SymmetricSecurityKey(raw)
 18131        {
 18132            KeyId = Guid.NewGuid().ToString("N")
 18133        };
 18134        _issuerSigningKey = key;
 135
 136        // 3) Resolve "Auto" or map the enum to the exact JWS alg string
 18137        var resolvedAlg = alg.ToJwtString(raw.Length);
 138
 139        // 4) Store the pending sign using the resolved algorithm
 18140        _pendingSign = new PendingSymmetricSign(b64Url, resolvedAlg);
 141
 18142        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    {
 2152        var clone = (JwtTokenBuilder)MemberwiseClone();
 2153        clone._claims = [.. _claims];
 2154        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>
 2163    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    {
 1175        ArgumentNullException.ThrowIfNull(passPhrase);
 176
 177        // Marshal to unmanaged Unicode (UTF-16) buffer
 1178        var unicodePtr = Marshal.SecureStringToGlobalAllocUnicode(passPhrase);
 179        try
 180        {
 1181            var charCount = passPhrase.Length;
 1182            var unicodeBytes = new byte[charCount * sizeof(char)];
 183            // copy from unmanaged -> managed
 1184            Marshal.Copy(unicodePtr, unicodeBytes, 0, unicodeBytes.Length);
 185
 186            // convert UTF-16 bytes directly to UTF-8
 1187            var utf8Bytes = Encoding.Convert(Encoding.Unicode, Encoding.UTF8, unicodeBytes);
 188            // zero-out the intermediate Unicode bytes
 1189            Array.Clear(unicodeBytes, 0, unicodeBytes.Length);
 190
 1191            var b64url = Base64UrlEncoder.Encode(utf8Bytes);
 192            // zero-out the UTF-8 bytes too
 1193            Array.Clear(utf8Bytes, 0, utf8Bytes.Length);
 194
 1195            return SignWithSecret(b64url, alg);
 196        }
 197        finally
 198        {
 199            // zero-free the unmanaged buffer
 1200            Marshal.ZeroFreeGlobalAllocUnicode(unicodePtr);
 1201        }
 1202    }
 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    {
 3216        var pem = File.ReadAllText(pemPath);
 217
 218        // Auto ⇒ default RS256; otherwise map enum to the exact string
 3219        var resolvedAlg = alg == JwtAlgorithm.Auto
 3220            ? SecurityAlgorithms.RsaSha256
 3221            : alg.ToJwtString(0);
 222
 3223        _pendingSign = new PendingRsaSign(pem, resolvedAlg);
 3224        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    {
 6234        if (!cert.HasPrivateKey)
 235        {
 1236            throw new ArgumentException(
 1237                "Certificate must contain a private key.", nameof(cert));
 238        }
 239
 240        // Auto ⇒ ES256 for ECDSA keys, RS256 for RSA keys
 5241        var resolvedAlg = alg == JwtAlgorithm.Auto
 5242            ? (cert.GetECDsaPublicKey() is not null
 5243                ? SecurityAlgorithms.EcdsaSha256
 5244                : SecurityAlgorithms.RsaSha256)
 5245            : alg.ToJwtString(0);
 246
 5247        _pendingSign = new PendingCertSign(cert, resolvedAlg);
 5248        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    {
 0268        _pendingEnc = new PendingCertEnc(cert, keyAlg, encAlg);
 0269        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    {
 1284        _pendingEnc = new PendingRsaEnc(File.ReadAllText(pemPath), keyAlg, encAlg);
 1285        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",
 0298        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",
 0310        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    {
 3324        var b64u = Base64UrlEncoder.Encode(keyBytes);
 3325        _pendingEnc = new PendingSymmetricEnc(b64u, keyAlg, encAlg);
 3326        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    {
 25343        var handler = new JwtSecurityTokenHandler();
 344        // ── build creds lazily now ───────────────────────────
 25345        var signCreds = BuildSigningCredentials(out _issuerSigningKey) ?? throw new InvalidOperationException("No signin
 25346        Algorithm = signCreds.Algorithm;
 25347        var encCreds = BuildEncryptingCredentials();
 24348        if (_nbf < DateTime.UtcNow)
 349        {
 24350            _nbf = DateTime.UtcNow;
 351        }
 24352        var token = handler.CreateJwtSecurityToken(
 24353            issuer: _issuer,
 24354            audience: _aud,
 24355            subject: new ClaimsIdentity(_claims),
 24356            notBefore: _nbf,
 24357            expires: _nbf.Add(_lifetime),
 24358            issuedAt: DateTime.UtcNow,
 24359            signingCredentials: signCreds,
 24360            encryptingCredentials: encCreds);
 361
 362
 54363        foreach (var kv in _header)
 364        {
 4365            token.Header[kv.Key] = kv.Value;
 366        }
 367
 23368        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    {
 25383        var jwt = BuildToken();          // call the original Build()
 23384        signingKey = _issuerSigningKey;  // may be null for unsigned / RSA / cert
 23385        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
 25396        var token = BuildToken(out var key);
 397        // ② Parse it immediately to pull out the valid-from / valid-to
 23398        var handler = new JwtSecurityTokenHandler();
 23399        var jwtToken = handler.ReadJwtToken(token);
 23400        var issuedAt = jwtToken.ValidFrom.ToUniversalTime();
 23401        var expires = jwtToken.ValidTo.ToUniversalTime();
 402        // ③ return the helper object
 23403        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    {
 27422        key = null;
 423
 27424        return _pendingSign switch
 27425        {
 19426            PendingSymmetricSign ps => CreateHsCreds(ps, out key),
 3427            PendingRsaSign pr => CreateRsaCreds(pr),
 5428            PendingCertSign pc => CreateCertCreds(pc),
 0429            _ => null
 27430        };
 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
 19451        var raw = Base64UrlEncoder.DecodeBytes(ps.B64u);
 452
 453        // 2) create the SymmetricSecurityKey (and record its KeyId)
 19454        key = new SymmetricSecurityKey(raw)
 19455        {
 19456            KeyId = Guid.NewGuid().ToString("N")
 19457        };
 458
 459        // 3) ps.Alg is *already* the exact SecurityAlgorithms.* constant
 19460        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    {
 3475        var rsa = RSA.Create();
 3476        rsa.ImportFromPem(pr.Pem);
 3477        var key = new RsaSecurityKey(rsa)
 3478        {
 3479            KeyId = Guid.NewGuid().ToString("N")
 3480        };
 3481        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    {
 5494        var cert = pc.Cert;
 5495        var key = new X509SecurityKey(cert);  // thumbprint becomes kid
 5496        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()
 27509        => _pendingEnc switch
 27510        {
 3511            PendingSymmetricEnc se => new SymmetricEncrypt(
 3512                                          se.B64u, se.KeyAlg, se.EncAlg).ToEncryptingCreds(),
 1513            PendingRsaEnc re => new RsaEncrypt(
 1514                                          re.Pem, re.KeyAlg, re.EncAlg).ToEncryptingCreds(),
 0515            PendingCertEnc ce => new CertEncrypt(
 0516                                          ce.Cert, ce.KeyAlg, ce.EncAlg).ToEncryptingCreds(),
 23517            _ => null
 27518        };
 519
 520
 521
 522    // ───── Internals ──────────────────────────────────────────────────
 523    /// <summary>
 524    /// Gets the claims to be included in the JWT token.
 525    /// </summary>
 29526    private List<Claim> _claims = [];
 527    /// <summary>
 528    /// Gets the headers to be included in the JWT token.
 529    /// </summary>
 29530    private readonly Dictionary<string, object> _header = new(StringComparer.OrdinalIgnoreCase);
 531    /// <summary>
 532    /// Gets the not before (nbf) claim for the JWT token.
 533    /// </summary>
 29534    private DateTime _nbf = DateTime.UtcNow;
 535    /// <summary>
 536    /// Gets the lifetime of the JWT token.
 537    /// </summary>
 29538    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
 1549        public static readonly IReadOnlyDictionary<string, string> Jws = new Dictionary<string, string>(StringComparer.O
 1550        {
 1551            ["HS256"] = SecurityAlgorithms.HmacSha256,
 1552            ["HS384"] = SecurityAlgorithms.HmacSha384,
 1553            ["HS512"] = SecurityAlgorithms.HmacSha512,
 1554            ["RS256"] = SecurityAlgorithms.RsaSha256,
 1555            ["RS384"] = SecurityAlgorithms.RsaSha384,
 1556            ["RS512"] = SecurityAlgorithms.RsaSha512,
 1557            ["PS256"] = SecurityAlgorithms.RsaSsaPssSha256,
 1558            ["PS384"] = SecurityAlgorithms.RsaSsaPssSha384,
 1559            ["PS512"] = SecurityAlgorithms.RsaSsaPssSha512,
 1560            ["ES256"] = SecurityAlgorithms.EcdsaSha256,
 1561            ["ES384"] = SecurityAlgorithms.EcdsaSha384,
 1562            ["ES512"] = SecurityAlgorithms.EcdsaSha512
 1563        };
 1564        public static readonly IReadOnlyDictionary<string, string> KeyAlg = new Dictionary<string, string>(StringCompare
 1565        {
 1566            ["RSA-OAEP"] = SecurityAlgorithms.RsaOAEP,
 1567            ["RSA-OAEP-256"] = "RSA-OAEP-256",
 1568            ["RSA-OAEP-384"] = "RSA-OAEP-384",
 1569            ["RSA-OAEP-512"] = "RSA-OAEP-512",
 1570            ["RSA1_5"] = SecurityAlgorithms.RsaPKCS1,
 1571            ["A128KW"] = SecurityAlgorithms.Aes128KW,
 1572            ["A192KW"] = SecurityAlgorithms.Aes192KW,
 1573            ["A256KW"] = SecurityAlgorithms.Aes256KW,
 1574            ["ECDH-ES"] = SecurityAlgorithms.EcdhEs,
 1575            ["ECDH-ESA128KW"] = SecurityAlgorithms.EcdhEsA128kw,
 1576            ["ECDH-ESA192KW"] = SecurityAlgorithms.EcdhEsA192kw,
 1577            ["ECDH-ESA256KW"] = SecurityAlgorithms.EcdhEsA256kw,
 1578            ["dir"] = "dir"
 1579        };
 1580        public static readonly IReadOnlyDictionary<string, string> EncAlg = new Dictionary<string, string>(StringCompare
 1581        {
 1582            ["A128GCM"] = SecurityAlgorithms.Aes128Gcm,
 1583            ["A192GCM"] = SecurityAlgorithms.Aes192Gcm,
 1584            ["A256GCM"] = SecurityAlgorithms.Aes256Gcm,
 1585            ["A128CBC-HS256"] = SecurityAlgorithms.Aes128CbcHmacSha256,
 1586            ["A192CBC-HS384"] = SecurityAlgorithms.Aes192CbcHmacSha384,
 1587            ["A256CBC-HS512"] = SecurityAlgorithms.Aes256CbcHmacSha512
 1588        };
 589    }
 590
 591    // ── Signing configs ───────────────────────────────────────────────
 0592    private sealed record SymmetricSign(
 0593         SecurityKey Key, string ResolvedAlg) : ISignConfig
 594    {
 595        public SigningCredentials ToSigningCreds()
 0596            => new(Key, ResolvedAlg);
 597    }
 598
 0599    private sealed record RsaSign(string Pem, string Alg) : ISignConfig
 600    {
 601        public SigningCredentials ToSigningCreds()
 602        {
 0603            var rsa = RSA.Create(); rsa.ImportFromPem(Pem);
 0604            var key = new RsaSecurityKey(rsa);
 0605            var algo = Alg.Equals("auto", StringComparison.OrdinalIgnoreCase) ? Map.Jws["RS256"] : Map.Jws[Alg];
 0606            return new SigningCredentials(key, algo);
 607        }
 608    }
 609
 610
 0611    private sealed record CertSign(X509Certificate2 Cert, string Alg) : ISignConfig
 612    {
 613        public SigningCredentials ToSigningCreds()
 614        {
 0615            if (!Cert.HasPrivateKey)
 616            {
 0617                throw new ArgumentException("Certificate must contain a private key.");
 618            }
 619
 0620            var key = new X509SecurityKey(Cert);
 621
 622            // Pick default alg if caller passed "auto"
 623            string resolvedAlg;
 0624            if (!Alg.Equals("auto", StringComparison.OrdinalIgnoreCase))
 625            {
 0626                resolvedAlg = Map.Jws[Alg];
 627            }
 628            else
 629            {
 0630                if (Cert.GetECDsaPublicKey() is not null)
 631                {
 0632                    resolvedAlg = Map.Jws["ES256"];   // ECDSA → ES256 by default
 633                }
 0634                else if (Cert.GetRSAPublicKey() is not null)
 635                {
 0636                    resolvedAlg = Map.Jws["RS256"];   // RSA   → RS256 by default
 637                }
 638                else
 639                {
 0640                    var keyType = "unknown";
 0641                    if (Cert.PublicKey != null && Cert.PublicKey.EncodedKeyValue != null && Cert.PublicKey.EncodedKeyVal
 642                    {
 0643                        keyType = Cert.PublicKey.EncodedKeyValue.Oid.FriendlyName ?? "unknown";
 644                    }
 645
 0646                    throw new NotSupportedException(
 0647                        $"Unsupported key type: {keyType}");
 648                }
 649            }
 650
 0651            return new SigningCredentials(key, resolvedAlg);
 652        }
 653    }
 654
 655    // ── Encryption configs ────────────────────────────────────────────
 27656    private abstract record BaseEnc(string KeyAlg, string EncAlg) : IEncConfig
 657    {
 1658        protected string KeyAlgMapped => Map.KeyAlg[KeyAlg];
 1659        protected string EncAlgMapped => Map.EncAlg[EncAlg];
 660        public abstract EncryptingCredentials ToEncryptingCreds();
 661    }
 662
 0663    private sealed record CertEncrypt(X509Certificate2 Cert, string KeyAlg, string EncAlg) : BaseEnc(KeyAlg, EncAlg)
 664    {
 665        public override EncryptingCredentials ToEncryptingCreds()
 666        {
 0667            var key = new X509SecurityKey(Cert);
 0668            return new EncryptingCredentials(key, KeyAlgMapped, EncAlgMapped);
 669        }
 670    }
 671
 2672    private sealed record RsaEncrypt(string Pem, string KeyAlg, string EncAlg) : BaseEnc(KeyAlg, EncAlg)
 673    {
 674        public override EncryptingCredentials ToEncryptingCreds()
 675        {
 2676            var rsa = RSA.Create(); rsa.ImportFromPem(Pem);
 1677            var key = new RsaSecurityKey(rsa);
 1678            return new EncryptingCredentials(key, KeyAlgMapped, EncAlgMapped);
 679        }
 680    }
 681
 682    private sealed record SymmetricEncrypt(
 6683     string B64,
 684     string KeyAlg,
 3685     string EncAlg) : BaseEnc(KeyAlg, EncAlg)
 686    {
 687        public override EncryptingCredentials ToEncryptingCreds()
 688        {
 3689            if (Log.IsEnabled(LogEventLevel.Debug))
 690            {
 3691                Log.Debug(
 3692                    "Encrypting with {KeyAlg} and {EncAlg} ({Bits} bits)",
 3693                    KeyAlg, EncAlg, Base64UrlEncoder.DecodeBytes(B64).Length * 8);
 694            }
 695            // ────────── shared-secret → SymmetricSecurityKey ──────────
 3696            if (!Map.KeyAlg.ContainsKey(KeyAlg))
 697            {
 0698                throw new ArgumentException($"Unknown key algorithm: {KeyAlg}");
 699            }
 700
 3701            var key = new SymmetricSecurityKey(Base64UrlEncoder.DecodeBytes(B64));
 3702            var bits = key.KeySize;                        // 128 / 192 / 256 / 384 / 512 …
 703
 704            // ────────── auto-pick encAlg for 'dir' default case ───────
 3705            var encEff = EncAlg;
 706
 3707            if (KeyAlg.Equals("dir", StringComparison.OrdinalIgnoreCase) &&
 3708                EncAlg.Equals("A256CBC-HS512", StringComparison.OrdinalIgnoreCase))
 709            {
 0710                encEff = bits switch
 0711                {
 0712                    128 => "A128GCM",
 0713                    192 => "A192GCM",
 0714                    256 => "A256GCM",
 0715                    384 => "A192CBC-HS384",
 0716                    512 => "A256CBC-HS512",
 0717                    _ => throw new ArgumentException(
 0718                               $"Unsupported key size {bits} bits for direct encryption.")
 0719                };
 720            }
 721
 722            // ────────── hard validation (caller may specify any enc) ──
 723            static void Require(int actualBits, int requiredBits, string alg)
 724            {
 3725                _ = actualBits == requiredBits
 3726                    ? true
 3727                    : throw new ArgumentException($"{alg} requires a {requiredBits}-bit key.");
 2728            }
 729
 3730            switch (encEff.ToUpperInvariant())
 731            {
 2732                case "A128GCM": Require(bits, 128, encEff); break;
 0733                case "A192GCM": Require(bits, 192, encEff); break;
 1734                case "A256GCM": Require(bits, 256, encEff); break;
 2735                case "A128CBC-HS256": Require(bits, 256, encEff); break;
 0736                case "A192CBC-HS384": Require(bits, 384, encEff); break;
 0737                case "A256CBC-HS512": Require(bits, 512, encEff); break;
 738                default:
 0739                    throw new ArgumentException($"Unknown or unsupported enc algorithm: {encEff}");
 740            }
 2741            if (Log.IsEnabled(LogEventLevel.Debug))
 742            {
 2743                Log.Debug(
 2744                    "Encrypting with {KeyAlg} and {EncAlg} ({Bits} bits)",
 2745                    KeyAlg, encEff, bits);
 746            }
 747            // ────────── build EncryptingCredentials ───────────────────
 2748            return new EncryptingCredentials(
 2749                key,
 2750                Map.KeyAlg[KeyAlg.ToUpperInvariant()],          // 'dir', 'A256KW', …
 2751                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    {
 2767        if (ctx.Request.Authorization == null || (!ctx.Request.Authorization?.StartsWith("Bearer ") ?? true))
 768        {
 1769            return string.Empty;
 770        }
 1771        var authHeader = ctx.Request.Authorization;
 1772        var strToken = authHeader != null ? authHeader["Bearer ".Length..].Trim() : throw new UnauthorizedAccessExceptio
 1773        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    {
 2786        var handler = new JwtSecurityTokenHandler();
 787
 788        // Read raw token (no mapping, no validation)
 2789        var old = handler.ReadJwtToken(jwt);
 2790        var _builder = CloneBuilder();
 791        // Copy all non-time claims
 2792        var reserved = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
 2793        { "exp", "nbf", "iat"  };
 794
 15795        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)
 4798        if (!claims.Any(c => c.Type == JwtRegisteredClaimNames.Sub))
 799        {
 0800            var sub = old.Claims.FirstOrDefault(c =>
 0801                         c.Type is JwtRegisteredClaimNames.Sub or
 0802                         ClaimTypes.NameIdentifier)?.Value;
 0803            if (!string.IsNullOrEmpty(sub))
 804            {
 0805                claims.Add(new Claim(JwtRegisteredClaimNames.Sub, sub));
 806            }
 807        }
 808
 2809        var signCreds = BuildSigningCredentials(out _issuerSigningKey) ?? throw new InvalidOperationException("No signin
 2810        Algorithm = signCreds.Algorithm;
 2811        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'
 2815        if (_nbf < DateTime.UtcNow)
 816        {
 2817            _nbf = DateTime.UtcNow;
 818        }
 2819        if (lifetime is null)
 820        {
 0821            lifetime = _lifetime;
 822        }
 2823        else if (lifetime < TimeSpan.Zero)
 824        {
 0825            throw new ArgumentOutOfRangeException(nameof(lifetime), "Lifetime must be a positive TimeSpan.");
 826        }
 2827        var token = handler.CreateJwtSecurityToken(
 2828                issuer: _issuer,
 2829                audience: _aud,
 2830                subject: new ClaimsIdentity(claims),
 2831                notBefore: _nbf,
 2832                expires: _nbf.Add((TimeSpan)lifetime),
 2833                issuedAt: DateTime.UtcNow,
 2834                signingCredentials: signCreds,
 2835                encryptingCredentials: encCreds);
 836
 4837        foreach (var kv in _header)
 838        {
 0839            token.Header[kv.Key] = kv.Value;
 840        }
 841
 2842        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}

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_B64u()
get_Pem()
get_Cert()
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)
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)
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)
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_B64()
.ctor(System.String,System.String,System.String)
ToEncryptingCreds()
Require()
RenewJwt(Kestrun.Hosting.KestrunContext,System.Nullable`1<System.TimeSpan>)
RenewJwt(System.String,System.Nullable`1<System.TimeSpan>)