< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Hosting.KestrunHostAuthnExtensions
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostAuthnExtensions.cs
Tag: Kestrun/Kestrun@0d738bf294e6281b936d031e1979d928007495ff
Line coverage
51%
Covered lines: 342
Uncovered lines: 316
Coverable lines: 658
Total lines: 1558
Line coverage: 51.9%
Branch coverage
45%
Covered branches: 134
Total branches: 296
Branch coverage: 45.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 09/12/2025 - 16:20:13 Line coverage: 78.5% (249/317) Branch coverage: 76.3% (84/110) Total lines: 731 Tag: Kestrun/Kestrun@bd014be0a15f3c9298922d2ff67068869adda2a009/15/2025 - 19:16:35 Line coverage: 79% (256/324) Branch coverage: 75.8% (85/112) Total lines: 760 Tag: Kestrun/Kestrun@bfb58693b9baaed61644ace5b29e014d9ffacbc909/16/2025 - 04:01:29 Line coverage: 79.3% (254/320) Branch coverage: 75.8% (85/112) Total lines: 763 Tag: Kestrun/Kestrun@e5263347b0baba68d9fd62ffbf60a7dd87f994bb10/13/2025 - 16:52:37 Line coverage: 79.3% (254/320) Branch coverage: 75.8% (85/112) Total lines: 765 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 52.2% (269/515) Branch coverage: 47.4% (92/194) Total lines: 1164 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 51.8% (341/658) Branch coverage: 44.9% (133/296) Total lines: 1558 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 51.9% (342/658) Branch coverage: 45.2% (134/296) Total lines: 1558 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c8330 09/12/2025 - 16:20:13 Line coverage: 78.5% (249/317) Branch coverage: 76.3% (84/110) Total lines: 731 Tag: Kestrun/Kestrun@bd014be0a15f3c9298922d2ff67068869adda2a009/15/2025 - 19:16:35 Line coverage: 79% (256/324) Branch coverage: 75.8% (85/112) Total lines: 760 Tag: Kestrun/Kestrun@bfb58693b9baaed61644ace5b29e014d9ffacbc909/16/2025 - 04:01:29 Line coverage: 79.3% (254/320) Branch coverage: 75.8% (85/112) Total lines: 763 Tag: Kestrun/Kestrun@e5263347b0baba68d9fd62ffbf60a7dd87f994bb10/13/2025 - 16:52:37 Line coverage: 79.3% (254/320) Branch coverage: 75.8% (85/112) Total lines: 765 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e11/19/2025 - 02:25:56 Line coverage: 52.2% (269/515) Branch coverage: 47.4% (92/194) Total lines: 1164 Tag: Kestrun/Kestrun@98ff905e5605a920343154665980a71211a03c6d12/12/2025 - 17:27:19 Line coverage: 51.8% (341/658) Branch coverage: 44.9% (133/296) Total lines: 1558 Tag: Kestrun/Kestrun@826bf9dcf9db118c5de4c78a3259bce9549f0dcd12/15/2025 - 02:23:46 Line coverage: 51.9% (342/658) Branch coverage: 45.2% (134/296) Total lines: 1558 Tag: Kestrun/Kestrun@7a3839f4de2254e22daae81ab8dc7cb2f40c8330

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
AddBasicAuthentication(...)100%66100%
AddBasicAuthentication(...)100%44100%
ConfigureBasicAuthValidators(...)81.25%171684.21%
ConfigureBasicIssueClaims(...)81.25%171684.21%
AddGitHubOAuthAuthentication(...)0%2040%
ConfigureGitHubOAuth2Options(...)100%210%
ConfigureGitHubClaimMappings(...)100%210%
FetchGitHubUserInfoAsync()100%210%
EnrichGitHubEmailClaimAsync()0%110100%
FindPrimaryVerifiedEmail(...)0%110100%
FindFirstVerifiedEmail(...)0%7280%
AddJwtBearerAuthentication(...)100%6694.73%
AddJwtBearerAuthentication(...)75%4494.73%
AddCookieAuthentication(...)100%6695.23%
AddCookieAuthentication(...)0%2040%
AddWindowsAuthentication(...)75%4485.71%
AddWindowsAuthentication(...)0%2040%
AddWindowsAuthentication(...)100%11100%
AddApiKeyAuthentication(...)100%66100%
AddApiKeyAuthentication(...)100%44100%
ConfigureApiKeyValidators(...)81.25%171684.21%
ConfigureApiKeyIssueClaims(...)81.25%171684.21%
AddOAuth2Authentication(...)0%600240%
ConfigureScopes(...)83.33%6690.9%
BuildClaimPolicyFromScopes(...)100%22100%
AddMissingScopesToClaimPolicy(...)75%4491.66%
BackfillScopesFromClaimPolicy(...)66.66%6683.33%
LogScopeAdded(...)100%22100%
LogScopeAddedToClaimPolicy(...)100%22100%
LogMissingScopes(...)100%22100%
LogConfiguredScopes(...)100%22100%
LogClaimPolicyConfigured(...)100%22100%
AddOpenIdConnectAuthentication(...)0%1056320%
GetSupportedScopes(...)0%342180%
ConfigureOpenApi(...)100%66100%
AddAuthentication(...)81.25%161694%
HasAuthScheme(...)100%11100%
AddAuthorization(...)50%22100%
HasAuthPolicy(...)100%11100%
.ctor(...)0%620%
get_LastTokenResponseBody()100%210%
SendAsync()0%812280%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Hosting/KestrunHostAuthnExtensions.cs

