< 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@ca54e35c77799b76774b3805b6f075cdbc0c5fbe
Line coverage
51%
Covered lines: 360
Uncovered lines: 335
Coverable lines: 695
Total lines: 1658
Line coverage: 51.7%
Branch coverage
45%
Covered branches: 138
Total branches: 304
Branch coverage: 45.3%
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@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/21/2025 - 06:07:10 Line coverage: 51.8% (342/659) Branch coverage: 45.2% (134/296) Total lines: 1561 Tag: Kestrun/Kestrun@8cf7f77e55fd1fd046ea4e5413eb9ef96e49fe6a12/26/2025 - 18:43:06 Line coverage: 51.8% (342/659) Branch coverage: 45.2% (134/296) Total lines: 1562 Tag: Kestrun/Kestrun@66a9a3a4461391825b9a1ffc8190f76adb1bb67f01/21/2026 - 17:07:46 Line coverage: 51.7% (360/695) Branch coverage: 45.3% (138/304) Total lines: 1658 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36 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@7a3839f4de2254e22daae81ab8dc7cb2f40c833012/21/2025 - 06:07:10 Line coverage: 51.8% (342/659) Branch coverage: 45.2% (134/296) Total lines: 1561 Tag: Kestrun/Kestrun@8cf7f77e55fd1fd046ea4e5413eb9ef96e49fe6a12/26/2025 - 18:43:06 Line coverage: 51.8% (342/659) Branch coverage: 45.2% (134/296) Total lines: 1562 Tag: Kestrun/Kestrun@66a9a3a4461391825b9a1ffc8190f76adb1bb67f01/21/2026 - 17:07:46 Line coverage: 51.7% (360/695) Branch coverage: 45.3% (138/304) Total lines: 1658 Tag: Kestrun/Kestrun@3f6f61710c7ef7d5953cab578fe699c1e5e01a36

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%
AddClientCertificateAuthentication(...)100%4490%
AddClientCertificateAuthentication(...)0%2040%
AddClientCertificateAuthentication(...)100%210%
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;
 17using Kestrun.OpenApi;
 18
 19namespace Kestrun.Hosting;
 20
 21/// <summary>
 22/// Provides extension methods for adding authentication schemes to the Kestrun host.
 23/// </summary>
 24public static class KestrunHostAuthnExtensions
 25{
 26    #region Basic Authentication
 27    /// <summary>
 28    /// Adds Basic Authentication to the Kestrun host.
 29    /// <para>Use this for simple username/password authentication.</para>
 30    /// </summary>
 31    /// <param name="host">The Kestrun host instance.</param>
 32    /// <param name="scheme">The authentication scheme name (e.g. "Basic").</param>
 33    /// <param name="displayName">The display name for the authentication scheme.</param>
 34    /// <param name="configure">Optional configuration for BasicAuthenticationOptions.</param>
 35    /// <returns>returns the KestrunHost instance.</returns>
 36    public static KestrunHost AddBasicAuthentication(
 37    this KestrunHost host,
 38    string scheme = AuthenticationDefaults.BasicSchemeName,
 39    string? displayName = AuthenticationDefaults.BasicDisplayName,
 40    Action<BasicAuthenticationOptions>? configure = null
 41    )
 42    {
 43        // Build a prototype options instance (single source of truth)
 844        var prototype = new BasicAuthenticationOptions { Host = host };
 45
 46        // Let the caller mutate the prototype
 847        configure?.Invoke(prototype);
 48
 49        // Configure validators / claims / OpenAPI on the prototype
 850        ConfigureBasicAuthValidators(host, prototype);
 851        ConfigureBasicIssueClaims(host, prototype);
 852        ConfigureOpenApi(host, scheme, prototype);
 53        // register in host for introspection
 854        _ = host.RegisteredAuthentications.Register(scheme, AuthenticationType.Basic, prototype);
 855        var h = host.AddAuthentication(
 856           defaultScheme: scheme,
 857           buildSchemes: ab =>
 858           {
 859               _ = ab.AddScheme<BasicAuthenticationOptions, BasicAuthHandler>(
 860                   authenticationScheme: scheme,
 861                   displayName: displayName,
 862                   configureOptions: opts =>
 863                   {
 864                       // Copy from the prototype into the runtime instance
 665                       prototype.ApplyTo(opts);
 866
 667                       host.Logger.Debug("Configured Basic Authentication using scheme {Scheme}", scheme);
 1468                   });
 869           }
 870       );
 71        //  register the post-configurer **after** the scheme so it can
 72        //    read BasicAuthenticationOptions for <scheme>
 873        return h.AddService(services =>
 874        {
 875            _ = services.AddSingleton<IPostConfigureOptions<AuthorizationOptions>>(
 1176                sp => new ClaimPolicyPostConfigurer(
 1177                          scheme,
 1178                          sp.GetRequiredService<
 1179                              IOptionsMonitor<BasicAuthenticationOptions>>()));
 1680        });
 81    }
 82
 83    /// <summary>
 84    /// Adds Basic Authentication to the Kestrun host using the provided options object.
 85    /// </summary>
 86    /// <param name="host">The Kestrun host instance.</param>
 87    /// <param name="scheme">The authentication scheme name (e.g. "Basic").</param>
 88    /// <param name="displayName">The display name for the authentication scheme.</param>
 89    /// <param name="configure">The BasicAuthenticationOptions object to configure the authentication.</param>
 90    /// <returns>The configured KestrunHost instance.</returns>
 91    public static KestrunHost AddBasicAuthentication(
 92        this KestrunHost host,
 93        string scheme,
 94        string? displayName,
 95        BasicAuthenticationOptions configure
 96        )
 97    {
 198        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 99        {
 1100            host.Logger.Debug("Adding Basic Authentication with scheme: {Scheme}", scheme);
 101        }
 102        // Ensure the scheme is not null
 1103        ArgumentNullException.ThrowIfNull(host);
 1104        ArgumentNullException.ThrowIfNull(scheme);
 1105        ArgumentNullException.ThrowIfNull(configure);
 106        // Ensure host is set
 1107        if (configure.Host != host)
 108        {
 1109            configure.Host = host;
 110        }
 1111        return host.AddBasicAuthentication(
 1112            scheme: scheme,
 1113            displayName: displayName,
 1114            configure: configure.ApplyTo
 1115        );
 116    }
 117
 118    /// <summary>
 119    /// Configures the validators for Basic authentication.
 120    /// </summary>
 121    /// <param name="host">The Kestrun host instance.</param>
 122    /// <param name="opts">The options to configure.</param>
 123    private static void ConfigureBasicAuthValidators(KestrunHost host, BasicAuthenticationOptions opts)
 124    {
 8125        var settings = opts.ValidateCodeSettings;
 8126        if (string.IsNullOrWhiteSpace(settings.Code))
 127        {
 5128            return;
 129        }
 130
 3131        switch (settings.Language)
 132        {
 133            case ScriptLanguage.PowerShell:
 1134                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 135                {
 1136                    opts.Logger.Debug("Building PowerShell validator for Basic authentication");
 137                }
 138
 1139                opts.ValidateCredentialsAsync = BasicAuthHandler.BuildPsValidator(host, settings);
 1140                break;
 141            case ScriptLanguage.CSharp:
 1142                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 143                {
 1144                    opts.Logger.Debug("Building C# validator for Basic authentication");
 145                }
 146
 1147                opts.ValidateCredentialsAsync = BasicAuthHandler.BuildCsValidator(host, settings);
 1148                break;
 149            case ScriptLanguage.VBNet:
 1150                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 151                {
 1152                    opts.Logger.Debug("Building VB.NET validator for Basic authentication");
 153                }
 154
 1155                opts.ValidateCredentialsAsync = BasicAuthHandler.BuildVBNetValidator(host, settings);
 1156                break;
 157            default:
 0158                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 159                {
 0160                    opts.Logger.Warning("No valid language specified for Basic authentication");
 161                }
 162                break;
 163        }
 0164    }
 165
 166    /// <summary>
 167    /// Configures the issue claims for Basic authentication.
 168    /// </summary>
 169    /// <param name="host">The Kestrun host instance.</param>
 170    /// <param name="opts">The options to configure.</param>
 171    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 172    private static void ConfigureBasicIssueClaims(KestrunHost host, BasicAuthenticationOptions opts)
 173    {
 8174        var settings = opts.IssueClaimsCodeSettings;
 8175        if (string.IsNullOrWhiteSpace(settings.Code))
 176        {
 5177            return;
 178        }
 179
 3180        switch (settings.Language)
 181        {
 182            case ScriptLanguage.PowerShell:
 1183                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 184                {
 1185                    opts.Logger.Debug("Building PowerShell Issue Claims for API Basic authentication");
 186                }
 187
 1188                opts.IssueClaims = IAuthHandler.BuildPsIssueClaims(host, settings);
 1189                break;
 190            case ScriptLanguage.CSharp:
 1191                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 192                {
 1193                    opts.Logger.Debug("Building C# Issue Claims for API Basic authentication");
 194                }
 195
 1196                opts.IssueClaims = IAuthHandler.BuildCsIssueClaims(host, settings);
 1197                break;
 198            case ScriptLanguage.VBNet:
 1199                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 200                {
 1201                    opts.Logger.Debug("Building VB.NET Issue Claims for API Basic authentication");
 202                }
 203
 1204                opts.IssueClaims = IAuthHandler.BuildVBNetIssueClaims(host, settings);
 1205                break;
 206            default:
 0207                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 208                {
 0209                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 210                }
 0211                throw new NotSupportedException("Unsupported language");
 212        }
 213    }
 214
 215    #endregion
 216    #region GitHub OAuth Authentication
 217    /// <summary>
 218    /// Adds GitHub OAuth (Authorization Code) authentication with optional email enrichment.
 219    /// Creates three schemes: <paramref name="scheme"/>, <paramref name="scheme"/>.Cookies, <paramref name="scheme"/>.P
 220    /// </summary>
 221    /// <param name="host">The Kestrun host instance.</param>
 222    /// <param name="scheme">Base scheme name (e.g. "GitHub").</param>
 223    /// <param name="displayName">The display name for the authentication scheme.</param>
 224    /// <param name="documentationId">Documentation IDs for the authentication scheme.</param>
 225    /// <param name="description">A description of the authentication scheme.</param>
 226    /// <param name="deprecated">If true, marks the authentication scheme as deprecated in OpenAPI documentation.</param
 227    /// <param name="clientId">GitHub OAuth App Client ID.</param>
 228    /// <param name="clientSecret">GitHub OAuth App Client Secret.</param>
 229    /// <param name="callbackPath">The callback path for OAuth redirection (e.g. "/signin-github").</param>
 230    /// <returns>The configured KestrunHost.</returns>
 231    public static KestrunHost AddGitHubOAuthAuthentication(
 232        this KestrunHost host,
 233        string scheme,
 234        string? displayName,
 235        string[]? documentationId,
 236        string? description,
 237        bool deprecated,
 238        string clientId,
 239        string clientSecret,
 240        string callbackPath)
 241    {
 0242        var opts = ConfigureGitHubOAuth2Options(host, clientId, clientSecret, callbackPath);
 0243        ConfigureGitHubClaimMappings(opts);
 0244        opts.DocumentationId = documentationId ?? [];
 0245        if (!string.IsNullOrWhiteSpace(description))
 246        {
 0247            opts.Description = description;
 248        }
 0249        opts.Deprecated = deprecated;
 0250        opts.Events = new OAuthEvents
 0251        {
 0252            OnCreatingTicket = async context =>
 0253            {
 0254                await FetchGitHubUserInfoAsync(context);
 0255                await EnrichGitHubEmailClaimAsync(context, host);
 0256            }
 0257        };
 0258        return host.AddOAuth2Authentication(scheme, displayName, opts);
 259    }
 260
 261    /// <summary>
 262    /// Configures OAuth2Options for GitHub authentication.
 263    /// </summary>
 264    /// <param name="host">The Kestrun host instance.</param>
 265    /// <param name="clientId">GitHub OAuth App Client ID.</param>
 266    /// <param name="clientSecret">GitHub OAuth App Client Secret.</param>
 267    /// <param name="callbackPath">The callback path for OAuth redirection (e.g. "/signin-github").</param>
 268    /// <returns>The configured OAuth2Options.</returns>
 269    private static OAuth2Options ConfigureGitHubOAuth2Options(KestrunHost host, string clientId, string clientSecret, st
 270    {
 0271        return new OAuth2Options()
 0272        {
 0273            Host = host,
 0274            ClientId = clientId,
 0275            ClientSecret = clientSecret,
 0276            CallbackPath = callbackPath,
 0277            AuthorizationEndpoint = "https://github.com/login/oauth/authorize",
 0278            TokenEndpoint = "https://github.com/login/oauth/access_token",
 0279            UserInformationEndpoint = "https://api.github.com/user",
 0280            SaveTokens = true,
 0281            SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
 0282            Scope = { "read:user", "user:email" }
 0283        };
 284    }
 285
 286    /// <summary>
 287    /// Configures claim mappings for GitHub OAuth2Options.
 288    /// </summary>
 289    /// <param name="opts">The OAuth2Options to configure.</param>
 290    private static void ConfigureGitHubClaimMappings(OAuth2Options opts)
 291    {
 0292        opts.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
 0293        opts.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
 0294        opts.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
 0295        opts.ClaimActions.MapJsonKey("name", "name");
 0296        opts.ClaimActions.MapJsonKey("urn:github:login", "login");
 0297        opts.ClaimActions.MapJsonKey("urn:github:avatar_url", "avatar_url");
 0298        opts.ClaimActions.MapJsonKey("urn:github:html_url", "html_url");
 0299    }
 300
 301    /// <summary>
 302    /// Fetches GitHub user information and adds claims to the identity.
 303    /// </summary>
 304    /// <param name="context">The OAuthCreatingTicketContext.</param>
 305    /// <returns>A task representing the asynchronous operation.</returns>
 306    private static async Task FetchGitHubUserInfoAsync(OAuthCreatingTicketContext context)
 307    {
 0308        using var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
 0309        request.Headers.Accept.Add(new("application/json"));
 0310        request.Headers.Add("User-Agent", "KestrunOAuth/1.0");
 0311        request.Headers.Authorization = new("Bearer", context.AccessToken);
 312
 0313        using var response = await context.Backchannel.SendAsync(request,
 0314            HttpCompletionOption.ResponseHeadersRead,
 0315            context.HttpContext.RequestAborted);
 316
 0317        _ = response.EnsureSuccessStatusCode();
 318
 0319        using var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync(context.HttpContext.RequestAborted)
 0320        context.RunClaimActions(user.RootElement);
 0321    }
 322
 323    /// <summary>
 324    /// Fetches GitHub user emails and enriches the identity with the primary verified email claim.
 325    /// </summary>
 326    /// <param name="context">The OAuthCreatingTicketContext.</param>
 327    /// <param name="host">The KestrunHost instance for logging.</param>
 328    /// <returns>A task representing the asynchronous operation.</returns>
 329    private static async Task EnrichGitHubEmailClaimAsync(OAuthCreatingTicketContext context, KestrunHost host)
 330    {
 0331        if (context.Identity is null || context.Identity.HasClaim(c => c.Type == ClaimTypes.Email))
 332        {
 0333            return;
 334        }
 335
 336        try
 337        {
 0338            using var emailRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails");
 0339            emailRequest.Headers.Accept.Add(new("application/json"));
 0340            emailRequest.Headers.Add("User-Agent", "KestrunOAuth/1.0");
 0341            emailRequest.Headers.Authorization = new("Bearer", context.AccessToken);
 342
 0343            using var emailResponse = await context.Backchannel.SendAsync(emailRequest,
 0344                HttpCompletionOption.ResponseHeadersRead,
 0345                context.HttpContext.RequestAborted);
 346
 0347            if (!emailResponse.IsSuccessStatusCode)
 348            {
 0349                return;
 350            }
 351
 0352            using var emails = JsonDocument.Parse(await emailResponse.Content.ReadAsStringAsync(context.HttpContext.Requ
 0353            var primaryEmail = FindPrimaryVerifiedEmail(emails) ?? FindFirstVerifiedEmail(emails);
 354
 0355            if (!string.IsNullOrWhiteSpace(primaryEmail))
 356            {
 0357                context.Identity.AddClaim(new Claim(
 0358                    ClaimTypes.Email,
 0359                    primaryEmail,
 0360                    ClaimValueTypes.String,
 0361                    context.Options.ClaimsIssuer));
 362            }
 0363        }
 0364        catch (Exception ex)
 365        {
 0366            host.Logger.Verbose(exception: ex, messageTemplate: "Failed to enrich GitHub email claim.");
 0367        }
 0368    }
 369
 370    /// <summary>
 371    /// Finds the primary verified email from the GitHub emails JSON document.
 372    /// </summary>
 373    /// <param name="emails">The JSON document containing GitHub emails.</param>
 374    /// <returns>The primary verified email if found; otherwise, null.</returns>
 375    private static string? FindPrimaryVerifiedEmail(JsonDocument emails)
 376    {
 0377        foreach (var emailObj in emails.RootElement.EnumerateArray())
 378        {
 0379            var isPrimary = emailObj.TryGetProperty("primary", out var primaryProp) && primaryProp.GetBoolean();
 0380            var isVerified = emailObj.TryGetProperty("verified", out var verifiedProp) && verifiedProp.GetBoolean();
 381
 0382            if (isPrimary && isVerified && emailObj.TryGetProperty("email", out var emailProp))
 383            {
 0384                return emailProp.GetString();
 385            }
 386        }
 0387        return null;
 0388    }
 389
 390    /// <summary>
 391    /// Finds the primary verified email from the GitHub emails JSON document.
 392    /// </summary>
 393    /// <param name="emails">The JSON document containing GitHub emails.</param>
 394    /// <returns>The primary verified email if found; otherwise, null.</returns>
 395    private static string? FindFirstVerifiedEmail(JsonDocument emails)
 396    {
 0397        foreach (var emailObj in emails.RootElement.EnumerateArray())
 398        {
 0399            var isVerified = emailObj.TryGetProperty("verified", out var verifiedProp) && verifiedProp.GetBoolean();
 0400            if (isVerified && emailObj.TryGetProperty("email", out var emailProp))
 401            {
 0402                return emailProp.GetString();
 403            }
 404        }
 0405        return null;
 0406    }
 407
 408    #endregion
 409    #region JWT Bearer Authentication
 410    /// <summary>
 411    /// Adds JWT Bearer authentication to the Kestrun host.
 412    /// <para>Use this for APIs that require token-based authentication.</para>
 413    /// </summary>
 414    /// <param name="host">The Kestrun host instance.</param>
 415    /// <param name="authenticationScheme">The authentication scheme name (e.g. "Bearer").</param>
 416    /// <param name="displayName">The display name for the authentication scheme.</param>
 417    /// <param name="configureOptions">Optional configuration for JwtAuthOptions.</param>
 418    /// <example>
 419    /// HS512 (HMAC-SHA-512, symmetric)
 420    /// </example>
 421    /// <code>
 422    ///     var hmacKey = new SymmetricSecurityKey(
 423    ///         Encoding.UTF8.GetBytes("32-bytes-or-more-secret……"));
 424    ///     host.AddJwtBearerAuthentication(
 425    ///         scheme:          "Bearer",
 426    ///         issuer:          "KestrunApi",
 427    ///         audience:        "KestrunClients",
 428    ///         validationKey:   hmacKey,
 429    ///         validAlgorithms: new[] { SecurityAlgorithms.HmacSha512 });
 430    /// </code>
 431    /// <example>
 432    /// RS256 (RSA-SHA-256, asymmetric)
 433    /// <para>Requires a PEM-encoded private key file.</para>
 434    /// <code>
 435    ///    using var rsa = RSA.Create();
 436    ///     rsa.ImportFromPem(File.ReadAllText("private-key.pem"));
 437    ///     var rsaKey = new RsaSecurityKey(rsa);
 438    ///
 439    ///     host.AddJwtBearerAuthentication(
 440    ///         scheme:          "Rs256",
 441    ///         issuer:          "KestrunApi",
 442    ///         audience:        "KestrunClients",
 443    ///         validationKey:   rsaKey,
 444    ///         validAlgorithms: new[] { SecurityAlgorithms.RsaSha256 });
 445    /// </code>
 446    /// </example>
 447    /// <example>
 448    /// ES256 (ECDSA-SHA-256, asymmetric)
 449    /// <para>Requires a PEM-encoded private key file.</para>
 450    /// <code>
 451    ///     using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
 452    ///     var esKey = new ECDsaSecurityKey(ecdsa);
 453    ///     host.AddJwtBearerAuthentication(
 454    ///         "Es256", "KestrunApi", "KestrunClients",
 455    ///         esKey, new[] { SecurityAlgorithms.EcdsaSha256 });
 456    /// </code>
 457    /// </example>
 458    /// <returns></returns>
 459    public static KestrunHost AddJwtBearerAuthentication(
 460      this KestrunHost host,
 461      string authenticationScheme = AuthenticationDefaults.JwtBearerSchemeName,
 462      string? displayName = AuthenticationDefaults.JwtBearerDisplayName,
 463      Action<JwtAuthOptions>? configureOptions = null)
 464    {
 3465        ArgumentNullException.ThrowIfNull(configureOptions);
 466        // Build a prototype options instance (single source of truth)
 3467        var prototype = new JwtAuthOptions { Host = host };
 3468        configureOptions?.Invoke(prototype);
 3469        ConfigureOpenApi(host, authenticationScheme, prototype);
 470
 471        // register in host for introspection
 3472        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Bearer, prototype);
 473
 3474        return host.AddAuthentication(
 3475            defaultScheme: authenticationScheme,
 3476            buildSchemes: ab =>
 3477            {
 3478                _ = ab.AddJwtBearer(
 3479                    authenticationScheme: authenticationScheme,
 3480                    displayName: displayName,
 3481                    configureOptions: opts =>
 3482                {
 0483                    prototype.ApplyTo(opts);
 3484                });
 3485            },
 3486            configureAuthz: prototype.ClaimPolicy?.ToAuthzDelegate()
 3487            );
 488    }
 489
 490    /// <summary>
 491    /// Adds JWT Bearer authentication to the Kestrun host using the provided options object.
 492    /// </summary>
 493    /// <param name="host">The Kestrun host instance.</param>
 494    /// <param name="authenticationScheme">The authentication scheme name.</param>
 495    /// <param name="displayName">The display name for the authentication scheme.</param>
 496    /// <param name="configureOptions">Optional configuration for JwtAuthOptions.</param>
 497    /// <returns>The configured KestrunHost instance.</returns>
 498    public static KestrunHost AddJwtBearerAuthentication(
 499        this KestrunHost host,
 500        string authenticationScheme = AuthenticationDefaults.JwtBearerSchemeName,
 501        string? displayName = AuthenticationDefaults.JwtBearerDisplayName,
 502        JwtAuthOptions? configureOptions = null)
 503    {
 3504        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 505        {
 3506            host.Logger.Debug("Adding Jwt Bearer Authentication with scheme: {Scheme}", authenticationScheme);
 507        }
 508        // Ensure the scheme is not null
 3509        ArgumentNullException.ThrowIfNull(host);
 3510        ArgumentNullException.ThrowIfNull(authenticationScheme);
 3511        ArgumentNullException.ThrowIfNull(configureOptions);
 512
 513        // Ensure host is set
 3514        if (configureOptions.Host != host)
 515        {
 0516            configureOptions.Host = host;
 517        }
 518
 3519        return host.AddJwtBearerAuthentication(
 3520            authenticationScheme: authenticationScheme,
 3521              displayName: displayName,
 3522              configureOptions: opts =>
 3523              {
 3524                  // Copy relevant properties from provided options instance to the framework-created one
 3525                  configureOptions.ApplyTo(opts);
 3526                  host.Logger.Debug(
 3527                           "Configured JWT Authentication using scheme {Scheme}.",
 3528                           authenticationScheme);
 3529              }
 3530            );
 531    }
 532    #endregion
 533    #region Cookie Authentication
 534    /// <summary>
 535    /// Adds Cookie Authentication to the Kestrun host.
 536    /// <para>Use this for browser-based authentication using cookies.</para>
 537    /// </summary>
 538    /// <param name="host">The Kestrun host instance.</param>
 539    /// <param name="authenticationScheme">The authentication scheme name (default is CookieAuthenticationDefaults.Authe
 540    /// <param name="displayName">The display name for the authentication scheme.</param>
 541    /// <param name="configureOptions">Optional configuration for CookieAuthenticationOptions.</param>
 542    /// <param name="claimPolicy">Optional authorization policy configuration.</param>
 543    /// <returns>The configured KestrunHost instance.</returns>
 544    public static KestrunHost AddCookieAuthentication(
 545        this KestrunHost host,
 546        string authenticationScheme = AuthenticationDefaults.CookiesSchemeName,
 547        string? displayName = AuthenticationDefaults.CookiesDisplayName,
 548        Action<CookieAuthOptions>? configureOptions = null,
 549     ClaimPolicyConfig? claimPolicy = null)
 550    {
 551        // Build a prototype options instance (single source of truth)
 2552        var prototype = new CookieAuthOptions { Host = host };
 2553        configureOptions?.Invoke(prototype);
 2554        ConfigureOpenApi(host, authenticationScheme, prototype);
 555
 556        // register in host for introspection
 2557        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Cookie, prototype);
 558
 559        // Add authentication
 2560        return host.AddAuthentication(
 2561            defaultScheme: authenticationScheme,
 2562            buildSchemes: ab =>
 2563            {
 2564                _ = ab.AddCookie(
 2565                    authenticationScheme: authenticationScheme,
 2566                    displayName: displayName,
 2567                    configureOptions: opts =>
 2568                    {
 2569                        // Copy everything from the prototype into the real options instance
 0570                        prototype.ApplyTo(opts);
 2571                        // let caller mutate everything first
 2572                        //configure?.Invoke(opts);
 2573                    });
 2574            },
 2575            configureAuthz: claimPolicy?.ToAuthzDelegate()
 2576        );
 577    }
 578
 579    /// <summary>
 580    /// Adds Cookie Authentication to the Kestrun host using the provided options object.
 581    /// </summary>
 582    /// <param name="host">The Kestrun host instance.</param>
 583    /// <param name="authenticationScheme">The authentication scheme name (default is CookieAuthenticationDefaults.Authe
 584    /// <param name="displayName">The display name for the authentication scheme.</param>
 585    /// <param name="configureOptions">The CookieAuthenticationOptions object to configure the authentication.</param>
 586    /// <param name="claimPolicy">Optional authorization policy configuration.</param>
 587    /// <returns>The configured KestrunHost instance.</returns>
 588    public static KestrunHost AddCookieAuthentication(
 589          this KestrunHost host,
 590          string authenticationScheme = AuthenticationDefaults.CookiesSchemeName,
 591          string? displayName = AuthenticationDefaults.CookiesDisplayName,
 592          CookieAuthOptions? configureOptions = null,
 593       ClaimPolicyConfig? claimPolicy = null)
 594    {
 0595        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 596        {
 0597            host.Logger.Debug("Adding Cookie Authentication with scheme: {Scheme}", authenticationScheme);
 598        }
 599        // Ensure the scheme is not null
 0600        ArgumentNullException.ThrowIfNull(host);
 0601        ArgumentNullException.ThrowIfNull(authenticationScheme);
 0602        ArgumentNullException.ThrowIfNull(configureOptions);
 603        // Ensure host is set
 0604        if (configureOptions.Host != host)
 605        {
 0606            configureOptions.Host = host;
 607        }
 608        // Copy relevant properties from provided options instance to the framework-created one
 0609        return host.AddCookieAuthentication(
 0610            authenticationScheme: authenticationScheme,
 0611            displayName: displayName,
 0612            configureOptions: configureOptions.ApplyTo,
 0613            claimPolicy: claimPolicy
 0614        );
 615    }
 616    #endregion
 617
 618    /*
 619        public static KestrunHost AddClientCertificateAuthentication(
 620            this KestrunHost host,
 621            string scheme = CertificateAuthenticationDefaults.AuthenticationScheme,
 622            Action<CertificateAuthenticationOptions>? configure = null,
 623            Action<AuthorizationOptions>? configureAuthz = null)
 624        {
 625            return host.AddAuthentication(
 626                defaultScheme: scheme,
 627                buildSchemes: ab =>
 628                {
 629                    ab.AddCertificate(
 630                        authenticationScheme: scheme,
 631                        configureOptions: configure ?? (opts => { }));
 632                },
 633                configureAuthz: configureAuthz
 634            );
 635        }
 636    */
 637
 638    #region Windows Authentication
 639
 640    /// <summary>
 641    /// Adds Windows Authentication to the Kestrun host.
 642    /// <para>The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 643    /// This enables Kerberos and NTLM authentication.</para>
 644    /// </summary>
 645    /// <param name="host">The Kestrun host instance.</param>
 646    /// <param name="authenticationScheme">The authentication scheme name (default is NegotiateDefaults.AuthenticationSc
 647    /// <param name="displayName">The display name for the authentication scheme.</param>
 648    /// <param name="configureOptions">The WindowsAuthOptions object to configure the authentication.</param>
 649    /// <returns>The configured KestrunHost instance.</returns>
 650    public static KestrunHost AddWindowsAuthentication(
 651       this KestrunHost host,
 652       string authenticationScheme = AuthenticationDefaults.WindowsSchemeName,
 653       string? displayName = AuthenticationDefaults.WindowsDisplayName,
 654       Action<WindowsAuthOptions>? configureOptions = null)
 655    {
 656        // Build a prototype options instance (single source of truth)
 1657        var prototype = new WindowsAuthOptions { Host = host };
 1658        configureOptions?.Invoke(prototype);
 1659        ConfigureOpenApi(host, authenticationScheme, prototype);
 660
 661        // register in host for introspection
 1662        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Cookie, prototype);
 663
 664        // Add authentication
 1665        return host.AddAuthentication(
 1666            defaultScheme: authenticationScheme,
 1667            buildSchemes: ab =>
 1668            {
 1669                _ = ab.AddNegotiate(
 1670                    authenticationScheme: authenticationScheme,
 1671                    displayName: displayName,
 1672                    configureOptions: opts =>
 1673                    {
 1674                        // Copy everything from the prototype into the real options instance
 0675                        prototype.ApplyTo(opts);
 1676
 0677                        host.Logger.Debug("Configured Windows Authentication using scheme {Scheme}", authenticationSchem
 0678                    }
 1679                );
 1680            }
 1681        );
 682    }
 683    /// <summary>
 684    /// Adds Windows Authentication to the Kestrun host.
 685    /// <para>
 686    /// The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 687    /// This enables Kerberos and NTLM authentication.
 688    /// </para>
 689    /// </summary>
 690    /// <param name="host">The Kestrun host instance.</param>
 691    /// <param name="authenticationScheme">The authentication scheme name (default is NegotiateDefaults.AuthenticationSc
 692    /// <param name="displayName">The display name for the authentication scheme.</param>
 693    /// <param name="configureOptions">The WindowsAuthOptions object to configure the authentication.</param>
 694    /// <returns>The configured KestrunHost instance.</returns>
 695    public static KestrunHost AddWindowsAuthentication(
 696        this KestrunHost host,
 697        string authenticationScheme = AuthenticationDefaults.WindowsSchemeName,
 698        string? displayName = AuthenticationDefaults.WindowsDisplayName,
 699        WindowsAuthOptions? configureOptions = null)
 700    {
 0701        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 702        {
 0703            host.Logger.Debug("Adding Windows Authentication with scheme: {Scheme}", authenticationScheme);
 704        }
 705        // Ensure the scheme is not null
 0706        ArgumentNullException.ThrowIfNull(host);
 0707        ArgumentNullException.ThrowIfNull(configureOptions);
 708        // Ensure host is set
 0709        if (configureOptions.Host != host)
 710        {
 0711            configureOptions.Host = host;
 712        }
 713        // Copy relevant properties from provided options instance to the framework-created one
 714        // Add authentication
 0715        return host.AddWindowsAuthentication(
 0716           authenticationScheme: authenticationScheme,
 0717           displayName: displayName,
 0718           configureOptions: configureOptions.ApplyTo
 0719       );
 720    }
 721
 722    /// <summary>
 723    /// Adds Windows Authentication to the Kestrun host.
 724    /// <para>The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 725    /// This enables Kerberos and NTLM authentication.</para>
 726    /// </summary>
 727    /// <param name="host"> The Kestrun host instance.</param>
 728    /// <returns> The configured KestrunHost instance.</returns>
 729    public static KestrunHost AddWindowsAuthentication(this KestrunHost host) =>
 1730        host.AddWindowsAuthentication(
 1731            AuthenticationDefaults.WindowsSchemeName,
 1732            AuthenticationDefaults.WindowsDisplayName,
 1733            (Action<WindowsAuthOptions>?)null);
 734
 735    #endregion
 736
 737    #region Client Certificate Authentication
 738
 739    /// <summary>
 740    /// Adds Client Certificate Authentication to the Kestrun host.
 741    /// <para>Use this for authenticating clients using X.509 certificates.</para>
 742    /// </summary>
 743    /// <param name="host">The Kestrun host instance.</param>
 744    /// <param name="scheme">The authentication scheme name (default is "Certificate").</param>
 745    /// <param name="displayName">The display name for the authentication scheme.</param>
 746    /// <param name="configure">Optional configuration for ClientCertificateAuthenticationOptions.</param>
 747    /// <returns>The configured KestrunHost instance.</returns>
 748    public static KestrunHost AddClientCertificateAuthentication(
 749        this KestrunHost host,
 750        string scheme = AuthenticationDefaults.CertificateSchemeName,
 751        string? displayName = AuthenticationDefaults.CertificateDisplayName,
 752        Action<ClientCertificateAuthenticationOptions>? configure = null)
 753    {
 754        // Build a prototype options instance (single source of truth)
 1755        var prototype = new ClientCertificateAuthenticationOptions { Host = host };
 756
 757        // Let the caller mutate the prototype
 1758        configure?.Invoke(prototype);
 759
 1760        ConfigureOpenApi(host, scheme, prototype);
 761
 762        // Register in host for introspection
 1763        _ = host.RegisteredAuthentications.Register(scheme, AuthenticationType.Certificate, prototype);
 764
 1765        return host.AddAuthentication(
 1766            defaultScheme: scheme,
 1767            buildSchemes: ab =>
 1768            {
 1769                _ = ab.AddScheme<ClientCertificateAuthenticationOptions, ClientCertificateAuthHandler>(
 1770                    authenticationScheme: scheme,
 1771                    displayName: displayName,
 1772                    configureOptions: opts =>
 1773                    {
 1774                        // Copy from the prototype into the runtime instance
 0775                        prototype.ApplyTo(opts);
 1776
 0777                        host.Logger.Debug("Configured Client Certificate Authentication using scheme {Scheme}", scheme);
 1778                    });
 1779            }
 1780        );
 781    }
 782
 783    /// <summary>
 784    /// Adds Client Certificate Authentication to the Kestrun host using the provided options object.
 785    /// </summary>
 786    /// <param name="host">The Kestrun host instance.</param>
 787    /// <param name="scheme">The authentication scheme name (default is "Certificate").</param>
 788    /// <param name="displayName">The display name for the authentication scheme.</param>
 789    /// <param name="configure">The ClientCertificateAuthenticationOptions object to configure the authentication.</para
 790    /// <returns>The configured KestrunHost instance.</returns>
 791    public static KestrunHost AddClientCertificateAuthentication(
 792        this KestrunHost host,
 793        string scheme,
 794        string? displayName,
 795        ClientCertificateAuthenticationOptions configure)
 796    {
 0797        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 798        {
 0799            host.Logger.Debug("Adding Client Certificate Authentication with scheme: {Scheme}", scheme);
 800        }
 801
 802        // Ensure the scheme is not null
 0803        ArgumentNullException.ThrowIfNull(host);
 0804        ArgumentNullException.ThrowIfNull(scheme);
 0805        ArgumentNullException.ThrowIfNull(configure);
 806
 807        // Ensure host is set
 0808        if (configure.Host != host)
 809        {
 0810            configure.Host = host;
 811        }
 812
 0813        return host.AddClientCertificateAuthentication(
 0814            scheme: scheme,
 0815            displayName: displayName,
 0816            configure: configure.ApplyTo
 0817        );
 818    }
 819
 820    /// <summary>
 821    /// Adds Client Certificate Authentication to the Kestrun host with default settings.
 822    /// </summary>
 823    /// <param name="host">The Kestrun host instance.</param>
 824    /// <returns>The configured KestrunHost instance.</returns>
 825    public static KestrunHost AddClientCertificateAuthentication(this KestrunHost host) =>
 0826        host.AddClientCertificateAuthentication(
 0827            AuthenticationDefaults.CertificateSchemeName,
 0828            AuthenticationDefaults.CertificateDisplayName,
 0829            (Action<ClientCertificateAuthenticationOptions>?)null);
 830
 831    #endregion
 832    #region API Key Authentication
 833    /// <summary>
 834    /// Adds API Key Authentication to the Kestrun host.
 835    /// <para>Use this for endpoints that require an API key for access.</para>
 836    /// </summary>
 837    /// <param name="host">The Kestrun host instance.</param>
 838    /// <param name="authenticationScheme">The authentication scheme name (default is "ApiKey").</param>
 839    /// <param name="displayName">The display name for the authentication scheme (default is "API Key").</param>
 840    /// <param name="configureOptions">Optional configuration for ApiKeyAuthenticationOptions.</param>
 841    /// <returns>The configured KestrunHost instance.</returns>
 842    public static KestrunHost AddApiKeyAuthentication(
 843    this KestrunHost host,
 844    string authenticationScheme = AuthenticationDefaults.ApiKeySchemeName,
 845    string? displayName = AuthenticationDefaults.ApiKeyDisplayName,
 846    Action<ApiKeyAuthenticationOptions>? configureOptions = null)
 847    {
 848        // Build a prototype options instance (single source of truth)
 6849        var prototype = new ApiKeyAuthenticationOptions { Host = host };
 850
 851        // Let the caller mutate the prototype
 6852        configureOptions?.Invoke(prototype);
 853
 854        // Configure validators / claims / OpenAPI on the prototype
 6855        ConfigureApiKeyValidators(host, prototype);
 6856        ConfigureApiKeyIssueClaims(host, prototype);
 6857        ConfigureOpenApi(host, authenticationScheme, prototype);
 858
 859        // register in host for introspection
 6860        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.ApiKey, prototype);
 861        // Add authentication
 6862        return host.AddAuthentication(
 6863             defaultScheme: authenticationScheme,
 6864             buildSchemes: ab =>
 6865             {
 6866                 // ← TOptions == ApiKeyAuthenticationOptions
 6867                 //    THandler == ApiKeyAuthHandler
 6868                 _ = ab.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthHandler>(
 6869                     authenticationScheme: authenticationScheme,
 6870                     displayName: displayName,
 6871                     configureOptions: opts =>
 6872                     {
 6873                         // Copy from the prototype into the runtime instance
 6874                         prototype.ApplyTo(opts);
 6875
 6876                         host.Logger.Debug(
 6877                             "Configured API Key Authentication using scheme {Scheme} with header {Header} (In={In})",
 6878                             authenticationScheme, prototype.ApiKeyName, prototype.In);
 12879                     });
 6880             }
 6881         )
 6882        //  register the post-configurer **after** the scheme so it can
 6883        //    read BasicAuthenticationOptions for <scheme>
 6884        .AddService(services =>
 6885          {
 6886              _ = services.AddSingleton<IPostConfigureOptions<AuthorizationOptions>>(
 9887                  sp => new ClaimPolicyPostConfigurer(
 9888                            authenticationScheme,
 9889                            sp.GetRequiredService<
 9890                                IOptionsMonitor<ApiKeyAuthenticationOptions>>()));
 12891          });
 892    }
 893
 894    /// <summary>
 895    /// Adds API Key Authentication to the Kestrun host using the provided options object.
 896    /// </summary>
 897    /// <param name="host">The Kestrun host instance.</param>
 898    /// <param name="authenticationScheme">The authentication scheme name.</param>
 899    /// <param name="displayName">The display name for the authentication scheme.</param>
 900    /// <param name="configureOptions">The ApiKeyAuthenticationOptions object to configure the authentication.</param>
 901    /// <returns>The configured KestrunHost instance.</returns>
 902    public static KestrunHost AddApiKeyAuthentication(
 903    this KestrunHost host,
 904    string authenticationScheme = AuthenticationDefaults.ApiKeySchemeName,
 905    string? displayName = AuthenticationDefaults.ApiKeyDisplayName,
 906    ApiKeyAuthenticationOptions? configureOptions = null)
 907    {
 1908        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 909        {
 1910            host.Logger.Debug("Adding API Key Authentication with scheme: {Scheme}", authenticationScheme);
 911        }
 912        // Ensure the scheme is not null
 1913        ArgumentNullException.ThrowIfNull(host);
 1914        ArgumentNullException.ThrowIfNull(authenticationScheme);
 1915        ArgumentNullException.ThrowIfNull(configureOptions);
 916        // Ensure host is set
 1917        if (configureOptions.Host != host)
 918        {
 1919            configureOptions.Host = host;
 920        }
 921        // Copy properties from the provided configure object
 1922        return host.AddApiKeyAuthentication(
 1923            authenticationScheme: authenticationScheme,
 1924            displayName: displayName,
 1925            configureOptions: configureOptions.ApplyTo
 1926        );
 927    }
 928
 929    /// <summary>
 930    /// Configures the API Key validators.
 931    /// </summary>
 932    /// <param name="host">The Kestrun host instance.</param>
 933    /// <param name="opts">The options to configure.</param>
 934    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 935    private static void ConfigureApiKeyValidators(KestrunHost host, ApiKeyAuthenticationOptions opts)
 936    {
 6937        var settings = opts.ValidateCodeSettings;
 6938        if (string.IsNullOrWhiteSpace(settings.Code))
 939        {
 3940            return;
 941        }
 942
 3943        switch (settings.Language)
 944        {
 945            case ScriptLanguage.PowerShell:
 1946                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 947                {
 1948                    opts.Logger.Debug("Building PowerShell validator for API Key authentication");
 949                }
 950
 1951                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildPsValidator(host, settings);
 1952                break;
 953            case ScriptLanguage.CSharp:
 1954                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 955                {
 1956                    opts.Logger.Debug("Building C# validator for API Key authentication");
 957                }
 958
 1959                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildCsValidator(host, settings);
 1960                break;
 961            case ScriptLanguage.VBNet:
 1962                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 963                {
 1964                    opts.Logger.Debug("Building VB.NET validator for API Key authentication");
 965                }
 966
 1967                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildVBNetValidator(host, settings);
 1968                break;
 969            default:
 0970                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 971                {
 0972                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 973                }
 0974                throw new NotSupportedException("Unsupported language");
 975        }
 976    }
 977
 978    /// <summary>
 979    /// Configures the API Key issue claims.
 980    /// </summary>
 981    /// <param name="host">The Kestrun host instance.</param>
 982    /// <param name="opts">The options to configure.</param>
 983    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 984    private static void ConfigureApiKeyIssueClaims(KestrunHost host, ApiKeyAuthenticationOptions opts)
 985    {
 6986        var settings = opts.IssueClaimsCodeSettings;
 6987        if (string.IsNullOrWhiteSpace(settings.Code))
 988        {
 3989            return;
 990        }
 991
 3992        switch (settings.Language)
 993        {
 994            case ScriptLanguage.PowerShell:
 1995                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 996                {
 1997                    opts.Logger.Debug("Building PowerShell Issue Claims for API Key authentication");
 998                }
 999
 11000                opts.IssueClaims = IAuthHandler.BuildPsIssueClaims(host, settings);
 11001                break;
 1002            case ScriptLanguage.CSharp:
 11003                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 1004                {
 11005                    opts.Logger.Debug("Building C# Issue Claims for API Key authentication");
 1006                }
 1007
 11008                opts.IssueClaims = IAuthHandler.BuildCsIssueClaims(host, settings);
 11009                break;
 1010            case ScriptLanguage.VBNet:
 11011                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 1012                {
 11013                    opts.Logger.Debug("Building VB.NET Issue Claims for API Key authentication");
 1014                }
 1015
 11016                opts.IssueClaims = IAuthHandler.BuildVBNetIssueClaims(host, settings);
 11017                break;
 1018            default:
 01019                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 1020                {
 01021                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 1022                }
 01023                throw new NotSupportedException("Unsupported language");
 1024        }
 1025    }
 1026
 1027    #endregion
 1028
 1029    #region OAuth2 Authentication
 1030
 1031    /// <summary>
 1032    /// Adds OAuth2 authentication to the Kestrun host.
 1033    /// <para>Use this for applications that require OAuth2 authentication.</para>
 1034    /// </summary>
 1035    /// <param name="host">The Kestrun host instance.</param>
 1036    /// <param name="authenticationScheme">The authentication scheme name.</param>
 1037    /// <param name="displayName">The display name for the authentication scheme.</param>
 1038    /// <param name="configureOptions">The OAuth2Options to configure the authentication.</param>
 1039    /// <returns>The configured KestrunHost instance.</returns>
 1040    public static KestrunHost AddOAuth2Authentication(
 1041        this KestrunHost host,
 1042        string authenticationScheme = AuthenticationDefaults.OAuth2SchemeName,
 1043        string? displayName = AuthenticationDefaults.OAuth2DisplayName,
 1044        OAuth2Options? configureOptions = null)
 1045    {
 01046        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1047        {
 01048            host.Logger.Debug("Adding OAuth2 Authentication with scheme: {Scheme}", authenticationScheme);
 1049        }
 1050        // Ensure the scheme is not null
 01051        ArgumentNullException.ThrowIfNull(host);
 01052        ArgumentNullException.ThrowIfNull(authenticationScheme);
 01053        ArgumentNullException.ThrowIfNull(configureOptions);
 1054
 1055        // Required for OAuth2
 01056        if (string.IsNullOrWhiteSpace(configureOptions.ClientId))
 1057        {
 01058            throw new ArgumentException("ClientId must be provided in OAuth2Options", nameof(configureOptions));
 1059        }
 1060
 01061        if (string.IsNullOrWhiteSpace(configureOptions.AuthorizationEndpoint))
 1062        {
 01063            throw new ArgumentException("AuthorizationEndpoint must be provided in OAuth2Options", nameof(configureOptio
 1064        }
 1065
 01066        if (string.IsNullOrWhiteSpace(configureOptions.TokenEndpoint))
 1067        {
 01068            throw new ArgumentException("TokenEndpoint must be provided in OAuth2Options", nameof(configureOptions));
 1069        }
 1070
 1071        // Default CallbackPath if not set: /signin-{scheme}
 01072        if (string.IsNullOrWhiteSpace(configureOptions.CallbackPath))
 1073        {
 01074            configureOptions.CallbackPath = $"/signin-{authenticationScheme.ToLowerInvariant()}";
 1075        }
 1076        // Ensure host is set
 01077        if (configureOptions.Host != host)
 1078        {
 01079            configureOptions.Host = host;
 1080        }
 1081        // Ensure scheme is set
 01082        if (authenticationScheme != configureOptions.AuthenticationScheme)
 1083        {
 01084            configureOptions.AuthenticationScheme = authenticationScheme;
 1085        }
 1086        // Configure scopes and claim policies
 01087        ConfigureScopes(configureOptions, host.Logger);
 1088        // Configure OpenAPI
 01089        ConfigureOpenApi(host, authenticationScheme, configureOptions);
 1090
 1091        // register in host for introspection
 01092        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.OAuth2, configureOptions);
 1093
 1094        // Add authentication
 01095        return host.AddAuthentication(
 01096            defaultScheme: configureOptions.CookieScheme,
 01097            defaultChallengeScheme: authenticationScheme,
 01098            buildSchemes: ab =>
 01099            {
 01100                // Add cookie scheme for sign-in
 01101                _ = ab.AddCookie(configureOptions.CookieScheme, cookieOpts =>
 01102               {
 01103                   configureOptions.CookieOptions.ApplyTo(cookieOpts);
 01104               });
 01105                // Add OAuth2 scheme
 01106                _ = ab.AddOAuth(
 01107                    authenticationScheme: authenticationScheme,
 01108                    displayName: displayName ?? OAuthDefaults.DisplayName,
 01109                    configureOptions: oauthOpts =>
 01110                {
 01111                    configureOptions.ApplyTo(oauthOpts);
 01112                    if (host.Logger.IsEnabled(LogEventLevel.Debug))
 01113                    {
 01114                        host.Logger.Debug("Configured OpenID Connect with ClientId: {ClientId}, Scopes: {Scopes}",
 01115                          oauthOpts.ClientId, string.Join(", ", oauthOpts.Scope));
 01116                    }
 01117                });
 01118            },
 01119              configureAuthz: configureOptions.ClaimPolicy?.ToAuthzDelegate()
 01120        );
 1121    }
 1122
 1123    /// <summary>
 1124    /// Configures OAuth2 scopes and claim policies.
 1125    /// </summary>
 1126    /// <param name="configureOptions">The OAuth2 options to configure.</param>
 1127    /// <param name="logger">The logger for debug output.</param>
 1128    private static void ConfigureScopes(IOAuthCommonOptions configureOptions, Serilog.ILogger logger)
 1129    {
 31130        if (configureOptions.Scope is null)
 1131        {
 01132            return;
 1133        }
 1134
 31135        if (configureOptions.Scope.Count == 0)
 1136        {
 11137            BackfillScopesFromClaimPolicy(configureOptions, logger);
 11138            return;
 1139        }
 1140
 21141        LogConfiguredScopes(configureOptions.Scope, logger);
 1142
 21143        if (configureOptions.ClaimPolicy is null)
 1144        {
 11145            configureOptions.ClaimPolicy = BuildClaimPolicyFromScopes(configureOptions.Scope, logger);
 11146            return;
 1147        }
 1148
 11149        AddMissingScopesToClaimPolicy(configureOptions.Scope, configureOptions.ClaimPolicy, logger);
 11150    }
 1151
 1152    private static ClaimPolicyConfig BuildClaimPolicyFromScopes(ICollection<string> scopes, Serilog.ILogger logger)
 1153    {
 11154        var claimPolicyBuilder = new ClaimPolicyBuilder();
 61155        foreach (var scope in scopes)
 1156        {
 21157            LogScopeAdded(logger, scope);
 21158            _ = claimPolicyBuilder.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedVa
 1159        }
 1160
 11161        return claimPolicyBuilder.Build();
 1162    }
 1163
 1164    private static void AddMissingScopesToClaimPolicy(ICollection<string> scopes, ClaimPolicyConfig claimPolicy, Serilog
 1165    {
 11166        var missingScopes = scopes
 21167            .Where(s => !claimPolicy.Policies.ContainsKey(s))
 11168            .ToList();
 1169
 11170        if (missingScopes.Count == 0)
 1171        {
 01172            return;
 1173        }
 1174
 11175        LogMissingScopes(missingScopes, logger);
 1176
 11177        var claimPolicyBuilder = new ClaimPolicyBuilder();
 41178        foreach (var scope in missingScopes)
 1179        {
 11180            _ = claimPolicyBuilder.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedVa
 11181            LogScopeAddedToClaimPolicy(logger, scope);
 1182        }
 1183
 11184        claimPolicy.AddPolicies(claimPolicyBuilder.Policies);
 11185    }
 1186
 1187    private static void BackfillScopesFromClaimPolicy(IOAuthCommonOptions configureOptions, Serilog.ILogger logger)
 1188    {
 11189        if (configureOptions.ClaimPolicy is null)
 1190        {
 01191            return;
 1192        }
 1193
 61194        foreach (var policy in configureOptions.ClaimPolicy.PolicyNames)
 1195        {
 21196            LogClaimPolicyConfigured(logger, policy);
 21197            configureOptions.Scope?.Add(policy);
 1198        }
 11199    }
 1200
 1201    private static void LogScopeAdded(Serilog.ILogger logger, string scope)
 1202    {
 21203        if (logger.IsEnabled(LogEventLevel.Debug))
 1204        {
 21205            logger.Debug("OAuth2 scope added: {Scope}", scope);
 1206        }
 21207    }
 1208
 1209    private static void LogScopeAddedToClaimPolicy(Serilog.ILogger logger, string scope)
 1210    {
 11211        if (logger.IsEnabled(LogEventLevel.Debug))
 1212        {
 11213            logger.Debug("OAuth2 scope added to claim policy: {Scope}", scope);
 1214        }
 11215    }
 1216
 1217    private static void LogMissingScopes(IEnumerable<string> missingScopes, Serilog.ILogger logger)
 1218    {
 11219        if (logger.IsEnabled(LogEventLevel.Debug))
 1220        {
 11221            logger.Debug("Adding missing OAuth2 scopes to claim policy: {Scopes}", string.Join(", ", missingScopes));
 1222        }
 11223    }
 1224
 1225    private static void LogConfiguredScopes(IEnumerable<string> scopes, Serilog.ILogger logger)
 1226    {
 21227        if (logger.IsEnabled(LogEventLevel.Debug))
 1228        {
 21229            logger.Debug("OAuth2 scopes configured: {Scopes}", string.Join(", ", scopes));
 1230        }
 21231    }
 1232
 1233    private static void LogClaimPolicyConfigured(Serilog.ILogger logger, string policy)
 1234    {
 21235        if (logger.IsEnabled(LogEventLevel.Debug))
 1236        {
 21237            logger.Debug("OAuth2 claim policy configured: {Policy}", policy);
 1238        }
 21239    }
 1240
 1241    #endregion
 1242    #region OpenID Connect Authentication
 1243
 1244    /// <summary>
 1245    /// Adds OpenID Connect authentication to the Kestrun host with private key JWT client assertion.
 1246    /// <para>Use this for applications that require OpenID Connect authentication with client credentials using JWT ass
 1247    /// </summary>
 1248    /// <param name="host">The Kestrun host instance.</param>
 1249    /// <param name="authenticationScheme">The authentication scheme name.</param>
 1250    /// <param name="displayName">The display name for the authentication scheme.</param>
 1251    /// <param name="configureOptions">The OpenIdConnectOptions to configure the authentication.</param>
 1252    /// <returns>The configured KestrunHost instance.</returns>
 1253    public static KestrunHost AddOpenIdConnectAuthentication(
 1254           this KestrunHost host,
 1255           string authenticationScheme = AuthenticationDefaults.OidcSchemeName,
 1256           string? displayName = AuthenticationDefaults.OidcDisplayName,
 1257           OidcOptions? configureOptions = null)
 1258    {
 01259        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1260        {
 01261            host.Logger.Debug("Adding OpenID Connect Authentication with scheme: {Scheme}", authenticationScheme);
 1262        }
 1263        // Ensure the scheme is not null
 01264        ArgumentNullException.ThrowIfNull(host);
 01265        ArgumentNullException.ThrowIfNull(authenticationScheme);
 01266        ArgumentNullException.ThrowIfNull(configureOptions);
 1267
 1268        // Ensure ClientId is set
 01269        if (string.IsNullOrWhiteSpace(configureOptions.ClientId))
 1270        {
 01271            throw new ArgumentException("ClientId must be provided in OpenIdConnectOptions", nameof(configureOptions));
 1272        }
 1273        // Ensure host is set
 01274        if (configureOptions.Host != host)
 1275        {
 01276            configureOptions.Host = host;
 1277        }
 1278        // Ensure scheme is set
 01279        if (authenticationScheme != configureOptions.AuthenticationScheme)
 1280        {
 01281            configureOptions.AuthenticationScheme = authenticationScheme;
 1282        }
 1283        // Retrieve supported scopes from the OIDC provider
 01284        if (!string.IsNullOrWhiteSpace(configureOptions.Authority))
 1285        {
 01286            configureOptions.ClaimPolicy = GetSupportedScopes(configureOptions.Authority, host.Logger);
 01287            if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1288            {
 01289                host.Logger.Debug("OIDC supported scopes: {Scopes}", string.Join(", ", configureOptions.ClaimPolicy?.Pol
 1290            }
 1291        }
 1292        // Configure scopes and claim policies
 01293        ConfigureScopes(configureOptions, host.Logger);
 1294        // Configure OpenAPI
 01295        ConfigureOpenApi(host, authenticationScheme, configureOptions);
 1296
 1297        // register in host for introspection
 01298        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Oidc, configureOptions);
 1299
 1300        // CRITICAL: Register OidcEvents and AssertionService in DI before configuring authentication
 1301        // This is required because EventsType expects these to be available in the service provider
 01302        return host.AddService(services =>
 01303         {
 01304             // Register AssertionService as a singleton with factory to pass clientId and jwkJson
 01305             // Only register if JwkJson is provided (for private_key_jwt authentication)
 01306             if (!string.IsNullOrWhiteSpace(configureOptions.JwkJson))
 01307             {
 01308                 services.TryAddSingleton(sp => new AssertionService(configureOptions.ClientId, configureOptions.JwkJson
 01309                 // Register OidcEvents as scoped (per-request)
 01310                 services.TryAddScoped<OidcEvents>();
 01311             }
 01312         }).AddAuthentication(
 01313              defaultScheme: configureOptions.CookieScheme,
 01314              defaultChallengeScheme: authenticationScheme,
 01315              buildSchemes: ab =>
 01316              {
 01317                  // Add cookie scheme for sign-in
 01318                  _ = ab.AddCookie(configureOptions.CookieScheme, cookieOpts =>
 01319                 {
 01320                     // Copy cookie configuration from options.CookieOptions
 01321                     configureOptions.CookieOptions.ApplyTo(cookieOpts);
 01322                 });
 01323                  // Add OpenID Connect scheme
 01324                  _ = ab.AddOpenIdConnect(
 01325                    authenticationScheme: authenticationScheme,
 01326                    displayName: displayName ?? OpenIdConnectDefaults.DisplayName,
 01327                    configureOptions: oidcOpts =>
 01328                 {
 01329                     // Copy all properties from the provided options to the framework's options
 01330                     configureOptions.ApplyTo(oidcOpts);
 01331
 01332                     // Inject private key JWT at code → token step (only if JwkJson is provided)
 01333                     // This will be resolved from DI at runtime
 01334                     if (!string.IsNullOrWhiteSpace(configureOptions.JwkJson))
 01335                     {
 01336                         oidcOpts.EventsType = typeof(OidcEvents);
 01337                     }
 01338                     if (host.Logger.IsEnabled(LogEventLevel.Debug))
 01339                     {
 01340                         host.Logger.Debug("Configured OpenID Connect with Authority: {Authority}, ClientId: {ClientId},
 01341                             oidcOpts.Authority, oidcOpts.ClientId, string.Join(", ", oidcOpts.Scope));
 01342                     }
 01343                 });
 01344              },
 01345              configureAuthz: configureOptions.ClaimPolicy?.ToAuthzDelegate()
 01346            );
 1347    }
 1348
 1349    /// <summary>
 1350    /// Retrieves the supported scopes from the OpenID Connect provider's metadata.
 1351    /// </summary>
 1352    /// <param name="authority">The authority URL of the OpenID Connect provider.</param>
 1353    /// <param name="logger">The logger instance for logging.</param>
 1354    /// <returns>A ClaimPolicyConfig containing the supported scopes, or null if retrieval fails.</returns>
 1355    private static ClaimPolicyConfig? GetSupportedScopes(string authority, Serilog.ILogger logger)
 1356    {
 01357        if (logger.IsEnabled(LogEventLevel.Debug))
 1358        {
 01359            logger.Debug("Retrieving OpenID Connect configuration from authority: {Authority}", authority);
 1360        }
 01361        var claimPolicy = new ClaimPolicyBuilder();
 01362        if (string.IsNullOrWhiteSpace(authority))
 1363        {
 01364            throw new ArgumentException("Authority must be provided to retrieve OpenID Connect scopes.", nameof(authorit
 1365        }
 1366
 01367        var metadataAddress = authority.TrimEnd('/') + "/.well-known/openid-configuration";
 1368
 01369        var documentRetriever = new HttpDocumentRetriever
 01370        {
 01371            RequireHttps = metadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
 01372        };
 1373
 01374        var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
 01375            metadataAddress,
 01376            new OpenIdConnectConfigurationRetriever(),
 01377            documentRetriever);
 1378
 1379        try
 1380        {
 01381            var cfg = configManager.GetConfigurationAsync(CancellationToken.None)
 01382                                   .GetAwaiter()
 01383                                   .GetResult();
 1384            // First try the strongly-typed property
 01385            var scopes = cfg.ScopesSupported;
 1386
 1387            // If it's null or empty, fall back to raw JSON
 01388            if (scopes == null || scopes.Count == 0)
 1389            {
 01390                var json = documentRetriever.GetDocumentAsync(metadataAddress, CancellationToken.None)
 01391                                            .GetAwaiter()
 01392                                            .GetResult();
 1393
 01394                using var doc = JsonDocument.Parse(json);
 01395                if (doc.RootElement.TryGetProperty("scopes_supported", out var scopesElement) &&
 01396                    scopesElement.ValueKind == JsonValueKind.Array)
 1397                {
 01398                    foreach (var scope in scopesElement.EnumerateArray().Select(item => item.GetString()).Where(s => !st
 1399                    {
 01400                        if (scope != null)
 1401                        {
 01402                            _ = claimPolicy.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, 
 1403                        }
 1404                    }
 1405                }
 1406            }
 1407            else
 1408            {
 1409                // Normal path: configuration object had scopes
 01410                foreach (var scope in scopes)
 1411                {
 01412                    _ = claimPolicy.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedV
 1413                }
 1414            }
 01415            return claimPolicy.Build();
 1416        }
 01417        catch (Exception ex)
 1418        {
 01419            logger.Warning(ex, "Failed to retrieve OpenID Connect configuration from {MetadataAddress}", metadataAddress
 01420            return null;
 1421        }
 01422    }
 1423
 1424    #endregion
 1425    #region Helper Methods
 1426    /// <summary>
 1427    /// Configures OpenAPI security schemes for the given authentication options.
 1428    /// </summary>
 1429    /// <param name="host">The Kestrun host instance.</param>
 1430    /// <param name="scheme">The authentication scheme name.</param>
 1431    /// <param name="opts">The OpenAPI authentication options.</param>
 1432    private static void ConfigureOpenApi(KestrunHost host, string scheme, IOpenApiAuthenticationOptions opts)
 1433    {
 1434        // Apply to specified documentation IDs or all if none specified
 211435        if (opts.DocumentationId == null || opts.DocumentationId.Length == 0)
 1436        {
 211437            opts.DocumentationId = OpenApiDocDescriptor.DefaultDocumentationIds;
 1438        }
 1439
 841440        foreach (var docDescriptor in opts.DocumentationId
 211441            .Select(host.GetOrCreateOpenApiDocument)
 421442            .Where(docDescriptor => docDescriptor != null))
 1443        {
 211444            docDescriptor.ApplySecurityScheme(scheme, opts);
 1445        }
 211446    }
 1447
 1448    #endregion
 1449
 1450    /// <summary>
 1451    /// Adds authentication and authorization middleware to the Kestrun host.
 1452    /// </summary>
 1453    /// <param name="host">The Kestrun host instance.</param>
 1454    /// <param name="buildSchemes">A delegate to configure authentication schemes.</param>
 1455    /// <param name="defaultScheme">The default authentication scheme.</param>
 1456    /// <param name="configureAuthz">Optional authorization policy configuration.</param>
 1457    /// <param name="defaultChallengeScheme">The default challenge scheme .</param>
 1458    /// <returns>The configured KestrunHost instance.</returns>
 1459    internal static KestrunHost AddAuthentication(
 1460    this KestrunHost host,
 1461    string defaultScheme,
 1462    Action<AuthenticationBuilder>? buildSchemes = null,    // e.g., ab => ab.AddCookie().AddOpenIdConnect("oidc", ...)
 1463    Action<AuthorizationOptions>? configureAuthz = null,
 1464    string? defaultChallengeScheme = null)
 1465    {
 211466        ArgumentNullException.ThrowIfNull(buildSchemes);
 211467        if (string.IsNullOrWhiteSpace(defaultScheme))
 1468        {
 01469            throw new ArgumentException("Default scheme is required.", nameof(defaultScheme));
 1470        }
 1471
 211472        _ = host.AddService(services =>
 211473        {
 211474            // CRITICAL: Check if authentication services are already registered
 211475            // If they are, we only need to add new schemes, not reconfigure defaults
 24181476            var authDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IAuthenticationService));
 211477
 211478            AuthenticationBuilder authBuilder;
 211479            if (authDescriptor != null)
 211480            {
 211481                // Authentication already registered - only add new schemes without changing defaults
 01482                host.Logger.Debug("Authentication services already registered - adding schemes only (default={DefaultSch
 01483                authBuilder = new AuthenticationBuilder(services);
 211484            }
 211485            else
 211486            {
 211487                // First time registration - configure defaults
 211488                host.Logger.Debug(
 211489                    "Registering authentication services with defaults (default={DefaultScheme}, challenge={ChallengeSch
 211490                    defaultScheme,
 211491                    defaultChallengeScheme ?? defaultScheme);
 211492                authBuilder = services.AddAuthentication(options =>
 211493                {
 141494                    options.DefaultScheme = defaultScheme;
 141495                    options.DefaultChallengeScheme = defaultChallengeScheme ?? defaultScheme;
 351496                });
 211497            }
 211498
 211499            // Let caller add handlers/schemes
 211500            buildSchemes?.Invoke(authBuilder);
 211501
 211502            // Ensure Authorization is available (with optional customization)
 211503            // AddAuthorization is idempotent - safe to call multiple times
 211504            _ = configureAuthz is not null ?
 211505                services.AddAuthorization(configureAuthz) :
 211506                services.AddAuthorization();
 241507        });
 1508
 1509        // Add middleware once
 211510        return host.Use(app =>
 211511        {
 211512            const string Key = "__kr.authmw";
 211513            if (!app.Properties.ContainsKey(Key))
 211514            {
 211515                _ = app.UseAuthentication();
 211516                _ = app.UseAuthorization();
 211517                app.Properties[Key] = true;
 211518                host.Logger.Information("Kestrun: Authentication & Authorization middleware added.");
 211519            }
 421520        });
 1521    }
 1522
 1523    /// <summary>
 1524    /// Checks if the specified authentication scheme is registered in the Kestrun host.
 1525    /// </summary>
 1526    /// <param name="host">The Kestrun host instance.</param>
 1527    /// <param name="schemeName">The name of the authentication scheme to check.</param>
 1528    /// <returns>True if the scheme is registered; otherwise, false.</returns>
 1529    public static bool HasAuthScheme(this KestrunHost host, string schemeName)
 1530    {
 141531        var schemeProvider = host.App.Services.GetRequiredService<IAuthenticationSchemeProvider>();
 141532        var scheme = schemeProvider.GetSchemeAsync(schemeName).GetAwaiter().GetResult();
 141533        return scheme != null;
 1534    }
 1535
 1536    /// <summary>
 1537    /// Adds authorization services to the Kestrun host.
 1538    /// </summary>
 1539    /// <param name="host">The Kestrun host instance.</param>
 1540    /// <param name="cfg">Optional configuration for authorization options.</param>
 1541    /// <returns>The configured KestrunHost instance.</returns>
 1542    public static KestrunHost AddAuthorization(this KestrunHost host, Action<AuthorizationOptions>? cfg = null)
 1543    {
 11544        return host.AddService(services =>
 11545        {
 11546            _ = cfg == null ? services.AddAuthorization() : services.AddAuthorization(cfg);
 21547        });
 1548    }
 1549
 1550    /// <summary>
 1551    /// Checks if the specified authorization policy is registered in the Kestrun host.
 1552    /// </summary>
 1553    /// <param name="host">The Kestrun host instance.</param>
 1554    /// <param name="policyName">The name of the authorization policy to check.</param>
 1555    /// <returns>True if the policy is registered; otherwise, false.</returns>
 1556    public static bool HasAuthPolicy(this KestrunHost host, string policyName)
 1557    {
 131558        var policyProvider = host.App.Services.GetRequiredService<IAuthorizationPolicyProvider>();
 131559        var policy = policyProvider.GetPolicyAsync(policyName).GetAwaiter().GetResult();
 131560        return policy != null;
 1561    }
 1562
 1563    /// <summary>
 1564    /// HTTP message handler that logs all HTTP requests and responses for debugging.
 1565    /// </summary>
 01566    internal class LoggingHttpMessageHandler(HttpMessageHandler innerHandler, Serilog.ILogger logger) : DelegatingHandle
 1567    {
 01568        private readonly Serilog.ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 1569
 1570        // CRITICAL: Static field to store the last token response body so we can manually parse it
 1571        // The framework's OpenIdConnectMessage parser fails to populate AccessToken correctly
 01572        internal static string? LastTokenResponseBody { get; private set; }
 1573
 1574        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cance
 1575        {
 1576            // Log request
 01577            _logger.Warning($"HTTP {request.Method} {request.RequestUri}");
 1578
 1579            // Check if this is a token endpoint request
 01580            var isTokenEndpoint = request.RequestUri?.PathAndQuery?.Contains("/connect/token") == true ||
 01581                                 request.RequestUri?.PathAndQuery?.Contains("/token") == true;
 1582
 01583            if (request.Content != null && !isTokenEndpoint)
 1584            {
 1585                // Read request body without consuming it (only for non-token requests)
 01586                var requestBytes = await request.Content.ReadAsByteArrayAsync(cancellationToken);
 01587                var requestBody = System.Text.Encoding.UTF8.GetString(requestBytes);
 01588                _logger.Warning($"Request Body: {requestBody}");
 1589
 1590                // Recreate the content so it can be read again
 01591                request.Content = new ByteArrayContent(requestBytes);
 01592                request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-ww
 1593            }
 01594            else if (request.Content != null && isTokenEndpoint)
 1595            {
 01596                _logger.Warning("Token endpoint request - skipping body logging to preserve stream");
 1597            }
 1598
 1599            // Send request
 01600            var response = await base.SendAsync(request, cancellationToken);
 1601
 1602            // Log response
 01603            _logger.Warning($"HTTP Response: {(int)response.StatusCode} {response.StatusCode}");
 1604
 1605            // CRITICAL: For token endpoint responses, capture the body for manual parsing
 1606            // but then recreate the stream so the framework can also read it
 01607            if (response.Content != null && isTokenEndpoint)
 1608            {
 1609                // Read the response body
 01610                var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
 01611                var responseBody = System.Text.Encoding.UTF8.GetString(responseBytes);
 1612
 1613                // Store it in static field for later manual parsing
 01614                LastTokenResponseBody = responseBody;
 01615                _logger.Warning($"Captured token response body ({responseBytes.Length} bytes) for manual parsing");
 1616
 1617                // Recreate the content stream with ALL original headers preserved
 01618                var originalHeaders = response.Content.Headers.ToList();
 01619                var newContent = new ByteArrayContent(responseBytes);
 1620
 01621                foreach (var header in originalHeaders)
 1622                {
 01623                    _ = newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
 1624                }
 1625
 01626                response.Content = newContent;
 01627                _logger.Warning("Recreated token response stream for framework parsing");
 1628            }
 01629            else if (response.Content != null && !isTokenEndpoint)
 1630            {
 1631                // Save original headers
 01632                var originalHeaders = response.Content.Headers;
 1633
 1634                // Read response body and preserve it for the handler
 01635                var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
 01636                var responseBody = System.Text.Encoding.UTF8.GetString(responseBytes);
 01637                _logger.Warning($"Response Body: {responseBody}");
 1638
 1639                // Recreate the content so it can be read again by the OIDC handler
 01640                var newContent = new ByteArrayContent(responseBytes);
 1641
 1642                // Copy all original headers to the new content
 01643                foreach (var header in originalHeaders)
 1644                {
 01645                    _ = newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
 1646                }
 1647
 01648                response.Content = newContent;
 01649            }
 01650            else if (response.Content != null && isTokenEndpoint)
 1651            {
 01652                _logger.Warning("Token endpoint response - skipping body logging to let framework parse it");
 1653            }
 1654
 01655            return response;
 01656        }
 1657    }
 1658}

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.Boolean,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)
AddClientCertificateAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,System.Action`1<Kestrun.Authentication.ClientCertificateAuthenticationOptions>)
AddClientCertificateAuthentication(Kestrun.Hosting.KestrunHost,System.String,System.String,Kestrun.Authentication.ClientCertificateAuthenticationOptions)
AddClientCertificateAuthentication(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()