#LineLine coverage
 1using Microsoft.AspNetCore.Authentication;
 2using Microsoft.AspNetCore.Authentication.Cookies;
 3using Microsoft.AspNetCore.Authorization;
 4using Microsoft.Extensions.Options;
 5using Kestrun.Authentication;
 6using Serilog.Events;
 7using Kestrun.Scripting;
 8using Microsoft.AspNetCore.Authentication.Negotiate;
 9using Kestrun.Claims;
 10using Microsoft.AspNetCore.Authentication.OAuth;
 11using System.Text.Json;
 12using System.Security.Claims;
 13using Microsoft.Extensions.DependencyInjection.Extensions;
 14using Microsoft.AspNetCore.Authentication.OpenIdConnect;
 15using Microsoft.IdentityModel.Protocols;
 16using Microsoft.IdentityModel.Protocols.OpenIdConnect;
 17
 18namespace Kestrun.Hosting;
 19
 20/// <summary>
 21/// Provides extension methods for adding authentication schemes to the Kestrun host.
 22/// </summary>
 23public static class KestrunHostAuthnExtensions
 24{
 25    #region Basic Authentication
 26    /// <summary>
 27    /// Adds Basic Authentication to the Kestrun host.
 28    /// <para>Use this for simple username/password authentication.</para>
 29    /// </summary>
 30    /// <param name="host">The Kestrun host instance.</param>
 31    /// <param name="scheme">The authentication scheme name (e.g. "Basic").</param>
 32    /// <param name="displayName">The display name for the authentication scheme.</param>
 33    /// <param name="configure">Optional configuration for BasicAuthenticationOptions.</param>
 34    /// <returns>returns the KestrunHost instance.</returns>
 35    public static KestrunHost AddBasicAuthentication(
 36    this KestrunHost host,
 37    string scheme = AuthenticationDefaults.BasicSchemeName,
 38    string? displayName = AuthenticationDefaults.BasicDisplayName,
 39    Action<BasicAuthenticationOptions>? configure = null
 40    )
 41    {
 42        // Build a prototype options instance (single source of truth)
 843        var prototype = new BasicAuthenticationOptions { Host = host };
 44
 45        // Let the caller mutate the prototype
 846        configure?.Invoke(prototype);
 47
 48        // Configure validators / claims / OpenAPI on the prototype
 849        ConfigureBasicAuthValidators(host, prototype);
 850        ConfigureBasicIssueClaims(host, prototype);
 851        ConfigureOpenApi(host, scheme, prototype);
 52        // register in host for introspection
 853        _ = host.RegisteredAuthentications.Register(scheme, AuthenticationType.Basic, prototype);
 854        var h = host.AddAuthentication(
 855           defaultScheme: scheme,
 856           buildSchemes: ab =>
 857           {
 858               _ = ab.AddScheme<BasicAuthenticationOptions, BasicAuthHandler>(
 859                   authenticationScheme: scheme,
 860                   displayName: displayName,
 861                   configureOptions: opts =>
 862                   {
 863                       // Copy from the prototype into the runtime instance
 664                       prototype.ApplyTo(opts);
 865
 666                       host.Logger.Debug("Configured Basic Authentication using scheme {Scheme}", scheme);
 1467                   });
 868           }
 869       );
 70        //  register the post-configurer **after** the scheme so it can
 71        //    read BasicAuthenticationOptions for <scheme>
 872        return h.AddService(services =>
 873        {
 874            _ = services.AddSingleton<IPostConfigureOptions<AuthorizationOptions>>(
 1175                sp => new ClaimPolicyPostConfigurer(
 1176                          scheme,
 1177                          sp.GetRequiredService<
 1178                              IOptionsMonitor<BasicAuthenticationOptions>>()));
 1679        });
 80    }
 81
 82    /// <summary>
 83    /// Adds Basic Authentication to the Kestrun host using the provided options object.
 84    /// </summary>
 85    /// <param name="host">The Kestrun host instance.</param>
 86    /// <param name="scheme">The authentication scheme name (e.g. "Basic").</param>
 87    /// <param name="displayName">The display name for the authentication scheme.</param>
 88    /// <param name="configure">The BasicAuthenticationOptions object to configure the authentication.</param>
 89    /// <returns>The configured KestrunHost instance.</returns>
 90    public static KestrunHost AddBasicAuthentication(
 91        this KestrunHost host,
 92        string scheme,
 93        string? displayName,
 94        BasicAuthenticationOptions configure
 95        )
 96    {
 197        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 98        {
 199            host.Logger.Debug("Adding Basic Authentication with scheme: {Scheme}", scheme);
 100        }
 101        // Ensure the scheme is not null
 1102        ArgumentNullException.ThrowIfNull(host);
 1103        ArgumentNullException.ThrowIfNull(scheme);
 1104        ArgumentNullException.ThrowIfNull(configure);
 105        // Ensure host is set
 1106        if (configure.Host != host)
 107        {
 1108            configure.Host = host;
 109        }
 1110        return host.AddBasicAuthentication(
 1111            scheme: scheme,
 1112            displayName: displayName,
 1113            configure: configure.ApplyTo
 1114        );
 115    }
 116
 117    /// <summary>
 118    /// Configures the validators for Basic authentication.
 119    /// </summary>
 120    /// <param name="host">The Kestrun host instance.</param>
 121    /// <param name="opts">The options to configure.</param>
 122    private static void ConfigureBasicAuthValidators(KestrunHost host, BasicAuthenticationOptions opts)
 123    {
 8124        var settings = opts.ValidateCodeSettings;
 8125        if (string.IsNullOrWhiteSpace(settings.Code))
 126        {
 5127            return;
 128        }
 129
 3130        switch (settings.Language)
 131        {
 132            case ScriptLanguage.PowerShell:
 1133                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 134                {
 1135                    opts.Logger.Debug("Building PowerShell validator for Basic authentication");
 136                }
 137
 1138                opts.ValidateCredentialsAsync = BasicAuthHandler.BuildPsValidator(host, settings);
 1139                break;
 140            case ScriptLanguage.CSharp:
 1141                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 142                {
 1143                    opts.Logger.Debug("Building C# validator for Basic authentication");
 144                }
 145
 1146                opts.ValidateCredentialsAsync = BasicAuthHandler.BuildCsValidator(host, settings);
 1147                break;
 148            case ScriptLanguage.VBNet:
 1149                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 150                {
 1151                    opts.Logger.Debug("Building VB.NET validator for Basic authentication");
 152                }
 153
 1154                opts.ValidateCredentialsAsync = BasicAuthHandler.BuildVBNetValidator(host, settings);
 1155                break;
 156            default:
 0157                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 158                {
 0159                    opts.Logger.Warning("No valid language specified for Basic authentication");
 160                }
 161                break;
 162        }
 0163    }
 164
 165    /// <summary>
 166    /// Configures the issue claims for Basic authentication.
 167    /// </summary>
 168    /// <param name="host">The Kestrun host instance.</param>
 169    /// <param name="opts">The options to configure.</param>
 170    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 171    private static void ConfigureBasicIssueClaims(KestrunHost host, BasicAuthenticationOptions opts)
 172    {
 8173        var settings = opts.IssueClaimsCodeSettings;
 8174        if (string.IsNullOrWhiteSpace(settings.Code))
 175        {
 5176            return;
 177        }
 178
 3179        switch (settings.Language)
 180        {
 181            case ScriptLanguage.PowerShell:
 1182                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 183                {
 1184                    opts.Logger.Debug("Building PowerShell Issue Claims for API Basic authentication");
 185                }
 186
 1187                opts.IssueClaims = IAuthHandler.BuildPsIssueClaims(host, settings);
 1188                break;
 189            case ScriptLanguage.CSharp:
 1190                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 191                {
 1192                    opts.Logger.Debug("Building C# Issue Claims for API Basic authentication");
 193                }
 194
 1195                opts.IssueClaims = IAuthHandler.BuildCsIssueClaims(host, settings);
 1196                break;
 197            case ScriptLanguage.VBNet:
 1198                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 199                {
 1200                    opts.Logger.Debug("Building VB.NET Issue Claims for API Basic authentication");
 201                }
 202
 1203                opts.IssueClaims = IAuthHandler.BuildVBNetIssueClaims(host, settings);
 1204                break;
 205            default:
 0206                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 207                {
 0208                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 209                }
 0210                throw new NotSupportedException("Unsupported language");
 211        }
 212    }
 213
 214    #endregion
 215    #region GitHub OAuth Authentication
 216    /// <summary>
 217    /// Adds GitHub OAuth (Authorization Code) authentication with optional email enrichment.
 218    /// Creates three schemes: <paramref name="scheme"/>, <paramref name="scheme"/>.Cookies, <paramref name="scheme"/>.P
 219    /// </summary>
 220    /// <param name="host">The Kestrun host instance.</param>
 221    /// <param name="scheme">Base scheme name (e.g. "GitHub").</param>
 222    /// <param name="displayName">The display name for the authentication scheme.</param>
 223    /// <param name="documentationId">Documentation IDs for the authentication scheme.</param>
 224    /// <param name="description">A description of the authentication scheme.</param>
 225    /// <param name="clientId">GitHub OAuth App Client ID.</param>
 226    /// <param name="clientSecret">GitHub OAuth App Client Secret.</param>
 227    /// <param name="callbackPath">The callback path for OAuth redirection (e.g. "/signin-github").</param>
 228    /// <returns>The configured KestrunHost.</returns>
 229    public static KestrunHost AddGitHubOAuthAuthentication(
 230        this KestrunHost host,
 231        string scheme,
 232        string? displayName,
 233        string[]? documentationId,
 234        string? description,
 235        string clientId,
 236        string clientSecret,
 237        string callbackPath)
 238    {
 0239        var opts = ConfigureGitHubOAuth2Options(host, clientId, clientSecret, callbackPath);
 0240        ConfigureGitHubClaimMappings(opts);
 0241        opts.DocumentationId = documentationId ?? [];
 0242        if (!string.IsNullOrWhiteSpace(description))
 243        {
 0244            opts.Description = description;
 245        }
 0246        opts.Events = new OAuthEvents
 0247        {
 0248            OnCreatingTicket = async context =>
 0249            {
 0250                await FetchGitHubUserInfoAsync(context);
 0251                await EnrichGitHubEmailClaimAsync(context, host);
 0252            }
 0253        };
 0254        return host.AddOAuth2Authentication(scheme, displayName, opts);
 255    }
 256
 257    /// <summary>
 258    /// Configures OAuth2Options for GitHub authentication.
 259    /// </summary>
 260    /// <param name="host">The Kestrun host instance.</param>
 261    /// <param name="clientId">GitHub OAuth App Client ID.</param>
 262    /// <param name="clientSecret">GitHub OAuth App Client Secret.</param>
 263    /// <param name="callbackPath">The callback path for OAuth redirection (e.g. "/signin-github").</param>
 264    /// <returns>The configured OAuth2Options.</returns>
 265    private static OAuth2Options ConfigureGitHubOAuth2Options(KestrunHost host, string clientId, string clientSecret, st
 266    {
 0267        return new OAuth2Options()
 0268        {
 0269            Host = host,
 0270            ClientId = clientId,
 0271            ClientSecret = clientSecret,
 0272            CallbackPath = callbackPath,
 0273            AuthorizationEndpoint = "https://github.com/login/oauth/authorize",
 0274            TokenEndpoint = "https://github.com/login/oauth/access_token",
 0275            UserInformationEndpoint = "https://api.github.com/user",
 0276            SaveTokens = true,
 0277            SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
 0278            Scope = { "read:user", "user:email" }
 0279        };
 280    }
 281
 282    /// <summary>
 283    /// Configures claim mappings for GitHub OAuth2Options.
 284    /// </summary>
 285    /// <param name="opts">The OAuth2Options to configure.</param>
 286    private static void ConfigureGitHubClaimMappings(OAuth2Options opts)
 287    {
 0288        opts.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
 0289        opts.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
 0290        opts.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
 0291        opts.ClaimActions.MapJsonKey("name", "name");
 0292        opts.ClaimActions.MapJsonKey("urn:github:login", "login");
 0293        opts.ClaimActions.MapJsonKey("urn:github:avatar_url", "avatar_url");
 0294        opts.ClaimActions.MapJsonKey("urn:github:html_url", "html_url");
 0295    }
 296
 297    /// <summary>
 298    /// Fetches GitHub user information and adds claims to the identity.
 299    /// </summary>
 300    /// <param name="context">The OAuthCreatingTicketContext.</param>
 301    /// <returns>A task representing the asynchronous operation.</returns>
 302    private static async Task FetchGitHubUserInfoAsync(OAuthCreatingTicketContext context)
 303    {
 0304        using var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
 0305        request.Headers.Accept.Add(new("application/json"));
 0306        request.Headers.Add("User-Agent", "KestrunOAuth/1.0");
 0307        request.Headers.Authorization = new("Bearer", context.AccessToken);
 308
 0309        using var response = await context.Backchannel.SendAsync(request,
 0310            HttpCompletionOption.ResponseHeadersRead,
 0311            context.HttpContext.RequestAborted);
 312
 0313        _ = response.EnsureSuccessStatusCode();
 314
 0315        using var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync(context.HttpContext.RequestAborted)
 0316        context.RunClaimActions(user.RootElement);
 0317    }
 318
 319    /// <summary>
 320    /// Fetches GitHub user emails and enriches the identity with the primary verified email claim.
 321    /// </summary>
 322    /// <param name="context">The OAuthCreatingTicketContext.</param>
 323    /// <param name="host">The KestrunHost instance for logging.</param>
 324    /// <returns>A task representing the asynchronous operation.</returns>
 325    private static async Task EnrichGitHubEmailClaimAsync(OAuthCreatingTicketContext context, KestrunHost host)
 326    {
 0327        if (context.Identity is null || context.Identity.HasClaim(c => c.Type == ClaimTypes.Email))
 328        {
 0329            return;
 330        }
 331
 332        try
 333        {
 0334            using var emailRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails");
 0335            emailRequest.Headers.Accept.Add(new("application/json"));
 0336            emailRequest.Headers.Add("User-Agent", "KestrunOAuth/1.0");
 0337            emailRequest.Headers.Authorization = new("Bearer", context.AccessToken);
 338
 0339            using var emailResponse = await context.Backchannel.SendAsync(emailRequest,
 0340                HttpCompletionOption.ResponseHeadersRead,
 0341                context.HttpContext.RequestAborted);
 342
 0343            if (!emailResponse.IsSuccessStatusCode)
 344            {
 0345                return;
 346            }
 347
 0348            using var emails = JsonDocument.Parse(await emailResponse.Content.ReadAsStringAsync(context.HttpContext.Requ
 0349            var primaryEmail = FindPrimaryVerifiedEmail(emails) ?? FindFirstVerifiedEmail(emails);
 350
 0351            if (!string.IsNullOrWhiteSpace(primaryEmail))
 352            {
 0353                context.Identity.AddClaim(new Claim(
 0354                    ClaimTypes.Email,
 0355                    primaryEmail,
 0356                    ClaimValueTypes.String,
 0357                    context.Options.ClaimsIssuer));
 358            }
 0359        }
 0360        catch (Exception ex)
 361        {
 0362            host.Logger.Verbose(exception: ex, messageTemplate: "Failed to enrich GitHub email claim.");
 0363        }
 0364    }
 365
 366    /// <summary>
 367    /// Finds the primary verified email from the GitHub emails JSON document.
 368    /// </summary>
 369    /// <param name="emails">The JSON document containing GitHub emails.</param>
 370    /// <returns>The primary verified email if found; otherwise, null.</returns>
 371    private static string? FindPrimaryVerifiedEmail(JsonDocument emails)
 372    {
 0373        foreach (var emailObj in emails.RootElement.EnumerateArray())
 374        {
 0375            var isPrimary = emailObj.TryGetProperty("primary", out var primaryProp) && primaryProp.GetBoolean();
 0376            var isVerified = emailObj.TryGetProperty("verified", out var verifiedProp) && verifiedProp.GetBoolean();
 377
 0378            if (isPrimary && isVerified && emailObj.TryGetProperty("email", out var emailProp))
 379            {
 0380                return emailProp.GetString();
 381            }
 382        }
 0383        return null;
 0384    }
 385
 386    /// <summary>
 387    /// Finds the primary verified email from the GitHub emails JSON document.
 388    /// </summary>
 389    /// <param name="emails">The JSON document containing GitHub emails.</param>
 390    /// <returns>The primary verified email if found; otherwise, null.</returns>
 391    private static string? FindFirstVerifiedEmail(JsonDocument emails)
 392    {
 0393        foreach (var emailObj in emails.RootElement.EnumerateArray())
 394        {
 0395            var isVerified = emailObj.TryGetProperty("verified", out var verifiedProp) && verifiedProp.GetBoolean();
 0396            if (isVerified && emailObj.TryGetProperty("email", out var emailProp))
 397            {
 0398                return emailProp.GetString();
 399            }
 400        }
 0401        return null;
 0402    }
 403
 404    #endregion
 405    #region JWT Bearer Authentication
 406    /// <summary>
 407    /// Adds JWT Bearer authentication to the Kestrun host.
 408    /// <para>Use this for APIs that require token-based authentication.</para>
 409    /// </summary>
 410    /// <param name="host">The Kestrun host instance.</param>
 411    /// <param name="authenticationScheme">The authentication scheme name (e.g. "Bearer").</param>
 412    /// <param name="displayName">The display name for the authentication scheme.</param>
 413    /// <param name="configureOptions">Optional configuration for JwtAuthOptions.</param>
 414    /// <example>
 415    /// HS512 (HMAC-SHA-512, symmetric)
 416    /// </example>
 417    /// <code>
 418    ///     var hmacKey = new SymmetricSecurityKey(
 419    ///         Encoding.UTF8.GetBytes("32-bytes-or-more-secret……"));
 420    ///     host.AddJwtBearerAuthentication(
 421    ///         scheme:          "Bearer",
 422    ///         issuer:          "KestrunApi",
 423    ///         audience:        "KestrunClients",
 424    ///         validationKey:   hmacKey,
 425    ///         validAlgorithms: new[] { SecurityAlgorithms.HmacSha512 });
 426    /// </code>
 427    /// <example>
 428    /// RS256 (RSA-SHA-256, asymmetric)
 429    /// <para>Requires a PEM-encoded private key file.</para>
 430    /// <code>
 431    ///    using var rsa = RSA.Create();
 432    ///     rsa.ImportFromPem(File.ReadAllText("private-key.pem"));
 433    ///     var rsaKey = new RsaSecurityKey(rsa);
 434    ///
 435    ///     host.AddJwtBearerAuthentication(
 436    ///         scheme:          "Rs256",
 437    ///         issuer:          "KestrunApi",
 438    ///         audience:        "KestrunClients",
 439    ///         validationKey:   rsaKey,
 440    ///         validAlgorithms: new[] { SecurityAlgorithms.RsaSha256 });
 441    /// </code>
 442    /// </example>
 443    /// <example>
 444    /// ES256 (ECDSA-SHA-256, asymmetric)
 445    /// <para>Requires a PEM-encoded private key file.</para>
 446    /// <code>
 447    ///     using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
 448    ///     var esKey = new ECDsaSecurityKey(ecdsa);
 449    ///     host.AddJwtBearerAuthentication(
 450    ///         "Es256", "KestrunApi", "KestrunClients",
 451    ///         esKey, new[] { SecurityAlgorithms.EcdsaSha256 });
 452    /// </code>
 453    /// </example>
 454    /// <returns></returns>
 455    public static KestrunHost AddJwtBearerAuthentication(
 456      this KestrunHost host,
 457      string authenticationScheme = AuthenticationDefaults.JwtBearerSchemeName,
 458      string? displayName = AuthenticationDefaults.JwtBearerDisplayName,
 459      Action<JwtAuthOptions>? configureOptions = null)
 460    {
 3461        ArgumentNullException.ThrowIfNull(configureOptions);
 462        // Build a prototype options instance (single source of truth)
 3463        var prototype = new JwtAuthOptions { Host = host };
 3464        configureOptions?.Invoke(prototype);
 3465        ConfigureOpenApi(host, authenticationScheme, prototype);
 466
 467        // register in host for introspection
 3468        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Bearer, prototype);
 469
 3470        return host.AddAuthentication(
 3471            defaultScheme: authenticationScheme,
 3472            buildSchemes: ab =>
 3473            {
 3474                _ = ab.AddJwtBearer(
 3475                    authenticationScheme: authenticationScheme,
 3476                    displayName: displayName,
 3477                    configureOptions: opts =>
 3478                {
 0479                    prototype.ApplyTo(opts);
 3480                });
 3481            },
 3482            configureAuthz: prototype.ClaimPolicy?.ToAuthzDelegate()
 3483            );
 484    }
 485
 486    /// <summary>
 487    /// Adds JWT Bearer authentication to the Kestrun host using the provided options object.
 488    /// </summary>
 489    /// <param name="host">The Kestrun host instance.</param>
 490    /// <param name="authenticationScheme">The authentication scheme name.</param>
 491    /// <param name="displayName">The display name for the authentication scheme.</param>
 492    /// <param name="configureOptions">Optional configuration for JwtAuthOptions.</param>
 493    /// <returns>The configured KestrunHost instance.</returns>
 494    public static KestrunHost AddJwtBearerAuthentication(
 495        this KestrunHost host,
 496        string authenticationScheme = AuthenticationDefaults.JwtBearerSchemeName,
 497        string? displayName = AuthenticationDefaults.JwtBearerDisplayName,
 498        JwtAuthOptions? configureOptions = null)
 499    {
 3500        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 501        {
 3502            host.Logger.Debug("Adding Jwt Bearer Authentication with scheme: {Scheme}", authenticationScheme);
 503        }
 504        // Ensure the scheme is not null
 3505        ArgumentNullException.ThrowIfNull(host);
 3506        ArgumentNullException.ThrowIfNull(authenticationScheme);
 3507        ArgumentNullException.ThrowIfNull(configureOptions);
 508
 509        // Ensure host is set
 3510        if (configureOptions.Host != host)
 511        {
 0512            configureOptions.Host = host;
 513        }
 514
 3515        return host.AddJwtBearerAuthentication(
 3516            authenticationScheme: authenticationScheme,
 3517              displayName: displayName,
 3518              configureOptions: opts =>
 3519              {
 3520                  // Copy relevant properties from provided options instance to the framework-created one
 3521                  configureOptions.ApplyTo(opts);
 3522                  host.Logger.Debug(
 3523                           "Configured JWT Authentication using scheme {Scheme}.",
 3524                           authenticationScheme);
 3525              }
 3526            );
 527    }
 528    #endregion
 529    #region Cookie Authentication
 530    /// <summary>
 531    /// Adds Cookie Authentication to the Kestrun host.
 532    /// <para>Use this for browser-based authentication using cookies.</para>
 533    /// </summary>
 534    /// <param name="host">The Kestrun host instance.</param>
 535    /// <param name="authenticationScheme">The authentication scheme name (default is CookieAuthenticationDefaults.Authe
 536    /// <param name="displayName">The display name for the authentication scheme.</param>
 537    /// <param name="configureOptions">Optional configuration for CookieAuthenticationOptions.</param>
 538    /// <param name="claimPolicy">Optional authorization policy configuration.</param>
 539    /// <returns>The configured KestrunHost instance.</returns>
 540    public static KestrunHost AddCookieAuthentication(
 541        this KestrunHost host,
 542        string authenticationScheme = AuthenticationDefaults.CookiesSchemeName,
 543        string? displayName = AuthenticationDefaults.CookiesDisplayName,
 544        Action<CookieAuthOptions>? configureOptions = null,
 545     ClaimPolicyConfig? claimPolicy = null)
 546    {
 547        // Build a prototype options instance (single source of truth)
 2548        var prototype = new CookieAuthOptions { Host = host };
 2549        configureOptions?.Invoke(prototype);
 2550        ConfigureOpenApi(host, authenticationScheme, prototype);
 551
 552        // register in host for introspection
 2553        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Cookie, prototype);
 554
 555        // Add authentication
 2556        return host.AddAuthentication(
 2557            defaultScheme: authenticationScheme,
 2558            buildSchemes: ab =>
 2559            {
 2560                _ = ab.AddCookie(
 2561                    authenticationScheme: authenticationScheme,
 2562                    displayName: displayName,
 2563                    configureOptions: opts =>
 2564                    {
 2565                        // Copy everything from the prototype into the real options instance
 0566                        prototype.ApplyTo(opts);
 2567                        // let caller mutate everything first
 2568                        //configure?.Invoke(opts);
 2569                    });
 2570            },
 2571            configureAuthz: claimPolicy?.ToAuthzDelegate()
 2572        );
 573    }
 574
 575    /// <summary>
 576    /// Adds Cookie Authentication to the Kestrun host using the provided options object.
 577    /// </summary>
 578    /// <param name="host">The Kestrun host instance.</param>
 579    /// <param name="authenticationScheme">The authentication scheme name (default is CookieAuthenticationDefaults.Authe
 580    /// <param name="displayName">The display name for the authentication scheme.</param>
 581    /// <param name="configureOptions">The CookieAuthenticationOptions object to configure the authentication.</param>
 582    /// <param name="claimPolicy">Optional authorization policy configuration.</param>
 583    /// <returns>The configured KestrunHost instance.</returns>
 584    public static KestrunHost AddCookieAuthentication(
 585          this KestrunHost host,
 586          string authenticationScheme = AuthenticationDefaults.CookiesSchemeName,
 587          string? displayName = AuthenticationDefaults.CookiesDisplayName,
 588          CookieAuthOptions? configureOptions = null,
 589       ClaimPolicyConfig? claimPolicy = null)
 590    {
 0591        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 592        {
 0593            host.Logger.Debug("Adding Cookie Authentication with scheme: {Scheme}", authenticationScheme);
 594        }
 595        // Ensure the scheme is not null
 0596        ArgumentNullException.ThrowIfNull(host);
 0597        ArgumentNullException.ThrowIfNull(authenticationScheme);
 0598        ArgumentNullException.ThrowIfNull(configureOptions);
 599        // Ensure host is set
 0600        if (configureOptions.Host != host)
 601        {
 0602            configureOptions.Host = host;
 603        }
 604        // Copy relevant properties from provided options instance to the framework-created one
 0605        return host.AddCookieAuthentication(
 0606            authenticationScheme: authenticationScheme,
 0607            displayName: displayName,
 0608            configureOptions: configureOptions.ApplyTo,
 0609            claimPolicy: claimPolicy
 0610        );
 611    }
 612    #endregion
 613
 614    /*
 615        public static KestrunHost AddClientCertificateAuthentication(
 616            this KestrunHost host,
 617            string scheme = CertificateAuthenticationDefaults.AuthenticationScheme,
 618            Action<CertificateAuthenticationOptions>? configure = null,
 619            Action<AuthorizationOptions>? configureAuthz = null)
 620        {
 621            return host.AddAuthentication(
 622                defaultScheme: scheme,
 623                buildSchemes: ab =>
 624                {
 625                    ab.AddCertificate(
 626                        authenticationScheme: scheme,
 627                        configureOptions: configure ?? (opts => { }));
 628                },
 629                configureAuthz: configureAuthz
 630            );
 631        }
 632    */
 633
 634    #region Windows Authentication
 635
 636    /// <summary>
 637    /// Adds Windows Authentication to the Kestrun host.
 638    /// <para>The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 639    /// This enables Kerberos and NTLM authentication.</para>
 640    /// </summary>
 641    /// <param name="host">The Kestrun host instance.</param>
 642    /// <param name="authenticationScheme">The authentication scheme name (default is NegotiateDefaults.AuthenticationSc
 643    /// <param name="displayName">The display name for the authentication scheme.</param>
 644    /// <param name="configureOptions">The WindowsAuthOptions object to configure the authentication.</param>
 645    /// <returns>The configured KestrunHost instance.</returns>
 646    public static KestrunHost AddWindowsAuthentication(
 647       this KestrunHost host,
 648       string authenticationScheme = AuthenticationDefaults.WindowsSchemeName,
 649       string? displayName = AuthenticationDefaults.WindowsDisplayName,
 650       Action<WindowsAuthOptions>? configureOptions = null)
 651    {
 652        // Build a prototype options instance (single source of truth)
 1653        var prototype = new WindowsAuthOptions { Host = host };
 1654        configureOptions?.Invoke(prototype);
 1655        ConfigureOpenApi(host, authenticationScheme, prototype);
 656
 657        // register in host for introspection
 1658        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Cookie, prototype);
 659
 660        // Add authentication
 1661        return host.AddAuthentication(
 1662            defaultScheme: authenticationScheme,
 1663            buildSchemes: ab =>
 1664            {
 1665                _ = ab.AddNegotiate(
 1666                    authenticationScheme: authenticationScheme,
 1667                    displayName: displayName,
 1668                    configureOptions: opts =>
 1669                    {
 1670                        // Copy everything from the prototype into the real options instance
 0671                        prototype.ApplyTo(opts);
 1672
 0673                        host.Logger.Debug("Configured Windows Authentication using scheme {Scheme}", authenticationSchem
 0674                    }
 1675                );
 1676            }
 1677        );
 678    }
 679    /// <summary>
 680    /// Adds Windows Authentication to the Kestrun host.
 681    /// <para>
 682    /// The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 683    /// This enables Kerberos and NTLM authentication.
 684    /// </para>
 685    /// </summary>
 686    /// <param name="host">The Kestrun host instance.</param>
 687    /// <param name="authenticationScheme">The authentication scheme name (default is NegotiateDefaults.AuthenticationSc
 688    /// <param name="displayName">The display name for the authentication scheme.</param>
 689    /// <param name="configureOptions">The WindowsAuthOptions object to configure the authentication.</param>
 690    /// <returns>The configured KestrunHost instance.</returns>
 691    public static KestrunHost AddWindowsAuthentication(
 692        this KestrunHost host,
 693        string authenticationScheme = AuthenticationDefaults.WindowsSchemeName,
 694        string? displayName = AuthenticationDefaults.WindowsDisplayName,
 695        WindowsAuthOptions? configureOptions = null)
 696    {
 0697        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 698        {
 0699            host.Logger.Debug("Adding Windows Authentication with scheme: {Scheme}", authenticationScheme);
 700        }
 701        // Ensure the scheme is not null
 0702        ArgumentNullException.ThrowIfNull(host);
 0703        ArgumentNullException.ThrowIfNull(configureOptions);
 704        // Ensure host is set
 0705        if (configureOptions.Host != host)
 706        {
 0707            configureOptions.Host = host;
 708        }
 709        // Copy relevant properties from provided options instance to the framework-created one
 710        // Add authentication
 0711        return host.AddWindowsAuthentication(
 0712           authenticationScheme: authenticationScheme,
 0713           displayName: displayName,
 0714           configureOptions: configureOptions.ApplyTo
 0715       );
 716    }
 717
 718    /// <summary>
 719    /// Adds Windows Authentication to the Kestrun host.
 720    /// <para>The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 721    /// This enables Kerberos and NTLM authentication.</para>
 722    /// </summary>
 723    /// <param name="host"> The Kestrun host instance.</param>
 724    /// <returns> The configured KestrunHost instance.</returns>
 725    public static KestrunHost AddWindowsAuthentication(this KestrunHost host) =>
 1726        host.AddWindowsAuthentication(
 1727            AuthenticationDefaults.WindowsSchemeName,
 1728            AuthenticationDefaults.WindowsDisplayName,
 1729            (Action<WindowsAuthOptions>?)null);
 730
 731    #endregion
 732    #region API Key Authentication
 733    /// <summary>
 734    /// Adds API Key Authentication to the Kestrun host.
 735    /// <para>Use this for endpoints that require an API key for access.</para>
 736    /// </summary>
 737    /// <param name="host">The Kestrun host instance.</param>
 738    /// <param name="authenticationScheme">The authentication scheme name (default is "ApiKey").</param>
 739    /// <param name="displayName">The display name for the authentication scheme (default is "API Key").</param>
 740    /// <param name="configureOptions">Optional configuration for ApiKeyAuthenticationOptions.</param>
 741    /// <returns>The configured KestrunHost instance.</returns>
 742    public static KestrunHost AddApiKeyAuthentication(
 743    this KestrunHost host,
 744    string authenticationScheme = AuthenticationDefaults.ApiKeySchemeName,
 745    string? displayName = AuthenticationDefaults.ApiKeyDisplayName,
 746    Action<ApiKeyAuthenticationOptions>? configureOptions = null)
 747    {
 748        // Build a prototype options instance (single source of truth)
 6749        var prototype = new ApiKeyAuthenticationOptions { Host = host };
 750
 751        // Let the caller mutate the prototype
 6752        configureOptions?.Invoke(prototype);
 753
 754        // Configure validators / claims / OpenAPI on the prototype
 6755        ConfigureApiKeyValidators(host, prototype);
 6756        ConfigureApiKeyIssueClaims(host, prototype);
 6757        ConfigureOpenApi(host, authenticationScheme, prototype);
 758
 759        // register in host for introspection
 6760        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.ApiKey, prototype);
 761        // Add authentication
 6762        return host.AddAuthentication(
 6763             defaultScheme: authenticationScheme,
 6764             buildSchemes: ab =>
 6765             {
 6766                 // ← TOptions == ApiKeyAuthenticationOptions
 6767                 //    THandler == ApiKeyAuthHandler
 6768                 _ = ab.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthHandler>(
 6769                     authenticationScheme: authenticationScheme,
 6770                     displayName: displayName,
 6771                     configureOptions: opts =>
 6772                     {
 6773                         // Copy from the prototype into the runtime instance
 6774                         prototype.ApplyTo(opts);
 6775
 6776                         host.Logger.Debug(
 6777                             "Configured API Key Authentication using scheme {Scheme} with header {Header} (In={In})",
 6778                             authenticationScheme, prototype.ApiKeyName, prototype.In);
 12779                     });
 6780             }
 6781         )
 6782        //  register the post-configurer **after** the scheme so it can
 6783        //    read BasicAuthenticationOptions for <scheme>
 6784        .AddService(services =>
 6785          {
 6786              _ = services.AddSingleton<IPostConfigureOptions<AuthorizationOptions>>(
 9787                  sp => new ClaimPolicyPostConfigurer(
 9788                            authenticationScheme,
 9789                            sp.GetRequiredService<
 9790                                IOptionsMonitor<ApiKeyAuthenticationOptions>>()));
 12791          });
 792    }
 793
 794    /// <summary>
 795    /// Adds API Key Authentication to the Kestrun host using the provided options object.
 796    /// </summary>
 797    /// <param name="host">The Kestrun host instance.</param>
 798    /// <param name="authenticationScheme">The authentication scheme name.</param>
 799    /// <param name="displayName">The display name for the authentication scheme.</param>
 800    /// <param name="configureOptions">The ApiKeyAuthenticationOptions object to configure the authentication.</param>
 801    /// <returns>The configured KestrunHost instance.</returns>
 802    public static KestrunHost AddApiKeyAuthentication(
 803    this KestrunHost host,
 804    string authenticationScheme = AuthenticationDefaults.ApiKeySchemeName,
 805    string? displayName = AuthenticationDefaults.ApiKeyDisplayName,
 806    ApiKeyAuthenticationOptions? configureOptions = null)
 807    {
 1808        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 809        {
 1810            host.Logger.Debug("Adding API Key Authentication with scheme: {Scheme}", authenticationScheme);
 811        }
 812        // Ensure the scheme is not null
 1813        ArgumentNullException.ThrowIfNull(host);
 1814        ArgumentNullException.ThrowIfNull(authenticationScheme);
 1815        ArgumentNullException.ThrowIfNull(configureOptions);
 816        // Ensure host is set
 1817        if (configureOptions.Host != host)
 818        {
 1819            configureOptions.Host = host;
 820        }
 821        // Copy properties from the provided configure object
 1822        return host.AddApiKeyAuthentication(
 1823            authenticationScheme: authenticationScheme,
 1824            displayName: displayName,
 1825            configureOptions: configureOptions.ApplyTo
 1826        );
 827    }
 828
 829    /// <summary>
 830    /// Configures the API Key validators.
 831    /// </summary>
 832    /// <param name="host">The Kestrun host instance.</param>
 833    /// <param name="opts">The options to configure.</param>
 834    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 835    private static void ConfigureApiKeyValidators(KestrunHost host, ApiKeyAuthenticationOptions opts)
 836    {
 6837        var settings = opts.ValidateCodeSettings;
 6838        if (string.IsNullOrWhiteSpace(settings.Code))
 839        {
 3840            return;
 841        }
 842
 3843        switch (settings.Language)
 844        {
 845            case ScriptLanguage.PowerShell:
 1846                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 847                {
 1848                    opts.Logger.Debug("Building PowerShell validator for API Key authentication");
 849                }
 850
 1851                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildPsValidator(host, settings);
 1852                break;
 853            case ScriptLanguage.CSharp:
 1854                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 855                {
 1856                    opts.Logger.Debug("Building C# validator for API Key authentication");
 857                }
 858
 1859                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildCsValidator(host, settings);
 1860                break;
 861            case ScriptLanguage.VBNet:
 1862                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 863                {
 1864                    opts.Logger.Debug("Building VB.NET validator for API Key authentication");
 865                }
 866
 1867                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildVBNetValidator(host, settings);
 1868                break;
 869            default:
 0870                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 871                {
 0872                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 873                }
 0874                throw new NotSupportedException("Unsupported language");
 875        }
 876    }
 877
 878    /// <summary>
 879    /// Configures the API Key issue claims.
 880    /// </summary>
 881    /// <param name="host">The Kestrun host instance.</param>
 882    /// <param name="opts">The options to configure.</param>
 883    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 884    private static void ConfigureApiKeyIssueClaims(KestrunHost host, ApiKeyAuthenticationOptions opts)
 885    {
 6886        var settings = opts.IssueClaimsCodeSettings;
 6887        if (string.IsNullOrWhiteSpace(settings.Code))
 888        {
 3889            return;
 890        }
 891
 3892        switch (settings.Language)
 893        {
 894            case ScriptLanguage.PowerShell:
 1895                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 896                {
 1897                    opts.Logger.Debug("Building PowerShell Issue Claims for API Key authentication");
 898                }
 899
 1900                opts.IssueClaims = IAuthHandler.BuildPsIssueClaims(host, settings);
 1901                break;
 902            case ScriptLanguage.CSharp:
 1903                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 904                {
 1905                    opts.Logger.Debug("Building C# Issue Claims for API Key authentication");
 906                }
 907
 1908                opts.IssueClaims = IAuthHandler.BuildCsIssueClaims(host, settings);
 1909                break;
 910            case ScriptLanguage.VBNet:
 1911                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 912                {
 1913                    opts.Logger.Debug("Building VB.NET Issue Claims for API Key authentication");
 914                }
 915
 1916                opts.IssueClaims = IAuthHandler.BuildVBNetIssueClaims(host, settings);
 1917                break;
 918            default:
 0919                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 920                {
 0921                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 922                }
 0923                throw new NotSupportedException("Unsupported language");
 924        }
 925    }
 926
 927    #endregion
 928
 929    #region OAuth2 Authentication
 930
 931    /// <summary>
 932    /// Adds OAuth2 authentication to the Kestrun host.
 933    /// <para>Use this for applications that require OAuth2 authentication.</para>
 934    /// </summary>
 935    /// <param name="host">The Kestrun host instance.</param>
 936    /// <param name="authenticationScheme">The authentication scheme name.</param>
 937    /// <param name="displayName">The display name for the authentication scheme.</param>
 938    /// <param name="configureOptions">The OAuth2Options to configure the authentication.</param>
 939    /// <returns>The configured KestrunHost instance.</returns>
 940    public static KestrunHost AddOAuth2Authentication(
 941        this KestrunHost host,
 942        string authenticationScheme = AuthenticationDefaults.OAuth2SchemeName,
 943        string? displayName = AuthenticationDefaults.OAuth2DisplayName,
 944        OAuth2Options? configureOptions = null)
 945    {
 0946        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 947        {
 0948            host.Logger.Debug("Adding OAuth2 Authentication with scheme: {Scheme}", authenticationScheme);
 949        }
 950        // Ensure the scheme is not null
 0951        ArgumentNullException.ThrowIfNull(host);
 0952        ArgumentNullException.ThrowIfNull(authenticationScheme);
 0953        ArgumentNullException.ThrowIfNull(configureOptions);
 954
 955        // Required for OAuth2
 0956        if (string.IsNullOrWhiteSpace(configureOptions.ClientId))
 957        {
 0958            throw new ArgumentException("ClientId must be provided in OAuth2Options", nameof(configureOptions));
 959        }
 960
 0961        if (string.IsNullOrWhiteSpace(configureOptions.AuthorizationEndpoint))
 962        {
 0963            throw new ArgumentException("AuthorizationEndpoint must be provided in OAuth2Options", nameof(configureOptio
 964        }
 965
 0966        if (string.IsNullOrWhiteSpace(configureOptions.TokenEndpoint))
 967        {
 0968            throw new ArgumentException("TokenEndpoint must be provided in OAuth2Options", nameof(configureOptions));
 969        }
 970
 971        // Default CallbackPath if not set: /signin-{scheme}
 0972        if (string.IsNullOrWhiteSpace(configureOptions.CallbackPath))
 973        {
 0974            configureOptions.CallbackPath = $"/signin-{authenticationScheme.ToLowerInvariant()}";
 975        }
 976        // Ensure host is set
 0977        if (configureOptions.Host != host)
 978        {
 0979            configureOptions.Host = host;
 980        }
 981        // Ensure scheme is set
 0982        if (authenticationScheme != configureOptions.AuthenticationScheme)
 983        {
 0984            configureOptions.AuthenticationScheme = authenticationScheme;
 985        }
 986        // Configure scopes and claim policies
 0987        ConfigureScopes(configureOptions, host.Logger);
 988        // Configure OpenAPI
 0989        ConfigureOpenApi(host, authenticationScheme, configureOptions);
 990
 991        // register in host for introspection
 0992        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.OAuth2, configureOptions);
 993
 994        // Add authentication
 0995        return host.AddAuthentication(
 0996            defaultScheme: configureOptions.CookieScheme,
 0997            defaultChallengeScheme: authenticationScheme,
 0998            buildSchemes: ab =>
 0999            {
 01000                // Add cookie scheme for sign-in
 01001                _ = ab.AddCookie(configureOptions.CookieScheme, cookieOpts =>
 01002               {
 01003                   configureOptions.CookieOptions.ApplyTo(cookieOpts);
 01004               });
 01005                // Add OAuth2 scheme
 01006                _ = ab.AddOAuth(
 01007                    authenticationScheme: authenticationScheme,
 01008                    displayName: displayName ?? OAuthDefaults.DisplayName,
 01009                    configureOptions: oauthOpts =>
 01010                {
 01011                    configureOptions.ApplyTo(oauthOpts);
 01012                    if (host.Logger.IsEnabled(LogEventLevel.Debug))
 01013                    {
 01014                        host.Logger.Debug("Configured OpenID Connect with ClientId: {ClientId}, Scopes: {Scopes}",
 01015                          oauthOpts.ClientId, string.Join(", ", oauthOpts.Scope));
 01016                    }
 01017                });
 01018            },
 01019              configureAuthz: configureOptions.ClaimPolicy?.ToAuthzDelegate()
 01020        );
 1021    }
 1022
 1023    /// <summary>
 1024    /// Configures OAuth2 scopes and claim policies.
 1025    /// </summary>
 1026    /// <param name="configureOptions">The OAuth2 options to configure.</param>
 1027    /// <param name="logger">The logger for debug output.</param>
 1028    private static void ConfigureScopes(IOAuthCommonOptions configureOptions, Serilog.ILogger logger)
 1029    {
 31030        if (configureOptions.Scope is null)
 1031        {
 01032            return;
 1033        }
 1034
 31035        if (configureOptions.Scope.Count == 0)
 1036        {
 11037            BackfillScopesFromClaimPolicy(configureOptions, logger);
 11038            return;
 1039        }
 1040
 21041        LogConfiguredScopes(configureOptions.Scope, logger);
 1042
 21043        if (configureOptions.ClaimPolicy is null)
 1044        {
 11045            configureOptions.ClaimPolicy = BuildClaimPolicyFromScopes(configureOptions.Scope, logger);
 11046            return;
 1047        }
 1048
 11049        AddMissingScopesToClaimPolicy(configureOptions.Scope, configureOptions.ClaimPolicy, logger);
 11050    }
 1051
 1052    private static ClaimPolicyConfig BuildClaimPolicyFromScopes(ICollection<string> scopes, Serilog.ILogger logger)
 1053    {
 11054        var claimPolicyBuilder = new ClaimPolicyBuilder();
 61055        foreach (var scope in scopes)
 1056        {
 21057            LogScopeAdded(logger, scope);
 21058            _ = claimPolicyBuilder.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedVa
 1059        }
 1060
 11061        return claimPolicyBuilder.Build();
 1062    }
 1063
 1064    private static void AddMissingScopesToClaimPolicy(ICollection<string> scopes, ClaimPolicyConfig claimPolicy, Serilog
 1065    {
 11066        var missingScopes = scopes
 21067            .Where(s => !claimPolicy.Policies.ContainsKey(s))
 11068            .ToList();
 1069
 11070        if (missingScopes.Count == 0)
 1071        {
 01072            return;
 1073        }
 1074
 11075        LogMissingScopes(missingScopes, logger);
 1076
 11077        var claimPolicyBuilder = new ClaimPolicyBuilder();
 41078        foreach (var scope in missingScopes)
 1079        {
 11080            _ = claimPolicyBuilder.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedVa
 11081            LogScopeAddedToClaimPolicy(logger, scope);
 1082        }
 1083
 11084        claimPolicy.AddPolicies(claimPolicyBuilder.Policies);
 11085    }
 1086
 1087    private static void BackfillScopesFromClaimPolicy(IOAuthCommonOptions configureOptions, Serilog.ILogger logger)
 1088    {
 11089        if (configureOptions.ClaimPolicy is null)
 1090        {
 01091            return;
 1092        }
 1093
 61094        foreach (var policy in configureOptions.ClaimPolicy.PolicyNames)
 1095        {
 21096            LogClaimPolicyConfigured(logger, policy);
 21097            configureOptions.Scope?.Add(policy);
 1098        }
 11099    }
 1100
 1101    private static void LogScopeAdded(Serilog.ILogger logger, string scope)
 1102    {
 21103        if (logger.IsEnabled(LogEventLevel.Debug))
 1104        {
 21105            logger.Debug("OAuth2 scope added: {Scope}", scope);
 1106        }
 21107    }
 1108
 1109    private static void LogScopeAddedToClaimPolicy(Serilog.ILogger logger, string scope)
 1110    {
 11111        if (logger.IsEnabled(LogEventLevel.Debug))
 1112        {
 11113            logger.Debug("OAuth2 scope added to claim policy: {Scope}", scope);
 1114        }
 11115    }
 1116
 1117    private static void LogMissingScopes(IEnumerable<string> missingScopes, Serilog.ILogger logger)
 1118    {
 11119        if (logger.IsEnabled(LogEventLevel.Debug))
 1120        {
 11121            logger.Debug("Adding missing OAuth2 scopes to claim policy: {Scopes}", string.Join(", ", missingScopes));
 1122        }
 11123    }
 1124
 1125    private static void LogConfiguredScopes(IEnumerable<string> scopes, Serilog.ILogger logger)
 1126    {
 21127        if (logger.IsEnabled(LogEventLevel.Debug))
 1128        {
 21129            logger.Debug("OAuth2 scopes configured: {Scopes}", string.Join(", ", scopes));
 1130        }
 21131    }
 1132
 1133    private static void LogClaimPolicyConfigured(Serilog.ILogger logger, string policy)
 1134    {
 21135        if (logger.IsEnabled(LogEventLevel.Debug))
 1136        {
 21137            logger.Debug("OAuth2 claim policy configured: {Policy}", policy);
 1138        }
 21139    }
 1140
 1141    #endregion
 1142    #region OpenID Connect Authentication
 1143
 1144    /// <summary>
 1145    /// Adds OpenID Connect authentication to the Kestrun host with private key JWT client assertion.
 1146    /// <para>Use this for applications that require OpenID Connect authentication with client credentials using JWT ass
 1147    /// </summary>
 1148    /// <param name="host">The Kestrun host instance.</param>
 1149    /// <param name="authenticationScheme">The authentication scheme name.</param>
 1150    /// <param name="displayName">The display name for the authentication scheme.</param>
 1151    /// <param name="configureOptions">The OpenIdConnectOptions to configure the authentication.</param>
 1152    /// <returns>The configured KestrunHost instance.</returns>
 1153    public static KestrunHost AddOpenIdConnectAuthentication(
 1154           this KestrunHost host,
 1155           string authenticationScheme = AuthenticationDefaults.OidcSchemeName,
 1156           string? displayName = AuthenticationDefaults.OidcDisplayName,
 1157           OidcOptions? configureOptions = null)
 1158    {
 01159        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1160        {
 01161            host.Logger.Debug("Adding OpenID Connect Authentication with scheme: {Scheme}", authenticationScheme);
 1162        }
 1163        // Ensure the scheme is not null
 01164        ArgumentNullException.ThrowIfNull(host);
 01165        ArgumentNullException.ThrowIfNull(authenticationScheme);
 01166        ArgumentNullException.ThrowIfNull(configureOptions);
 1167
 1168        // Ensure ClientId is set
 01169        if (string.IsNullOrWhiteSpace(configureOptions.ClientId))
 1170        {
 01171            throw new ArgumentException("ClientId must be provided in OpenIdConnectOptions", nameof(configureOptions));
 1172        }
 1173        // Ensure host is set
 01174        if (configureOptions.Host != host)
 1175        {
 01176            configureOptions.Host = host;
 1177        }
 1178        // Ensure scheme is set
 01179        if (authenticationScheme != configureOptions.AuthenticationScheme)
 1180        {
 01181            configureOptions.AuthenticationScheme = authenticationScheme;
 1182        }
 1183        // Retrieve supported scopes from the OIDC provider
 01184        if (!string.IsNullOrWhiteSpace(configureOptions.Authority))
 1185        {
 01186            configureOptions.ClaimPolicy = GetSupportedScopes(configureOptions.Authority, host.Logger);
 01187            if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1188            {
 01189                host.Logger.Debug("OIDC supported scopes: {Scopes}", string.Join(", ", configureOptions.ClaimPolicy?.Pol
 1190            }
 1191        }
 1192        // Configure scopes and claim policies
 01193        ConfigureScopes(configureOptions, host.Logger);
 1194        // Configure OpenAPI
 01195        ConfigureOpenApi(host, authenticationScheme, configureOptions);
 1196
 1197        // register in host for introspection
 01198        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Oidc, configureOptions);
 1199
 1200        // CRITICAL: Register OidcEvents and AssertionService in DI before configuring authentication
 1201        // This is required because EventsType expects these to be available in the service provider
 01202        return host.AddService(services =>
 01203         {
 01204             // Register AssertionService as a singleton with factory to pass clientId and jwkJson
 01205             // Only register if JwkJson is provided (for private_key_jwt authentication)
 01206             if (!string.IsNullOrWhiteSpace(configureOptions.JwkJson))
 01207             {
 01208                 services.TryAddSingleton(sp => new AssertionService(configureOptions.ClientId, configureOptions.JwkJson
 01209                 // Register OidcEvents as scoped (per-request)
 01210                 services.TryAddScoped<OidcEvents>();
 01211             }
 01212         }).AddAuthentication(
 01213              defaultScheme: configureOptions.CookieScheme,
 01214              defaultChallengeScheme: authenticationScheme,
 01215              buildSchemes: ab =>
 01216              {
 01217                  // Add cookie scheme for sign-in
 01218                  _ = ab.AddCookie(configureOptions.CookieScheme, cookieOpts =>
 01219                 {
 01220                     // Copy cookie configuration from options.CookieOptions
 01221                     configureOptions.CookieOptions.ApplyTo(cookieOpts);
 01222                 });
 01223                  // Add OpenID Connect scheme
 01224                  _ = ab.AddOpenIdConnect(
 01225                    authenticationScheme: authenticationScheme,
 01226                    displayName: displayName ?? OpenIdConnectDefaults.DisplayName,
 01227                    configureOptions: oidcOpts =>
 01228                 {
 01229                     // Copy all properties from the provided options to the framework's options
 01230                     configureOptions.ApplyTo(oidcOpts);
 01231
 01232                     // Inject private key JWT at code → token step (only if JwkJson is provided)
 01233                     // This will be resolved from DI at runtime
 01234                     if (!string.IsNullOrWhiteSpace(configureOptions.JwkJson))
 01235                     {
 01236                         oidcOpts.EventsType = typeof(OidcEvents);
 01237                     }
 01238                     if (host.Logger.IsEnabled(LogEventLevel.Debug))
 01239                     {
 01240                         host.Logger.Debug("Configured OpenID Connect with Authority: {Authority}, ClientId: {ClientId},
 01241                             oidcOpts.Authority, oidcOpts.ClientId, string.Join(", ", oidcOpts.Scope));
 01242                     }
 01243                 });
 01244              },
 01245              configureAuthz: configureOptions.ClaimPolicy?.ToAuthzDelegate()
 01246            );
 1247    }
 1248
 1249    /// <summary>
 1250    /// Retrieves the supported scopes from the OpenID Connect provider's metadata.
 1251    /// </summary>
 1252    /// <param name="authority">The authority URL of the OpenID Connect provider.</param>
 1253    /// <param name="logger">The logger instance for logging.</param>
 1254    /// <returns>A ClaimPolicyConfig containing the supported scopes, or null if retrieval fails.</returns>
 1255    private static ClaimPolicyConfig? GetSupportedScopes(string authority, Serilog.ILogger logger)
 1256    {
 01257        if (logger.IsEnabled(LogEventLevel.Debug))
 1258        {
 01259            logger.Debug("Retrieving OpenID Connect configuration from authority: {Authority}", authority);
 1260        }
 01261        var claimPolicy = new ClaimPolicyBuilder();
 01262        if (string.IsNullOrWhiteSpace(authority))
 1263        {
 01264            throw new ArgumentException("Authority must be provided to retrieve OpenID Connect scopes.", nameof(authorit
 1265        }
 1266
 01267        var metadataAddress = authority.TrimEnd('/') + "/.well-known/openid-configuration";
 1268
 01269        var documentRetriever = new HttpDocumentRetriever
 01270        {
 01271            RequireHttps = metadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
 01272        };
 1273
 01274        var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
 01275            metadataAddress,
 01276            new OpenIdConnectConfigurationRetriever(),
 01277            documentRetriever);
 1278
 1279        try
 1280        {
 01281            var cfg = configManager.GetConfigurationAsync(CancellationToken.None)
 01282                                   .GetAwaiter()
 01283                                   .GetResult();
 1284            // First try the strongly-typed property
 01285            var scopes = cfg.ScopesSupported;
 1286
 1287            // If it's null or empty, fall back to raw JSON
 01288            if (scopes == null || scopes.Count == 0)
 1289            {
 01290                var json = documentRetriever.GetDocumentAsync(metadataAddress, CancellationToken.None)
 01291                                            .GetAwaiter()
 01292                                            .GetResult();
 1293
 01294                using var doc = JsonDocument.Parse(json);
 01295                if (doc.RootElement.TryGetProperty("scopes_supported", out var scopesElement) &&
 01296                    scopesElement.ValueKind == JsonValueKind.Array)
 1297                {
 01298                    foreach (var scope in scopesElement.EnumerateArray().Select(item => item.GetString()).Where(s => !st
 1299                    {
 01300                        if (scope != null)
 1301                        {
 01302                            _ = claimPolicy.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, 
 1303                        }
 1304                    }
 1305                }
 1306            }
 1307            else
 1308            {
 1309                // Normal path: configuration object had scopes
 01310                foreach (var scope in scopes)
 1311                {
 01312                    _ = claimPolicy.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedV
 1313                }
 1314            }
 01315            return claimPolicy.Build();
 1316        }
 01317        catch (Exception ex)
 1318        {
 01319            logger.Warning(ex, "Failed to retrieve OpenID Connect configuration from {MetadataAddress}", metadataAddress
 01320            return null;
 1321        }
 01322    }
 1323
 1324    #endregion
 1325    #region Helper Methods
 1326    /// <summary>
 1327    /// Configures OpenAPI security schemes for the given authentication options.
 1328    /// </summary>
 1329    /// <param name="host">The Kestrun host instance.</param>
 1330    /// <param name="scheme">The authentication scheme name.</param>
 1331    /// <param name="opts">The OpenAPI authentication options.</param>
 1332    private static void ConfigureOpenApi(KestrunHost host, string scheme, IOpenApiAuthenticationOptions opts)
 1333    {
 1334        // Apply to specified documentation IDs or all if none specified
 201335        if (opts.DocumentationId == null || opts.DocumentationId.Length == 0)
 1336        {
 201337            opts.DocumentationId = IOpenApiAuthenticationOptions.DefaultDocumentationIds;
 1338        }
 1339
 801340        foreach (var docDescriptor in opts.DocumentationId
 201341            .Select(host.GetOrCreateOpenApiDocument)
 401342            .Where(docDescriptor => docDescriptor != null))
 1343        {
 201344            docDescriptor.ApplySecurityScheme(scheme, opts);
 1345        }
 201346    }
 1347
 1348    #endregion
 1349
 1350    /// <summary>
 1351    /// Adds authentication and authorization middleware to the Kestrun host.
 1352    /// </summary>
 1353    /// <param name="host">The Kestrun host instance.</param>
 1354    /// <param name="buildSchemes">A delegate to configure authentication schemes.</param>
 1355    /// <param name="defaultScheme">The default authentication scheme.</param>
 1356    /// <param name="configureAuthz">Optional authorization policy configuration.</param>
 1357    /// <param name="defaultChallengeScheme">The default challenge scheme .</param>
 1358    /// <returns>The configured KestrunHost instance.</returns>
 1359    internal static KestrunHost AddAuthentication(
 1360    this KestrunHost host,
 1361    string defaultScheme,
 1362    Action<AuthenticationBuilder>? buildSchemes = null,    // e.g., ab => ab.AddCookie().AddOpenIdConnect("oidc", ...)
 1363    Action<AuthorizationOptions>? configureAuthz = null,
 1364    string? defaultChallengeScheme = null)
 1365    {
 201366        ArgumentNullException.ThrowIfNull(buildSchemes);
 201367        if (string.IsNullOrWhiteSpace(defaultScheme))
 1368        {
 01369            throw new ArgumentException("Default scheme is required.", nameof(defaultScheme));
 1370        }
 1371
 201372        _ = host.AddService(services =>
 201373        {
 201374            // CRITICAL: Check if authentication services are already registered
 201375            // If they are, we only need to add new schemes, not reconfigure defaults
 22841376            var authDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IAuthenticationService));
 201377
 201378            AuthenticationBuilder authBuilder;
 201379            if (authDescriptor != null)
 201380            {
 201381                // Authentication already registered - only add new schemes without changing defaults
 01382                host.Logger.Debug("Authentication services already registered - adding schemes only (default={DefaultSch
 01383                authBuilder = new AuthenticationBuilder(services);
 201384            }
 201385            else
 201386            {
 201387                // First time registration - configure defaults
 201388                host.Logger.Debug(
 201389                    "Registering authentication services with defaults (default={DefaultScheme}, challenge={ChallengeSch
 201390                    defaultScheme,
 201391                    defaultChallengeScheme ?? defaultScheme);
 201392                authBuilder = services.AddAuthentication(options =>
 201393                {
 131394                    options.DefaultScheme = defaultScheme;
 131395                    options.DefaultChallengeScheme = defaultChallengeScheme ?? defaultScheme;
 331396                });
 201397            }
 201398
 201399            // Let caller add handlers/schemes
 201400            buildSchemes?.Invoke(authBuilder);
 201401
 201402            // Ensure Authorization is available (with optional customization)
 201403            // AddAuthorization is idempotent - safe to call multiple times
 201404            _ = configureAuthz is not null ?
 201405                services.AddAuthorization(configureAuthz) :
 201406                services.AddAuthorization();
 231407        });
 1408
 1409        // Add middleware once
 201410        return host.Use(app =>
 201411        {
 201412            const string Key = "__kr.authmw";
 201413            if (!app.Properties.ContainsKey(Key))
 201414            {
 201415                _ = app.UseAuthentication();
 201416                _ = app.UseAuthorization();
 201417                app.Properties[Key] = true;
 201418                host.Logger.Information("Kestrun: Authentication & Authorization middleware added.");
 201419            }
 401420        });
 1421    }
 1422
 1423    /// <summary>
 1424    /// Checks if the specified authentication scheme is registered in the Kestrun host.
 1425    /// </summary>
 1426    /// <param name="host">The Kestrun host instance.</param>
 1427    /// <param name="schemeName">The name of the authentication scheme to check.</param>
 1428    /// <returns>True if the scheme is registered; otherwise, false.</returns>
 1429    public static bool HasAuthScheme(this KestrunHost host, string schemeName)
 1430    {
 131431        var schemeProvider = host.App.Services.GetRequiredService<IAuthenticationSchemeProvider>();
 131432        var scheme = schemeProvider.GetSchemeAsync(schemeName).GetAwaiter().GetResult();
 131433        return scheme != null;
 1434    }
 1435
 1436    /// <summary>
 1437    /// Adds authorization services to the Kestrun host.
 1438    /// </summary>
 1439    /// <param name="host">The Kestrun host instance.</param>
 1440    /// <param name="cfg">Optional configuration for authorization options.</param>
 1441    /// <returns>The configured KestrunHost instance.</returns>
 1442    public static KestrunHost AddAuthorization(this KestrunHost host, Action<AuthorizationOptions>? cfg = null)
 1443    {
 11444        return host.AddService(services =>
 11445        {
 11446            _ = cfg == null ? services.AddAuthorization() : services.AddAuthorization(cfg);
 21447        });
 1448    }
 1449
 1450    /// <summary>
 1451    /// Checks if the specified authorization policy is registered in the Kestrun host.
 1452    /// </summary>
 1453    /// <param name="host">The Kestrun host instance.</param>
 1454    /// <param name="policyName">The name of the authorization policy to check.</param>
 1455    /// <returns>True if the policy is registered; otherwise, false.</returns>
 1456    public static bool HasAuthPolicy(this KestrunHost host, string policyName)
 1457    {
 131458        var policyProvider = host.App.Services.GetRequiredService<IAuthorizationPolicyProvider>();
 131459        var policy = policyProvider.GetPolicyAsync(policyName).GetAwaiter().GetResult();
 131460        return policy != null;
 1461    }
 1462
 1463    /// <summary>
 1464    /// HTTP message handler that logs all HTTP requests and responses for debugging.
 1465    /// </summary>
 01466    internal class LoggingHttpMessageHandler(HttpMessageHandler innerHandler, Serilog.ILogger logger) : DelegatingHandle
 1467    {
 01468        private readonly Serilog.ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 1469
 1470        // CRITICAL: Static field to store the last token response body so we can manually parse it
 1471        // The framework's OpenIdConnectMessage parser fails to populate AccessToken correctly
 01472        internal static string? LastTokenResponseBody { get; private set; }
 1473
 1474        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cance
 1475        {
 1476            // Log request
 01477            _logger.Warning($"HTTP {request.Method} {request.RequestUri}");
 1478
 1479            // Check if this is a token endpoint request
 01480            var isTokenEndpoint = request.RequestUri?.PathAndQuery?.Contains("/connect/token") == true ||
 01481                                 request.RequestUri?.PathAndQuery?.Contains("/token") == true;
 1482
 01483            if (request.Content != null && !isTokenEndpoint)
 1484            {
 1485                // Read request body without consuming it (only for non-token requests)
 01486                var requestBytes = await request.Content.ReadAsByteArrayAsync(cancellationToken);
 01487                var requestBody = System.Text.Encoding.UTF8.GetString(requestBytes);
 01488                _logger.Warning($"Request Body: {requestBody}");
 1489
 1490                // Recreate the content so it can be read again
 01491                request.Content = new ByteArrayContent(requestBytes);
 01492                request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-ww
 1493            }
 01494            else if (request.Content != null && isTokenEndpoint)
 1495            {
 01496                _logger.Warning("Token endpoint request - skipping body logging to preserve stream");
 1497            }
 1498
 1499            // Send request
 01500            var response = await base.SendAsync(request, cancellationToken);
 1501
 1502            // Log response
 01503            _logger.Warning($"HTTP Response: {(int)response.StatusCode} {response.StatusCode}");
 1504
 1505            // CRITICAL: For token endpoint responses, capture the body for manual parsing
 1506            // but then recreate the stream so the framework can also read it
 01507            if (response.Content != null && isTokenEndpoint)
 1508            {
 1509                // Read the response body
 01510                var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
 01511                var responseBody = System.Text.Encoding.UTF8.GetString(responseBytes);
 1512
 1513                // Store it in static field for later manual parsing
 01514                LastTokenResponseBody = responseBody;
 01515                _logger.Warning($"Captured token response body ({responseBytes.Length} bytes) for manual parsing");
 1516
 1517                // Recreate the content stream with ALL original headers preserved
 01518                var originalHeaders = response.Content.Headers.ToList();
 01519                var newContent = new ByteArrayContent(responseBytes);
 1520
 01521                foreach (var header in originalHeaders)
 1522                {
 01523                    _ = newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
 1524                }
 1525
 01526                response.Content = newContent;
 01527                _logger.Warning("Recreated token response stream for framework parsing");
 1528            }
 01529            else if (response.Content != null && !isTokenEndpoint)
 1530            {
 1531                // Save original headers
 01532                var originalHeaders = response.Content.Headers;
 1533
 1534                // Read response body and preserve it for the handler
 01535                var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
 01536                var responseBody = System.Text.Encoding.UTF8.GetString(responseBytes);
 01537                _logger.Warning($"Response Body: {responseBody}");
 1538
 1539                // Recreate the content so it can be read again by the OIDC handler
 01540                var newContent = new ByteArrayContent(responseBytes);
 1541
 1542                // Copy all original headers to the new content
 01543                foreach (var header in originalHeaders)
 1544                {
 01545                    _ = newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
 1546                }
 1547
 01548                response.Content = newContent;
 01549            }
 01550            else if (response.Content != null && isTokenEndpoint)
 1551            {
 01552                _logger.Warning("Token endpoint response - skipping body logging to let framework parse it");
 1553            }
 1554
 01555            return response;
 01556        }
 1557    }
 1558}

Methods/Properties

AddBasicAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,System.Action`1<Kestrun.Authentication.BasicAuthenticationOptions>)
AddBasicAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.BasicAuthenticationOptions)
ConfigureBasicAuthValidators(Kestrun.Hosting.KestrunHost,Kestrun.Authentication.BasicAuthenticationOptions)
ConfigureBasicIssueClaims(Kestrun.Hosting.KestrunHost,Kestrun.Authentication.BasicAuthenticationOptions)
AddGitHubOAuthAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,System.String[],System.String,System.String,System.String,System.String)
ConfigureGitHubOAuth2Options(Kestrun.Hosting.KestrunHost,System.String,System.String,System.String)
ConfigureGitHubClaimMappings(Kestrun.Authentication.OAuth2Options)
FetchGitHubUserInfoAsync()
EnrichGitHubEmailClaimAsync()
FindPrimaryVerifiedEmail(System.Text.Json.JsonDocument)
FindFirstVerifiedEmail(System.Text.Json.JsonDocument)
AddJwtBearerAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,System.Action`1<Kestrun.Authentication.JwtAuthOptions>)
AddJwtBearerAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.JwtAuthOptions)
AddCookieAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,System.Action`1<Kestrun.Authentication.CookieAuthOptions>,Kestrun.Claims.ClaimPolicyConfig)
AddCookieAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.CookieAuthOptions,Kestrun.Claims.ClaimPolicyConfig)
AddWindowsAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,System.Action`1<Kestrun.Authentication.WindowsAuthOptions>)
AddWindowsAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.WindowsAuthOptions)
AddWindowsAuthentication(Kestrun.Hosting.KestrunHost)
AddApiKeyAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,System.Action`1<Kestrun.Authentication.ApiKeyAuthenticationOptions>)
AddApiKeyAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.ApiKeyAuthenticationOptions)
ConfigureApiKeyValidators(Kestrun.Hosting.KestrunHost,Kestrun.Authentication.ApiKeyAuthenticationOptions)
ConfigureApiKeyIssueClaims(Kestrun.Hosting.KestrunHost,Kestrun.Authentication.ApiKeyAuthenticationOptions)
AddOAuth2Authentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.OAuth2Options)
ConfigureScopes(Kestrun.Authentication.IOAuthCommonOptions,Serilog.ILogger)
BuildClaimPolicyFromScopes(System.Collections.Generic.ICollection`1<System.String>,Serilog.ILogger)
AddMissingScopesToClaimPolicy(System.Collections.Generic.ICollection`1<System.String>,Kestrun.Claims.ClaimPolicyConfig,Serilog.ILogger)
BackfillScopesFromClaimPolicy(Kestrun.Authentication.IOAuthCommonOptions,Serilog.ILogger)
LogScopeAdded(Serilog.ILogger,System.String)
LogScopeAddedToClaimPolicy(Serilog.ILogger,System.String)
LogMissingScopes(System.Collections.Generic.IEnumerable`1<System.String>,Serilog.ILogger)
LogConfiguredScopes(System.Collections.Generic.IEnumerable`1<System.String>,Serilog.ILogger)
LogClaimPolicyConfigured(Serilog.ILogger,System.String)
AddOpenIdConnectAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.OidcOptions)
GetSupportedScopes(System.String,Serilog.ILogger)
ConfigureOpenApi(Kestrun.Hosting.KestrunHost,System.String,Kestrun.Authentication.IOpenApiAuthenticationOptions)
AddAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.Action`1<Microsoft.AspNetCore.Authentication.AuthenticationBuilder>,System.Action`1<Microsoft.AspNetCore.Authorization.AuthorizationOptions>,System.String)
HasAuthScheme(Kestrun.Hosting.KestrunHost,System.String)
AddAuthorization(Kestrun.Hosting.KestrunHost,System.Action`1<Microsoft.AspNetCore.Authorization.AuthorizationOptions>)
HasAuthPolicy(Kestrun.Hosting.KestrunHost,System.String)
.ctor(System.Net.Http.HttpMessageHandler,Serilog.ILogger)
get_LastTokenResponseBody()
SendAsync()