< 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@eeafbe813231ed23417e7b339e170e307b2c86f9
Line coverage
51%
Covered lines: 360
Uncovered lines: 334
Coverable lines: 694
Total lines: 1656
Line coverage: 51.8%
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@3f6f61710c7ef7d5953cab578fe699c1e5e01a3603/03/2026 - 20:14:57 Line coverage: 51.8% (360/694) Branch coverage: 45.3% (138/304) Total lines: 1656 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba 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@3f6f61710c7ef7d5953cab578fe699c1e5e01a3603/03/2026 - 20:14:57 Line coverage: 51.8% (360/694) Branch coverage: 45.3% (138/304) Total lines: 1656 Tag: Kestrun/Kestrun@d169bd1d1e32ec576bfd2135357b6d01a2ad23ba

Coverage delta

Coverage delta 29 -29

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.Authorization;
 3using Microsoft.Extensions.Options;
 4using Kestrun.Authentication;
 5using Serilog.Events;
 6using Kestrun.Scripting;
 7using Microsoft.AspNetCore.Authentication.Negotiate;
 8using Kestrun.Claims;
 9using Microsoft.AspNetCore.Authentication.OAuth;
 10using System.Text.Json;
 11using System.Security.Claims;
 12using Microsoft.Extensions.DependencyInjection.Extensions;
 13using Microsoft.AspNetCore.Authentication.OpenIdConnect;
 14using Microsoft.IdentityModel.Protocols;
 15using Microsoft.IdentityModel.Protocols.OpenIdConnect;
 16using Kestrun.OpenApi;
 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="deprecated">If true, marks the authentication scheme as deprecated in OpenAPI documentation.</param
 226    /// <param name="clientId">GitHub OAuth App Client ID.</param>
 227    /// <param name="clientSecret">GitHub OAuth App Client Secret.</param>
 228    /// <param name="callbackPath">The callback path for OAuth redirection (e.g. "/signin-github").</param>
 229    /// <returns>The configured KestrunHost.</returns>
 230    public static KestrunHost AddGitHubOAuthAuthentication(
 231        this KestrunHost host,
 232        string scheme,
 233        string? displayName,
 234        string[]? documentationId,
 235        string? description,
 236        bool deprecated,
 237        string clientId,
 238        string clientSecret,
 239        string callbackPath)
 240    {
 0241        var opts = ConfigureGitHubOAuth2Options(host, clientId, clientSecret, callbackPath);
 0242        ConfigureGitHubClaimMappings(opts);
 0243        opts.DocumentationId = documentationId ?? [];
 0244        if (!string.IsNullOrWhiteSpace(description))
 245        {
 0246            opts.Description = description;
 247        }
 0248        opts.Deprecated = deprecated;
 0249        opts.Events = new OAuthEvents
 0250        {
 0251            OnCreatingTicket = async context =>
 0252            {
 0253                await FetchGitHubUserInfoAsync(context);
 0254                await EnrichGitHubEmailClaimAsync(context, host);
 0255            }
 0256        };
 0257        return host.AddOAuth2Authentication(scheme, displayName, opts);
 258    }
 259
 260    /// <summary>
 261    /// Configures OAuth2Options for GitHub authentication.
 262    /// </summary>
 263    /// <param name="host">The Kestrun host instance.</param>
 264    /// <param name="clientId">GitHub OAuth App Client ID.</param>
 265    /// <param name="clientSecret">GitHub OAuth App Client Secret.</param>
 266    /// <param name="callbackPath">The callback path for OAuth redirection (e.g. "/signin-github").</param>
 267    /// <returns>The configured OAuth2Options.</returns>
 268    private static OAuth2Options ConfigureGitHubOAuth2Options(KestrunHost host, string clientId, string clientSecret, st
 269    {
 0270        return new OAuth2Options()
 0271        {
 0272            Host = host,
 0273            ClientId = clientId,
 0274            ClientSecret = clientSecret,
 0275            CallbackPath = callbackPath,
 0276            AuthorizationEndpoint = "https://github.com/login/oauth/authorize",
 0277            TokenEndpoint = "https://github.com/login/oauth/access_token",
 0278            UserInformationEndpoint = "https://api.github.com/user",
 0279            SaveTokens = true,
 0280            Scope = { "read:user", "user:email" }
 0281        };
 282    }
 283
 284    /// <summary>
 285    /// Configures claim mappings for GitHub OAuth2Options.
 286    /// </summary>
 287    /// <param name="opts">The OAuth2Options to configure.</param>
 288    private static void ConfigureGitHubClaimMappings(OAuth2Options opts)
 289    {
 0290        opts.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
 0291        opts.ClaimActions.MapJsonKey(ClaimTypes.Name, "login");
 0292        opts.ClaimActions.MapJsonKey(ClaimTypes.Email, "email");
 0293        opts.ClaimActions.MapJsonKey("name", "name");
 0294        opts.ClaimActions.MapJsonKey("urn:github:login", "login");
 0295        opts.ClaimActions.MapJsonKey("urn:github:avatar_url", "avatar_url");
 0296        opts.ClaimActions.MapJsonKey("urn:github:html_url", "html_url");
 0297    }
 298
 299    /// <summary>
 300    /// Fetches GitHub user information and adds claims to the identity.
 301    /// </summary>
 302    /// <param name="context">The OAuthCreatingTicketContext.</param>
 303    /// <returns>A task representing the asynchronous operation.</returns>
 304    private static async Task FetchGitHubUserInfoAsync(OAuthCreatingTicketContext context)
 305    {
 0306        using var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
 0307        request.Headers.Accept.Add(new("application/json"));
 0308        request.Headers.Add("User-Agent", "KestrunOAuth/1.0");
 0309        request.Headers.Authorization = new("Bearer", context.AccessToken);
 310
 0311        using var response = await context.Backchannel.SendAsync(request,
 0312            HttpCompletionOption.ResponseHeadersRead,
 0313            context.HttpContext.RequestAborted);
 314
 0315        _ = response.EnsureSuccessStatusCode();
 316
 0317        using var user = JsonDocument.Parse(await response.Content.ReadAsStringAsync(context.HttpContext.RequestAborted)
 0318        context.RunClaimActions(user.RootElement);
 0319    }
 320
 321    /// <summary>
 322    /// Fetches GitHub user emails and enriches the identity with the primary verified email claim.
 323    /// </summary>
 324    /// <param name="context">The OAuthCreatingTicketContext.</param>
 325    /// <param name="host">The KestrunHost instance for logging.</param>
 326    /// <returns>A task representing the asynchronous operation.</returns>
 327    private static async Task EnrichGitHubEmailClaimAsync(OAuthCreatingTicketContext context, KestrunHost host)
 328    {
 0329        if (context.Identity is null || context.Identity.HasClaim(c => c.Type == ClaimTypes.Email))
 330        {
 0331            return;
 332        }
 333
 334        try
 335        {
 0336            using var emailRequest = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails");
 0337            emailRequest.Headers.Accept.Add(new("application/json"));
 0338            emailRequest.Headers.Add("User-Agent", "KestrunOAuth/1.0");
 0339            emailRequest.Headers.Authorization = new("Bearer", context.AccessToken);
 340
 0341            using var emailResponse = await context.Backchannel.SendAsync(emailRequest,
 0342                HttpCompletionOption.ResponseHeadersRead,
 0343                context.HttpContext.RequestAborted);
 344
 0345            if (!emailResponse.IsSuccessStatusCode)
 346            {
 0347                return;
 348            }
 349
 0350            using var emails = JsonDocument.Parse(await emailResponse.Content.ReadAsStringAsync(context.HttpContext.Requ
 0351            var primaryEmail = FindPrimaryVerifiedEmail(emails) ?? FindFirstVerifiedEmail(emails);
 352
 0353            if (!string.IsNullOrWhiteSpace(primaryEmail))
 354            {
 0355                context.Identity.AddClaim(new Claim(
 0356                    ClaimTypes.Email,
 0357                    primaryEmail,
 0358                    ClaimValueTypes.String,
 0359                    context.Options.ClaimsIssuer));
 360            }
 0361        }
 0362        catch (Exception ex)
 363        {
 0364            host.Logger.Verbose(exception: ex, messageTemplate: "Failed to enrich GitHub email claim.");
 0365        }
 0366    }
 367
 368    /// <summary>
 369    /// Finds the primary verified email from the GitHub emails JSON document.
 370    /// </summary>
 371    /// <param name="emails">The JSON document containing GitHub emails.</param>
 372    /// <returns>The primary verified email if found; otherwise, null.</returns>
 373    private static string? FindPrimaryVerifiedEmail(JsonDocument emails)
 374    {
 0375        foreach (var emailObj in emails.RootElement.EnumerateArray())
 376        {
 0377            var isPrimary = emailObj.TryGetProperty("primary", out var primaryProp) && primaryProp.GetBoolean();
 0378            var isVerified = emailObj.TryGetProperty("verified", out var verifiedProp) && verifiedProp.GetBoolean();
 379
 0380            if (isPrimary && isVerified && emailObj.TryGetProperty("email", out var emailProp))
 381            {
 0382                return emailProp.GetString();
 383            }
 384        }
 0385        return null;
 0386    }
 387
 388    /// <summary>
 389    /// Finds the primary verified email from the GitHub emails JSON document.
 390    /// </summary>
 391    /// <param name="emails">The JSON document containing GitHub emails.</param>
 392    /// <returns>The primary verified email if found; otherwise, null.</returns>
 393    private static string? FindFirstVerifiedEmail(JsonDocument emails)
 394    {
 0395        foreach (var emailObj in emails.RootElement.EnumerateArray())
 396        {
 0397            var isVerified = emailObj.TryGetProperty("verified", out var verifiedProp) && verifiedProp.GetBoolean();
 0398            if (isVerified && emailObj.TryGetProperty("email", out var emailProp))
 399            {
 0400                return emailProp.GetString();
 401            }
 402        }
 0403        return null;
 0404    }
 405
 406    #endregion
 407    #region JWT Bearer Authentication
 408    /// <summary>
 409    /// Adds JWT Bearer authentication to the Kestrun host.
 410    /// <para>Use this for APIs that require token-based authentication.</para>
 411    /// </summary>
 412    /// <param name="host">The Kestrun host instance.</param>
 413    /// <param name="authenticationScheme">The authentication scheme name (e.g. "Bearer").</param>
 414    /// <param name="displayName">The display name for the authentication scheme.</param>
 415    /// <param name="configureOptions">Optional configuration for JwtAuthOptions.</param>
 416    /// <example>
 417    /// HS512 (HMAC-SHA-512, symmetric)
 418    /// </example>
 419    /// <code>
 420    ///     var hmacKey = new SymmetricSecurityKey(
 421    ///         Encoding.UTF8.GetBytes("32-bytes-or-more-secret……"));
 422    ///     host.AddJwtBearerAuthentication(
 423    ///         scheme:          "Bearer",
 424    ///         issuer:          "KestrunApi",
 425    ///         audience:        "KestrunClients",
 426    ///         validationKey:   hmacKey,
 427    ///         validAlgorithms: new[] { SecurityAlgorithms.HmacSha512 });
 428    /// </code>
 429    /// <example>
 430    /// RS256 (RSA-SHA-256, asymmetric)
 431    /// <para>Requires a PEM-encoded private key file.</para>
 432    /// <code>
 433    ///    using var rsa = RSA.Create();
 434    ///     rsa.ImportFromPem(File.ReadAllText("private-key.pem"));
 435    ///     var rsaKey = new RsaSecurityKey(rsa);
 436    ///
 437    ///     host.AddJwtBearerAuthentication(
 438    ///         scheme:          "Rs256",
 439    ///         issuer:          "KestrunApi",
 440    ///         audience:        "KestrunClients",
 441    ///         validationKey:   rsaKey,
 442    ///         validAlgorithms: new[] { SecurityAlgorithms.RsaSha256 });
 443    /// </code>
 444    /// </example>
 445    /// <example>
 446    /// ES256 (ECDSA-SHA-256, asymmetric)
 447    /// <para>Requires a PEM-encoded private key file.</para>
 448    /// <code>
 449    ///     using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
 450    ///     var esKey = new ECDsaSecurityKey(ecdsa);
 451    ///     host.AddJwtBearerAuthentication(
 452    ///         "Es256", "KestrunApi", "KestrunClients",
 453    ///         esKey, new[] { SecurityAlgorithms.EcdsaSha256 });
 454    /// </code>
 455    /// </example>
 456    /// <returns></returns>
 457    public static KestrunHost AddJwtBearerAuthentication(
 458      this KestrunHost host,
 459      string authenticationScheme = AuthenticationDefaults.JwtBearerSchemeName,
 460      string? displayName = AuthenticationDefaults.JwtBearerDisplayName,
 461      Action<JwtAuthOptions>? configureOptions = null)
 462    {
 3463        ArgumentNullException.ThrowIfNull(configureOptions);
 464        // Build a prototype options instance (single source of truth)
 3465        var prototype = new JwtAuthOptions { Host = host };
 3466        configureOptions?.Invoke(prototype);
 3467        ConfigureOpenApi(host, authenticationScheme, prototype);
 468
 469        // register in host for introspection
 3470        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Bearer, prototype);
 471
 3472        return host.AddAuthentication(
 3473            defaultScheme: authenticationScheme,
 3474            buildSchemes: ab =>
 3475            {
 3476                _ = ab.AddJwtBearer(
 3477                    authenticationScheme: authenticationScheme,
 3478                    displayName: displayName,
 3479                    configureOptions: opts =>
 3480                {
 0481                    prototype.ApplyTo(opts);
 3482                });
 3483            },
 3484            configureAuthz: prototype.ClaimPolicy?.ToAuthzDelegate()
 3485            );
 486    }
 487
 488    /// <summary>
 489    /// Adds JWT Bearer authentication to the Kestrun host using the provided options object.
 490    /// </summary>
 491    /// <param name="host">The Kestrun host instance.</param>
 492    /// <param name="authenticationScheme">The authentication scheme name.</param>
 493    /// <param name="displayName">The display name for the authentication scheme.</param>
 494    /// <param name="configureOptions">Optional configuration for JwtAuthOptions.</param>
 495    /// <returns>The configured KestrunHost instance.</returns>
 496    public static KestrunHost AddJwtBearerAuthentication(
 497        this KestrunHost host,
 498        string authenticationScheme = AuthenticationDefaults.JwtBearerSchemeName,
 499        string? displayName = AuthenticationDefaults.JwtBearerDisplayName,
 500        JwtAuthOptions? configureOptions = null)
 501    {
 3502        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 503        {
 3504            host.Logger.Debug("Adding Jwt Bearer Authentication with scheme: {Scheme}", authenticationScheme);
 505        }
 506        // Ensure the scheme is not null
 3507        ArgumentNullException.ThrowIfNull(host);
 3508        ArgumentNullException.ThrowIfNull(authenticationScheme);
 3509        ArgumentNullException.ThrowIfNull(configureOptions);
 510
 511        // Ensure host is set
 3512        if (configureOptions.Host != host)
 513        {
 0514            configureOptions.Host = host;
 515        }
 516
 3517        return host.AddJwtBearerAuthentication(
 3518            authenticationScheme: authenticationScheme,
 3519              displayName: displayName,
 3520              configureOptions: opts =>
 3521              {
 3522                  // Copy relevant properties from provided options instance to the framework-created one
 3523                  configureOptions.ApplyTo(opts);
 3524                  host.Logger.Debug(
 3525                           "Configured JWT Authentication using scheme {Scheme}.",
 3526                           authenticationScheme);
 3527              }
 3528            );
 529    }
 530    #endregion
 531    #region Cookie Authentication
 532    /// <summary>
 533    /// Adds Cookie Authentication to the Kestrun host.
 534    /// <para>Use this for browser-based authentication using cookies.</para>
 535    /// </summary>
 536    /// <param name="host">The Kestrun host instance.</param>
 537    /// <param name="authenticationScheme">The authentication scheme name (default is CookieAuthenticationDefaults.Authe
 538    /// <param name="displayName">The display name for the authentication scheme.</param>
 539    /// <param name="configureOptions">Optional configuration for CookieAuthenticationOptions.</param>
 540    /// <param name="claimPolicy">Optional authorization policy configuration.</param>
 541    /// <returns>The configured KestrunHost instance.</returns>
 542    public static KestrunHost AddCookieAuthentication(
 543        this KestrunHost host,
 544        string authenticationScheme = AuthenticationDefaults.CookiesSchemeName,
 545        string? displayName = AuthenticationDefaults.CookiesDisplayName,
 546        Action<CookieAuthOptions>? configureOptions = null,
 547     ClaimPolicyConfig? claimPolicy = null)
 548    {
 549        // Build a prototype options instance (single source of truth)
 2550        var prototype = new CookieAuthOptions { Host = host };
 2551        configureOptions?.Invoke(prototype);
 2552        ConfigureOpenApi(host, authenticationScheme, prototype);
 553
 554        // register in host for introspection
 2555        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Cookie, prototype);
 556
 557        // Add authentication
 2558        return host.AddAuthentication(
 2559            defaultScheme: authenticationScheme,
 2560            buildSchemes: ab =>
 2561            {
 2562                _ = ab.AddCookie(
 2563                    authenticationScheme: authenticationScheme,
 2564                    displayName: displayName,
 2565                    configureOptions: opts =>
 2566                    {
 2567                        // Copy everything from the prototype into the real options instance
 0568                        prototype.ApplyTo(opts);
 2569                        // let caller mutate everything first
 2570                        //configure?.Invoke(opts);
 2571                    });
 2572            },
 2573            configureAuthz: claimPolicy?.ToAuthzDelegate()
 2574        );
 575    }
 576
 577    /// <summary>
 578    /// Adds Cookie Authentication to the Kestrun host using the provided options object.
 579    /// </summary>
 580    /// <param name="host">The Kestrun host instance.</param>
 581    /// <param name="authenticationScheme">The authentication scheme name (default is CookieAuthenticationDefaults.Authe
 582    /// <param name="displayName">The display name for the authentication scheme.</param>
 583    /// <param name="configureOptions">The CookieAuthenticationOptions object to configure the authentication.</param>
 584    /// <param name="claimPolicy">Optional authorization policy configuration.</param>
 585    /// <returns>The configured KestrunHost instance.</returns>
 586    public static KestrunHost AddCookieAuthentication(
 587          this KestrunHost host,
 588          string authenticationScheme = AuthenticationDefaults.CookiesSchemeName,
 589          string? displayName = AuthenticationDefaults.CookiesDisplayName,
 590          CookieAuthOptions? configureOptions = null,
 591       ClaimPolicyConfig? claimPolicy = null)
 592    {
 0593        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 594        {
 0595            host.Logger.Debug("Adding Cookie Authentication with scheme: {Scheme}", authenticationScheme);
 596        }
 597        // Ensure the scheme is not null
 0598        ArgumentNullException.ThrowIfNull(host);
 0599        ArgumentNullException.ThrowIfNull(authenticationScheme);
 0600        ArgumentNullException.ThrowIfNull(configureOptions);
 601        // Ensure host is set
 0602        if (configureOptions.Host != host)
 603        {
 0604            configureOptions.Host = host;
 605        }
 606        // Copy relevant properties from provided options instance to the framework-created one
 0607        return host.AddCookieAuthentication(
 0608            authenticationScheme: authenticationScheme,
 0609            displayName: displayName,
 0610            configureOptions: configureOptions.ApplyTo,
 0611            claimPolicy: claimPolicy
 0612        );
 613    }
 614    #endregion
 615
 616    /*
 617        public static KestrunHost AddClientCertificateAuthentication(
 618            this KestrunHost host,
 619            string scheme = CertificateAuthenticationDefaults.AuthenticationScheme,
 620            Action<CertificateAuthenticationOptions>? configure = null,
 621            Action<AuthorizationOptions>? configureAuthz = null)
 622        {
 623            return host.AddAuthentication(
 624                defaultScheme: scheme,
 625                buildSchemes: ab =>
 626                {
 627                    ab.AddCertificate(
 628                        authenticationScheme: scheme,
 629                        configureOptions: configure ?? (opts => { }));
 630                },
 631                configureAuthz: configureAuthz
 632            );
 633        }
 634    */
 635
 636    #region Windows Authentication
 637
 638    /// <summary>
 639    /// Adds Windows Authentication to the Kestrun host.
 640    /// <para>The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 641    /// This enables Kerberos and NTLM authentication.</para>
 642    /// </summary>
 643    /// <param name="host">The Kestrun host instance.</param>
 644    /// <param name="authenticationScheme">The authentication scheme name (default is NegotiateDefaults.AuthenticationSc
 645    /// <param name="displayName">The display name for the authentication scheme.</param>
 646    /// <param name="configureOptions">The WindowsAuthOptions object to configure the authentication.</param>
 647    /// <returns>The configured KestrunHost instance.</returns>
 648    public static KestrunHost AddWindowsAuthentication(
 649       this KestrunHost host,
 650       string authenticationScheme = AuthenticationDefaults.WindowsSchemeName,
 651       string? displayName = AuthenticationDefaults.WindowsDisplayName,
 652       Action<WindowsAuthOptions>? configureOptions = null)
 653    {
 654        // Build a prototype options instance (single source of truth)
 1655        var prototype = new WindowsAuthOptions { Host = host };
 1656        configureOptions?.Invoke(prototype);
 1657        ConfigureOpenApi(host, authenticationScheme, prototype);
 658
 659        // register in host for introspection
 1660        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Cookie, prototype);
 661
 662        // Add authentication
 1663        return host.AddAuthentication(
 1664            defaultScheme: authenticationScheme,
 1665            buildSchemes: ab =>
 1666            {
 1667                _ = ab.AddNegotiate(
 1668                    authenticationScheme: authenticationScheme,
 1669                    displayName: displayName,
 1670                    configureOptions: opts =>
 1671                    {
 1672                        // Copy everything from the prototype into the real options instance
 0673                        prototype.ApplyTo(opts);
 1674
 0675                        host.Logger.Debug("Configured Windows Authentication using scheme {Scheme}", authenticationSchem
 0676                    }
 1677                );
 1678            }
 1679        );
 680    }
 681    /// <summary>
 682    /// Adds Windows Authentication to the Kestrun host.
 683    /// <para>
 684    /// The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 685    /// This enables Kerberos and NTLM authentication.
 686    /// </para>
 687    /// </summary>
 688    /// <param name="host">The Kestrun host instance.</param>
 689    /// <param name="authenticationScheme">The authentication scheme name (default is NegotiateDefaults.AuthenticationSc
 690    /// <param name="displayName">The display name for the authentication scheme.</param>
 691    /// <param name="configureOptions">The WindowsAuthOptions object to configure the authentication.</param>
 692    /// <returns>The configured KestrunHost instance.</returns>
 693    public static KestrunHost AddWindowsAuthentication(
 694        this KestrunHost host,
 695        string authenticationScheme = AuthenticationDefaults.WindowsSchemeName,
 696        string? displayName = AuthenticationDefaults.WindowsDisplayName,
 697        WindowsAuthOptions? configureOptions = null)
 698    {
 0699        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 700        {
 0701            host.Logger.Debug("Adding Windows Authentication with scheme: {Scheme}", authenticationScheme);
 702        }
 703        // Ensure the scheme is not null
 0704        ArgumentNullException.ThrowIfNull(host);
 0705        ArgumentNullException.ThrowIfNull(configureOptions);
 706        // Ensure host is set
 0707        if (configureOptions.Host != host)
 708        {
 0709            configureOptions.Host = host;
 710        }
 711        // Copy relevant properties from provided options instance to the framework-created one
 712        // Add authentication
 0713        return host.AddWindowsAuthentication(
 0714           authenticationScheme: authenticationScheme,
 0715           displayName: displayName,
 0716           configureOptions: configureOptions.ApplyTo
 0717       );
 718    }
 719
 720    /// <summary>
 721    /// Adds Windows Authentication to the Kestrun host.
 722    /// <para>The authentication scheme name is <see cref="NegotiateDefaults.AuthenticationScheme"/>.
 723    /// This enables Kerberos and NTLM authentication.</para>
 724    /// </summary>
 725    /// <param name="host"> The Kestrun host instance.</param>
 726    /// <returns> The configured KestrunHost instance.</returns>
 727    public static KestrunHost AddWindowsAuthentication(this KestrunHost host) =>
 1728        host.AddWindowsAuthentication(
 1729            AuthenticationDefaults.WindowsSchemeName,
 1730            AuthenticationDefaults.WindowsDisplayName,
 1731            (Action<WindowsAuthOptions>?)null);
 732
 733    #endregion
 734
 735    #region Client Certificate Authentication
 736
 737    /// <summary>
 738    /// Adds Client Certificate Authentication to the Kestrun host.
 739    /// <para>Use this for authenticating clients using X.509 certificates.</para>
 740    /// </summary>
 741    /// <param name="host">The Kestrun host instance.</param>
 742    /// <param name="scheme">The authentication scheme name (default is "Certificate").</param>
 743    /// <param name="displayName">The display name for the authentication scheme.</param>
 744    /// <param name="configure">Optional configuration for ClientCertificateAuthenticationOptions.</param>
 745    /// <returns>The configured KestrunHost instance.</returns>
 746    public static KestrunHost AddClientCertificateAuthentication(
 747        this KestrunHost host,
 748        string scheme = AuthenticationDefaults.CertificateSchemeName,
 749        string? displayName = AuthenticationDefaults.CertificateDisplayName,
 750        Action<ClientCertificateAuthenticationOptions>? configure = null)
 751    {
 752        // Build a prototype options instance (single source of truth)
 1753        var prototype = new ClientCertificateAuthenticationOptions { Host = host };
 754
 755        // Let the caller mutate the prototype
 1756        configure?.Invoke(prototype);
 757
 1758        ConfigureOpenApi(host, scheme, prototype);
 759
 760        // Register in host for introspection
 1761        _ = host.RegisteredAuthentications.Register(scheme, AuthenticationType.Certificate, prototype);
 762
 1763        return host.AddAuthentication(
 1764            defaultScheme: scheme,
 1765            buildSchemes: ab =>
 1766            {
 1767                _ = ab.AddScheme<ClientCertificateAuthenticationOptions, ClientCertificateAuthHandler>(
 1768                    authenticationScheme: scheme,
 1769                    displayName: displayName,
 1770                    configureOptions: opts =>
 1771                    {
 1772                        // Copy from the prototype into the runtime instance
 0773                        prototype.ApplyTo(opts);
 1774
 0775                        host.Logger.Debug("Configured Client Certificate Authentication using scheme {Scheme}", scheme);
 1776                    });
 1777            }
 1778        );
 779    }
 780
 781    /// <summary>
 782    /// Adds Client Certificate Authentication to the Kestrun host using the provided options object.
 783    /// </summary>
 784    /// <param name="host">The Kestrun host instance.</param>
 785    /// <param name="scheme">The authentication scheme name (default is "Certificate").</param>
 786    /// <param name="displayName">The display name for the authentication scheme.</param>
 787    /// <param name="configure">The ClientCertificateAuthenticationOptions object to configure the authentication.</para
 788    /// <returns>The configured KestrunHost instance.</returns>
 789    public static KestrunHost AddClientCertificateAuthentication(
 790        this KestrunHost host,
 791        string scheme,
 792        string? displayName,
 793        ClientCertificateAuthenticationOptions configure)
 794    {
 0795        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 796        {
 0797            host.Logger.Debug("Adding Client Certificate Authentication with scheme: {Scheme}", scheme);
 798        }
 799
 800        // Ensure the scheme is not null
 0801        ArgumentNullException.ThrowIfNull(host);
 0802        ArgumentNullException.ThrowIfNull(scheme);
 0803        ArgumentNullException.ThrowIfNull(configure);
 804
 805        // Ensure host is set
 0806        if (configure.Host != host)
 807        {
 0808            configure.Host = host;
 809        }
 810
 0811        return host.AddClientCertificateAuthentication(
 0812            scheme: scheme,
 0813            displayName: displayName,
 0814            configure: configure.ApplyTo
 0815        );
 816    }
 817
 818    /// <summary>
 819    /// Adds Client Certificate Authentication to the Kestrun host with default settings.
 820    /// </summary>
 821    /// <param name="host">The Kestrun host instance.</param>
 822    /// <returns>The configured KestrunHost instance.</returns>
 823    public static KestrunHost AddClientCertificateAuthentication(this KestrunHost host) =>
 0824        host.AddClientCertificateAuthentication(
 0825            AuthenticationDefaults.CertificateSchemeName,
 0826            AuthenticationDefaults.CertificateDisplayName,
 0827            (Action<ClientCertificateAuthenticationOptions>?)null);
 828
 829    #endregion
 830    #region API Key Authentication
 831    /// <summary>
 832    /// Adds API Key Authentication to the Kestrun host.
 833    /// <para>Use this for endpoints that require an API key for access.</para>
 834    /// </summary>
 835    /// <param name="host">The Kestrun host instance.</param>
 836    /// <param name="authenticationScheme">The authentication scheme name (default is "ApiKey").</param>
 837    /// <param name="displayName">The display name for the authentication scheme (default is "API Key").</param>
 838    /// <param name="configureOptions">Optional configuration for ApiKeyAuthenticationOptions.</param>
 839    /// <returns>The configured KestrunHost instance.</returns>
 840    public static KestrunHost AddApiKeyAuthentication(
 841    this KestrunHost host,
 842    string authenticationScheme = AuthenticationDefaults.ApiKeySchemeName,
 843    string? displayName = AuthenticationDefaults.ApiKeyDisplayName,
 844    Action<ApiKeyAuthenticationOptions>? configureOptions = null)
 845    {
 846        // Build a prototype options instance (single source of truth)
 6847        var prototype = new ApiKeyAuthenticationOptions { Host = host };
 848
 849        // Let the caller mutate the prototype
 6850        configureOptions?.Invoke(prototype);
 851
 852        // Configure validators / claims / OpenAPI on the prototype
 6853        ConfigureApiKeyValidators(host, prototype);
 6854        ConfigureApiKeyIssueClaims(host, prototype);
 6855        ConfigureOpenApi(host, authenticationScheme, prototype);
 856
 857        // register in host for introspection
 6858        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.ApiKey, prototype);
 859        // Add authentication
 6860        return host.AddAuthentication(
 6861             defaultScheme: authenticationScheme,
 6862             buildSchemes: ab =>
 6863             {
 6864                 // ← TOptions == ApiKeyAuthenticationOptions
 6865                 //    THandler == ApiKeyAuthHandler
 6866                 _ = ab.AddScheme<ApiKeyAuthenticationOptions, ApiKeyAuthHandler>(
 6867                     authenticationScheme: authenticationScheme,
 6868                     displayName: displayName,
 6869                     configureOptions: opts =>
 6870                     {
 6871                         // Copy from the prototype into the runtime instance
 6872                         prototype.ApplyTo(opts);
 6873
 6874                         host.Logger.Debug(
 6875                             "Configured API Key Authentication using scheme {Scheme} with header {Header} (In={In})",
 6876                             authenticationScheme, prototype.ApiKeyName, prototype.In);
 12877                     });
 6878             }
 6879         )
 6880        //  register the post-configurer **after** the scheme so it can
 6881        //    read BasicAuthenticationOptions for <scheme>
 6882        .AddService(services =>
 6883          {
 6884              _ = services.AddSingleton<IPostConfigureOptions<AuthorizationOptions>>(
 9885                  sp => new ClaimPolicyPostConfigurer(
 9886                            authenticationScheme,
 9887                            sp.GetRequiredService<
 9888                                IOptionsMonitor<ApiKeyAuthenticationOptions>>()));
 12889          });
 890    }
 891
 892    /// <summary>
 893    /// Adds API Key Authentication to the Kestrun host using the provided options object.
 894    /// </summary>
 895    /// <param name="host">The Kestrun host instance.</param>
 896    /// <param name="authenticationScheme">The authentication scheme name.</param>
 897    /// <param name="displayName">The display name for the authentication scheme.</param>
 898    /// <param name="configureOptions">The ApiKeyAuthenticationOptions object to configure the authentication.</param>
 899    /// <returns>The configured KestrunHost instance.</returns>
 900    public static KestrunHost AddApiKeyAuthentication(
 901    this KestrunHost host,
 902    string authenticationScheme = AuthenticationDefaults.ApiKeySchemeName,
 903    string? displayName = AuthenticationDefaults.ApiKeyDisplayName,
 904    ApiKeyAuthenticationOptions? configureOptions = null)
 905    {
 1906        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 907        {
 1908            host.Logger.Debug("Adding API Key Authentication with scheme: {Scheme}", authenticationScheme);
 909        }
 910        // Ensure the scheme is not null
 1911        ArgumentNullException.ThrowIfNull(host);
 1912        ArgumentNullException.ThrowIfNull(authenticationScheme);
 1913        ArgumentNullException.ThrowIfNull(configureOptions);
 914        // Ensure host is set
 1915        if (configureOptions.Host != host)
 916        {
 1917            configureOptions.Host = host;
 918        }
 919        // Copy properties from the provided configure object
 1920        return host.AddApiKeyAuthentication(
 1921            authenticationScheme: authenticationScheme,
 1922            displayName: displayName,
 1923            configureOptions: configureOptions.ApplyTo
 1924        );
 925    }
 926
 927    /// <summary>
 928    /// Configures the API Key validators.
 929    /// </summary>
 930    /// <param name="host">The Kestrun host instance.</param>
 931    /// <param name="opts">The options to configure.</param>
 932    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 933    private static void ConfigureApiKeyValidators(KestrunHost host, ApiKeyAuthenticationOptions opts)
 934    {
 6935        var settings = opts.ValidateCodeSettings;
 6936        if (string.IsNullOrWhiteSpace(settings.Code))
 937        {
 3938            return;
 939        }
 940
 3941        switch (settings.Language)
 942        {
 943            case ScriptLanguage.PowerShell:
 1944                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 945                {
 1946                    opts.Logger.Debug("Building PowerShell validator for API Key authentication");
 947                }
 948
 1949                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildPsValidator(host, settings);
 1950                break;
 951            case ScriptLanguage.CSharp:
 1952                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 953                {
 1954                    opts.Logger.Debug("Building C# validator for API Key authentication");
 955                }
 956
 1957                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildCsValidator(host, settings);
 1958                break;
 959            case ScriptLanguage.VBNet:
 1960                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 961                {
 1962                    opts.Logger.Debug("Building VB.NET validator for API Key authentication");
 963                }
 964
 1965                opts.ValidateKeyAsync = ApiKeyAuthHandler.BuildVBNetValidator(host, settings);
 1966                break;
 967            default:
 0968                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 969                {
 0970                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 971                }
 0972                throw new NotSupportedException("Unsupported language");
 973        }
 974    }
 975
 976    /// <summary>
 977    /// Configures the API Key issue claims.
 978    /// </summary>
 979    /// <param name="host">The Kestrun host instance.</param>
 980    /// <param name="opts">The options to configure.</param>
 981    /// <exception cref="NotSupportedException">Thrown when the language is not supported.</exception>
 982    private static void ConfigureApiKeyIssueClaims(KestrunHost host, ApiKeyAuthenticationOptions opts)
 983    {
 6984        var settings = opts.IssueClaimsCodeSettings;
 6985        if (string.IsNullOrWhiteSpace(settings.Code))
 986        {
 3987            return;
 988        }
 989
 3990        switch (settings.Language)
 991        {
 992            case ScriptLanguage.PowerShell:
 1993                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 994                {
 1995                    opts.Logger.Debug("Building PowerShell Issue Claims for API Key authentication");
 996                }
 997
 1998                opts.IssueClaims = IAuthHandler.BuildPsIssueClaims(host, settings);
 1999                break;
 1000            case ScriptLanguage.CSharp:
 11001                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 1002                {
 11003                    opts.Logger.Debug("Building C# Issue Claims for API Key authentication");
 1004                }
 1005
 11006                opts.IssueClaims = IAuthHandler.BuildCsIssueClaims(host, settings);
 11007                break;
 1008            case ScriptLanguage.VBNet:
 11009                if (opts.Logger.IsEnabled(LogEventLevel.Debug))
 1010                {
 11011                    opts.Logger.Debug("Building VB.NET Issue Claims for API Key authentication");
 1012                }
 1013
 11014                opts.IssueClaims = IAuthHandler.BuildVBNetIssueClaims(host, settings);
 11015                break;
 1016            default:
 01017                if (opts.Logger.IsEnabled(LogEventLevel.Warning))
 1018                {
 01019                    opts.Logger.Warning("{language} is not supported for API Basic authentication", settings.Language);
 1020                }
 01021                throw new NotSupportedException("Unsupported language");
 1022        }
 1023    }
 1024
 1025    #endregion
 1026
 1027    #region OAuth2 Authentication
 1028
 1029    /// <summary>
 1030    /// Adds OAuth2 authentication to the Kestrun host.
 1031    /// <para>Use this for applications that require OAuth2 authentication.</para>
 1032    /// </summary>
 1033    /// <param name="host">The Kestrun host instance.</param>
 1034    /// <param name="authenticationScheme">The authentication scheme name.</param>
 1035    /// <param name="displayName">The display name for the authentication scheme.</param>
 1036    /// <param name="configureOptions">The OAuth2Options to configure the authentication.</param>
 1037    /// <returns>The configured KestrunHost instance.</returns>
 1038    public static KestrunHost AddOAuth2Authentication(
 1039        this KestrunHost host,
 1040        string authenticationScheme = AuthenticationDefaults.OAuth2SchemeName,
 1041        string? displayName = AuthenticationDefaults.OAuth2DisplayName,
 1042        OAuth2Options? configureOptions = null)
 1043    {
 01044        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1045        {
 01046            host.Logger.Debug("Adding OAuth2 Authentication with scheme: {Scheme}", authenticationScheme);
 1047        }
 1048        // Ensure the scheme is not null
 01049        ArgumentNullException.ThrowIfNull(host);
 01050        ArgumentNullException.ThrowIfNull(authenticationScheme);
 01051        ArgumentNullException.ThrowIfNull(configureOptions);
 1052
 1053        // Required for OAuth2
 01054        if (string.IsNullOrWhiteSpace(configureOptions.ClientId))
 1055        {
 01056            throw new ArgumentException("ClientId must be provided in OAuth2Options", nameof(configureOptions));
 1057        }
 1058
 01059        if (string.IsNullOrWhiteSpace(configureOptions.AuthorizationEndpoint))
 1060        {
 01061            throw new ArgumentException("AuthorizationEndpoint must be provided in OAuth2Options", nameof(configureOptio
 1062        }
 1063
 01064        if (string.IsNullOrWhiteSpace(configureOptions.TokenEndpoint))
 1065        {
 01066            throw new ArgumentException("TokenEndpoint must be provided in OAuth2Options", nameof(configureOptions));
 1067        }
 1068
 1069        // Default CallbackPath if not set: /signin-{scheme}
 01070        if (string.IsNullOrWhiteSpace(configureOptions.CallbackPath))
 1071        {
 01072            configureOptions.CallbackPath = $"/signin-{authenticationScheme.ToLowerInvariant()}";
 1073        }
 1074        // Ensure host is set
 01075        if (configureOptions.Host != host)
 1076        {
 01077            configureOptions.Host = host;
 1078        }
 1079        // Ensure scheme is set
 01080        if (authenticationScheme != configureOptions.AuthenticationScheme)
 1081        {
 01082            configureOptions.AuthenticationScheme = authenticationScheme;
 1083        }
 1084        // Configure scopes and claim policies
 01085        ConfigureScopes(configureOptions, host.Logger);
 1086        // Configure OpenAPI
 01087        ConfigureOpenApi(host, authenticationScheme, configureOptions);
 1088
 1089        // register in host for introspection
 01090        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.OAuth2, configureOptions);
 1091
 1092        // Add authentication
 01093        return host.AddAuthentication(
 01094            defaultScheme: configureOptions.CookieScheme,
 01095            defaultChallengeScheme: authenticationScheme,
 01096            buildSchemes: ab =>
 01097            {
 01098                // Add cookie scheme for sign-in
 01099                _ = ab.AddCookie(configureOptions.CookieScheme, cookieOpts =>
 01100               {
 01101                   configureOptions.CookieOptions.ApplyTo(cookieOpts);
 01102               });
 01103                // Add OAuth2 scheme
 01104                _ = ab.AddOAuth(
 01105                    authenticationScheme: authenticationScheme,
 01106                    displayName: displayName ?? OAuthDefaults.DisplayName,
 01107                    configureOptions: oauthOpts =>
 01108                {
 01109                    configureOptions.ApplyTo(oauthOpts);
 01110                    if (host.Logger.IsEnabled(LogEventLevel.Debug))
 01111                    {
 01112                        host.Logger.Debug("Configured OpenID Connect with ClientId: {ClientId}, Scopes: {Scopes}",
 01113                          oauthOpts.ClientId, string.Join(", ", oauthOpts.Scope));
 01114                    }
 01115                });
 01116            },
 01117              configureAuthz: configureOptions.ClaimPolicy?.ToAuthzDelegate()
 01118        );
 1119    }
 1120
 1121    /// <summary>
 1122    /// Configures OAuth2 scopes and claim policies.
 1123    /// </summary>
 1124    /// <param name="configureOptions">The OAuth2 options to configure.</param>
 1125    /// <param name="logger">The logger for debug output.</param>
 1126    private static void ConfigureScopes(IOAuthCommonOptions configureOptions, Serilog.ILogger logger)
 1127    {
 31128        if (configureOptions.Scope is null)
 1129        {
 01130            return;
 1131        }
 1132
 31133        if (configureOptions.Scope.Count == 0)
 1134        {
 11135            BackfillScopesFromClaimPolicy(configureOptions, logger);
 11136            return;
 1137        }
 1138
 21139        LogConfiguredScopes(configureOptions.Scope, logger);
 1140
 21141        if (configureOptions.ClaimPolicy is null)
 1142        {
 11143            configureOptions.ClaimPolicy = BuildClaimPolicyFromScopes(configureOptions.Scope, logger);
 11144            return;
 1145        }
 1146
 11147        AddMissingScopesToClaimPolicy(configureOptions.Scope, configureOptions.ClaimPolicy, logger);
 11148    }
 1149
 1150    private static ClaimPolicyConfig BuildClaimPolicyFromScopes(ICollection<string> scopes, Serilog.ILogger logger)
 1151    {
 11152        var claimPolicyBuilder = new ClaimPolicyBuilder();
 61153        foreach (var scope in scopes)
 1154        {
 21155            LogScopeAdded(logger, scope);
 21156            _ = claimPolicyBuilder.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedVa
 1157        }
 1158
 11159        return claimPolicyBuilder.Build();
 1160    }
 1161
 1162    private static void AddMissingScopesToClaimPolicy(ICollection<string> scopes, ClaimPolicyConfig claimPolicy, Serilog
 1163    {
 11164        var missingScopes = scopes
 21165            .Where(s => !claimPolicy.Policies.ContainsKey(s))
 11166            .ToList();
 1167
 11168        if (missingScopes.Count == 0)
 1169        {
 01170            return;
 1171        }
 1172
 11173        LogMissingScopes(missingScopes, logger);
 1174
 11175        var claimPolicyBuilder = new ClaimPolicyBuilder();
 41176        foreach (var scope in missingScopes)
 1177        {
 11178            _ = claimPolicyBuilder.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedVa
 11179            LogScopeAddedToClaimPolicy(logger, scope);
 1180        }
 1181
 11182        claimPolicy.AddPolicies(claimPolicyBuilder.Policies);
 11183    }
 1184
 1185    private static void BackfillScopesFromClaimPolicy(IOAuthCommonOptions configureOptions, Serilog.ILogger logger)
 1186    {
 11187        if (configureOptions.ClaimPolicy is null)
 1188        {
 01189            return;
 1190        }
 1191
 61192        foreach (var policy in configureOptions.ClaimPolicy.PolicyNames)
 1193        {
 21194            LogClaimPolicyConfigured(logger, policy);
 21195            configureOptions.Scope?.Add(policy);
 1196        }
 11197    }
 1198
 1199    private static void LogScopeAdded(Serilog.ILogger logger, string scope)
 1200    {
 21201        if (logger.IsEnabled(LogEventLevel.Debug))
 1202        {
 21203            logger.Debug("OAuth2 scope added: {Scope}", scope);
 1204        }
 21205    }
 1206
 1207    private static void LogScopeAddedToClaimPolicy(Serilog.ILogger logger, string scope)
 1208    {
 11209        if (logger.IsEnabled(LogEventLevel.Debug))
 1210        {
 11211            logger.Debug("OAuth2 scope added to claim policy: {Scope}", scope);
 1212        }
 11213    }
 1214
 1215    private static void LogMissingScopes(IEnumerable<string> missingScopes, Serilog.ILogger logger)
 1216    {
 11217        if (logger.IsEnabled(LogEventLevel.Debug))
 1218        {
 11219            logger.Debug("Adding missing OAuth2 scopes to claim policy: {Scopes}", string.Join(", ", missingScopes));
 1220        }
 11221    }
 1222
 1223    private static void LogConfiguredScopes(IEnumerable<string> scopes, Serilog.ILogger logger)
 1224    {
 21225        if (logger.IsEnabled(LogEventLevel.Debug))
 1226        {
 21227            logger.Debug("OAuth2 scopes configured: {Scopes}", string.Join(", ", scopes));
 1228        }
 21229    }
 1230
 1231    private static void LogClaimPolicyConfigured(Serilog.ILogger logger, string policy)
 1232    {
 21233        if (logger.IsEnabled(LogEventLevel.Debug))
 1234        {
 21235            logger.Debug("OAuth2 claim policy configured: {Policy}", policy);
 1236        }
 21237    }
 1238
 1239    #endregion
 1240    #region OpenID Connect Authentication
 1241
 1242    /// <summary>
 1243    /// Adds OpenID Connect authentication to the Kestrun host with private key JWT client assertion.
 1244    /// <para>Use this for applications that require OpenID Connect authentication with client credentials using JWT ass
 1245    /// </summary>
 1246    /// <param name="host">The Kestrun host instance.</param>
 1247    /// <param name="authenticationScheme">The authentication scheme name.</param>
 1248    /// <param name="displayName">The display name for the authentication scheme.</param>
 1249    /// <param name="configureOptions">The OpenIdConnectOptions to configure the authentication.</param>
 1250    /// <returns>The configured KestrunHost instance.</returns>
 1251    public static KestrunHost AddOpenIdConnectAuthentication(
 1252           this KestrunHost host,
 1253           string authenticationScheme = AuthenticationDefaults.OidcSchemeName,
 1254           string? displayName = AuthenticationDefaults.OidcDisplayName,
 1255           OidcOptions? configureOptions = null)
 1256    {
 01257        if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1258        {
 01259            host.Logger.Debug("Adding OpenID Connect Authentication with scheme: {Scheme}", authenticationScheme);
 1260        }
 1261        // Ensure the scheme is not null
 01262        ArgumentNullException.ThrowIfNull(host);
 01263        ArgumentNullException.ThrowIfNull(authenticationScheme);
 01264        ArgumentNullException.ThrowIfNull(configureOptions);
 1265
 1266        // Ensure ClientId is set
 01267        if (string.IsNullOrWhiteSpace(configureOptions.ClientId))
 1268        {
 01269            throw new ArgumentException("ClientId must be provided in OpenIdConnectOptions", nameof(configureOptions));
 1270        }
 1271        // Ensure host is set
 01272        if (configureOptions.Host != host)
 1273        {
 01274            configureOptions.Host = host;
 1275        }
 1276        // Ensure scheme is set
 01277        if (authenticationScheme != configureOptions.AuthenticationScheme)
 1278        {
 01279            configureOptions.AuthenticationScheme = authenticationScheme;
 1280        }
 1281        // Retrieve supported scopes from the OIDC provider
 01282        if (!string.IsNullOrWhiteSpace(configureOptions.Authority))
 1283        {
 01284            configureOptions.ClaimPolicy = GetSupportedScopes(configureOptions.Authority, host.Logger);
 01285            if (host.Logger.IsEnabled(LogEventLevel.Debug))
 1286            {
 01287                host.Logger.Debug("OIDC supported scopes: {Scopes}", string.Join(", ", configureOptions.ClaimPolicy?.Pol
 1288            }
 1289        }
 1290        // Configure scopes and claim policies
 01291        ConfigureScopes(configureOptions, host.Logger);
 1292        // Configure OpenAPI
 01293        ConfigureOpenApi(host, authenticationScheme, configureOptions);
 1294
 1295        // register in host for introspection
 01296        _ = host.RegisteredAuthentications.Register(authenticationScheme, AuthenticationType.Oidc, configureOptions);
 1297
 1298        // CRITICAL: Register OidcEvents and AssertionService in DI before configuring authentication
 1299        // This is required because EventsType expects these to be available in the service provider
 01300        return host.AddService(services =>
 01301         {
 01302             // Register AssertionService as a singleton with factory to pass clientId and jwkJson
 01303             // Only register if JwkJson is provided (for private_key_jwt authentication)
 01304             if (!string.IsNullOrWhiteSpace(configureOptions.JwkJson))
 01305             {
 01306                 services.TryAddSingleton(sp => new AssertionService(configureOptions.ClientId, configureOptions.JwkJson
 01307                 // Register OidcEvents as scoped (per-request)
 01308                 services.TryAddScoped<OidcEvents>();
 01309             }
 01310         }).AddAuthentication(
 01311              defaultScheme: configureOptions.CookieScheme,
 01312              defaultChallengeScheme: authenticationScheme,
 01313              buildSchemes: ab =>
 01314              {
 01315                  // Add cookie scheme for sign-in
 01316                  _ = ab.AddCookie(configureOptions.CookieScheme, cookieOpts =>
 01317                 {
 01318                     // Copy cookie configuration from options.CookieOptions
 01319                     configureOptions.CookieOptions.ApplyTo(cookieOpts);
 01320                 });
 01321                  // Add OpenID Connect scheme
 01322                  _ = ab.AddOpenIdConnect(
 01323                    authenticationScheme: authenticationScheme,
 01324                    displayName: displayName ?? OpenIdConnectDefaults.DisplayName,
 01325                    configureOptions: oidcOpts =>
 01326                 {
 01327                     // Copy all properties from the provided options to the framework's options
 01328                     configureOptions.ApplyTo(oidcOpts);
 01329
 01330                     // Inject private key JWT at code → token step (only if JwkJson is provided)
 01331                     // This will be resolved from DI at runtime
 01332                     if (!string.IsNullOrWhiteSpace(configureOptions.JwkJson))
 01333                     {
 01334                         oidcOpts.EventsType = typeof(OidcEvents);
 01335                     }
 01336                     if (host.Logger.IsEnabled(LogEventLevel.Debug))
 01337                     {
 01338                         host.Logger.Debug("Configured OpenID Connect with Authority: {Authority}, ClientId: {ClientId},
 01339                             oidcOpts.Authority, oidcOpts.ClientId, string.Join(", ", oidcOpts.Scope));
 01340                     }
 01341                 });
 01342              },
 01343              configureAuthz: configureOptions.ClaimPolicy?.ToAuthzDelegate()
 01344            );
 1345    }
 1346
 1347    /// <summary>
 1348    /// Retrieves the supported scopes from the OpenID Connect provider's metadata.
 1349    /// </summary>
 1350    /// <param name="authority">The authority URL of the OpenID Connect provider.</param>
 1351    /// <param name="logger">The logger instance for logging.</param>
 1352    /// <returns>A ClaimPolicyConfig containing the supported scopes, or null if retrieval fails.</returns>
 1353    private static ClaimPolicyConfig? GetSupportedScopes(string authority, Serilog.ILogger logger)
 1354    {
 01355        if (logger.IsEnabled(LogEventLevel.Debug))
 1356        {
 01357            logger.Debug("Retrieving OpenID Connect configuration from authority: {Authority}", authority);
 1358        }
 01359        var claimPolicy = new ClaimPolicyBuilder();
 01360        if (string.IsNullOrWhiteSpace(authority))
 1361        {
 01362            throw new ArgumentException("Authority must be provided to retrieve OpenID Connect scopes.", nameof(authorit
 1363        }
 1364
 01365        var metadataAddress = authority.TrimEnd('/') + "/.well-known/openid-configuration";
 1366
 01367        var documentRetriever = new HttpDocumentRetriever
 01368        {
 01369            RequireHttps = metadataAddress.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
 01370        };
 1371
 01372        var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
 01373            metadataAddress,
 01374            new OpenIdConnectConfigurationRetriever(),
 01375            documentRetriever);
 1376
 1377        try
 1378        {
 01379            var cfg = configManager.GetConfigurationAsync(CancellationToken.None)
 01380                                   .GetAwaiter()
 01381                                   .GetResult();
 1382            // First try the strongly-typed property
 01383            var scopes = cfg.ScopesSupported;
 1384
 1385            // If it's null or empty, fall back to raw JSON
 01386            if (scopes == null || scopes.Count == 0)
 1387            {
 01388                var json = documentRetriever.GetDocumentAsync(metadataAddress, CancellationToken.None)
 01389                                            .GetAwaiter()
 01390                                            .GetResult();
 1391
 01392                using var doc = JsonDocument.Parse(json);
 01393                if (doc.RootElement.TryGetProperty("scopes_supported", out var scopesElement) &&
 01394                    scopesElement.ValueKind == JsonValueKind.Array)
 1395                {
 01396                    foreach (var scope in scopesElement.EnumerateArray().Select(item => item.GetString()).Where(s => !st
 1397                    {
 01398                        if (scope != null)
 1399                        {
 01400                            _ = claimPolicy.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, 
 1401                        }
 1402                    }
 1403                }
 1404            }
 1405            else
 1406            {
 1407                // Normal path: configuration object had scopes
 01408                foreach (var scope in scopes)
 1409                {
 01410                    _ = claimPolicy.AddPolicy(policyName: scope, claimType: "scope", description: string.Empty, allowedV
 1411                }
 1412            }
 01413            return claimPolicy.Build();
 1414        }
 01415        catch (Exception ex)
 1416        {
 01417            logger.Warning(ex, "Failed to retrieve OpenID Connect configuration from {MetadataAddress}", metadataAddress
 01418            return null;
 1419        }
 01420    }
 1421
 1422    #endregion
 1423    #region Helper Methods
 1424    /// <summary>
 1425    /// Configures OpenAPI security schemes for the given authentication options.
 1426    /// </summary>
 1427    /// <param name="host">The Kestrun host instance.</param>
 1428    /// <param name="scheme">The authentication scheme name.</param>
 1429    /// <param name="opts">The OpenAPI authentication options.</param>
 1430    private static void ConfigureOpenApi(KestrunHost host, string scheme, IOpenApiAuthenticationOptions opts)
 1431    {
 1432        // Apply to specified documentation IDs or all if none specified
 211433        if (opts.DocumentationId == null || opts.DocumentationId.Length == 0)
 1434        {
 211435            opts.DocumentationId = OpenApiDocDescriptor.DefaultDocumentationIds;
 1436        }
 1437
 841438        foreach (var docDescriptor in opts.DocumentationId
 211439            .Select(host.GetOrCreateOpenApiDocument)
 421440            .Where(docDescriptor => docDescriptor != null))
 1441        {
 211442            docDescriptor.ApplySecurityScheme(scheme, opts);
 1443        }
 211444    }
 1445
 1446    #endregion
 1447
 1448    /// <summary>
 1449    /// Adds authentication and authorization middleware to the Kestrun host.
 1450    /// </summary>
 1451    /// <param name="host">The Kestrun host instance.</param>
 1452    /// <param name="buildSchemes">A delegate to configure authentication schemes.</param>
 1453    /// <param name="defaultScheme">The default authentication scheme.</param>
 1454    /// <param name="configureAuthz">Optional authorization policy configuration.</param>
 1455    /// <param name="defaultChallengeScheme">The default challenge scheme .</param>
 1456    /// <returns>The configured KestrunHost instance.</returns>
 1457    internal static KestrunHost AddAuthentication(
 1458    this KestrunHost host,
 1459    string defaultScheme,
 1460    Action<AuthenticationBuilder>? buildSchemes = null,    // e.g., ab => ab.AddCookie().AddOpenIdConnect("oidc", ...)
 1461    Action<AuthorizationOptions>? configureAuthz = null,
 1462    string? defaultChallengeScheme = null)
 1463    {
 211464        ArgumentNullException.ThrowIfNull(buildSchemes);
 211465        if (string.IsNullOrWhiteSpace(defaultScheme))
 1466        {
 01467            throw new ArgumentException("Default scheme is required.", nameof(defaultScheme));
 1468        }
 1469
 211470        _ = host.AddService(services =>
 211471        {
 211472            // CRITICAL: Check if authentication services are already registered
 211473            // If they are, we only need to add new schemes, not reconfigure defaults
 24181474            var authDescriptor = services.FirstOrDefault(d => d.ServiceType == typeof(IAuthenticationService));
 211475
 211476            AuthenticationBuilder authBuilder;
 211477            if (authDescriptor != null)
 211478            {
 211479                // Authentication already registered - only add new schemes without changing defaults
 01480                host.Logger.Debug("Authentication services already registered - adding schemes only (default={DefaultSch
 01481                authBuilder = new AuthenticationBuilder(services);
 211482            }
 211483            else
 211484            {
 211485                // First time registration - configure defaults
 211486                host.Logger.Debug(
 211487                    "Registering authentication services with defaults (default={DefaultScheme}, challenge={ChallengeSch
 211488                    defaultScheme,
 211489                    defaultChallengeScheme ?? defaultScheme);
 211490                authBuilder = services.AddAuthentication(options =>
 211491                {
 141492                    options.DefaultScheme = defaultScheme;
 141493                    options.DefaultChallengeScheme = defaultChallengeScheme ?? defaultScheme;
 351494                });
 211495            }
 211496
 211497            // Let caller add handlers/schemes
 211498            buildSchemes?.Invoke(authBuilder);
 211499
 211500            // Ensure Authorization is available (with optional customization)
 211501            // AddAuthorization is idempotent - safe to call multiple times
 211502            _ = configureAuthz is not null ?
 211503                services.AddAuthorization(configureAuthz) :
 211504                services.AddAuthorization();
 241505        });
 1506
 1507        // Add middleware once
 211508        return host.Use(app =>
 211509        {
 211510            const string Key = "__kr.authmw";
 211511            if (!app.Properties.ContainsKey(Key))
 211512            {
 211513                _ = app.UseAuthentication();
 211514                _ = app.UseAuthorization();
 211515                app.Properties[Key] = true;
 211516                host.Logger.Information("Kestrun: Authentication & Authorization middleware added.");
 211517            }
 421518        });
 1519    }
 1520
 1521    /// <summary>
 1522    /// Checks if the specified authentication scheme is registered in the Kestrun host.
 1523    /// </summary>
 1524    /// <param name="host">The Kestrun host instance.</param>
 1525    /// <param name="schemeName">The name of the authentication scheme to check.</param>
 1526    /// <returns>True if the scheme is registered; otherwise, false.</returns>
 1527    public static bool HasAuthScheme(this KestrunHost host, string schemeName)
 1528    {
 141529        var schemeProvider = host.App.Services.GetRequiredService<IAuthenticationSchemeProvider>();
 141530        var scheme = schemeProvider.GetSchemeAsync(schemeName).GetAwaiter().GetResult();
 141531        return scheme != null;
 1532    }
 1533
 1534    /// <summary>
 1535    /// Adds authorization services to the Kestrun host.
 1536    /// </summary>
 1537    /// <param name="host">The Kestrun host instance.</param>
 1538    /// <param name="cfg">Optional configuration for authorization options.</param>
 1539    /// <returns>The configured KestrunHost instance.</returns>
 1540    public static KestrunHost AddAuthorization(this KestrunHost host, Action<AuthorizationOptions>? cfg = null)
 1541    {
 11542        return host.AddService(services =>
 11543        {
 11544            _ = cfg == null ? services.AddAuthorization() : services.AddAuthorization(cfg);
 21545        });
 1546    }
 1547
 1548    /// <summary>
 1549    /// Checks if the specified authorization policy is registered in the Kestrun host.
 1550    /// </summary>
 1551    /// <param name="host">The Kestrun host instance.</param>
 1552    /// <param name="policyName">The name of the authorization policy to check.</param>
 1553    /// <returns>True if the policy is registered; otherwise, false.</returns>
 1554    public static bool HasAuthPolicy(this KestrunHost host, string policyName)
 1555    {
 131556        var policyProvider = host.App.Services.GetRequiredService<IAuthorizationPolicyProvider>();
 131557        var policy = policyProvider.GetPolicyAsync(policyName).GetAwaiter().GetResult();
 131558        return policy != null;
 1559    }
 1560
 1561    /// <summary>
 1562    /// HTTP message handler that logs all HTTP requests and responses for debugging.
 1563    /// </summary>
 01564    internal class LoggingHttpMessageHandler(HttpMessageHandler innerHandler, Serilog.ILogger logger) : DelegatingHandle
 1565    {
 01566        private readonly Serilog.ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger));
 1567
 1568        // CRITICAL: Static field to store the last token response body so we can manually parse it
 1569        // The framework's OpenIdConnectMessage parser fails to populate AccessToken correctly
 01570        internal static string? LastTokenResponseBody { get; private set; }
 1571
 1572        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cance
 1573        {
 1574            // Log request
 01575            _logger.Warning($"HTTP {request.Method} {request.RequestUri}");
 1576
 1577            // Check if this is a token endpoint request
 01578            var isTokenEndpoint = request.RequestUri?.PathAndQuery?.Contains("/connect/token") == true ||
 01579                                 request.RequestUri?.PathAndQuery?.Contains("/token") == true;
 1580
 01581            if (request.Content != null && !isTokenEndpoint)
 1582            {
 1583                // Read request body without consuming it (only for non-token requests)
 01584                var requestBytes = await request.Content.ReadAsByteArrayAsync(cancellationToken);
 01585                var requestBody = System.Text.Encoding.UTF8.GetString(requestBytes);
 01586                _logger.Warning($"Request Body: {requestBody}");
 1587
 1588                // Recreate the content so it can be read again
 01589                request.Content = new ByteArrayContent(requestBytes);
 01590                request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/x-ww
 1591            }
 01592            else if (request.Content != null && isTokenEndpoint)
 1593            {
 01594                _logger.Warning("Token endpoint request - skipping body logging to preserve stream");
 1595            }
 1596
 1597            // Send request
 01598            var response = await base.SendAsync(request, cancellationToken);
 1599
 1600            // Log response
 01601            _logger.Warning($"HTTP Response: {(int)response.StatusCode} {response.StatusCode}");
 1602
 1603            // CRITICAL: For token endpoint responses, capture the body for manual parsing
 1604            // but then recreate the stream so the framework can also read it
 01605            if (response.Content != null && isTokenEndpoint)
 1606            {
 1607                // Read the response body
 01608                var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
 01609                var responseBody = System.Text.Encoding.UTF8.GetString(responseBytes);
 1610
 1611                // Store it in static field for later manual parsing
 01612                LastTokenResponseBody = responseBody;
 01613                _logger.Warning($"Captured token response body ({responseBytes.Length} bytes) for manual parsing");
 1614
 1615                // Recreate the content stream with ALL original headers preserved
 01616                var originalHeaders = response.Content.Headers.ToList();
 01617                var newContent = new ByteArrayContent(responseBytes);
 1618
 01619                foreach (var header in originalHeaders)
 1620                {
 01621                    _ = newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
 1622                }
 1623
 01624                response.Content = newContent;
 01625                _logger.Warning("Recreated token response stream for framework parsing");
 1626            }
 01627            else if (response.Content != null && !isTokenEndpoint)
 1628            {
 1629                // Save original headers
 01630                var originalHeaders = response.Content.Headers;
 1631
 1632                // Read response body and preserve it for the handler
 01633                var responseBytes = await response.Content.ReadAsByteArrayAsync(cancellationToken);
 01634                var responseBody = System.Text.Encoding.UTF8.GetString(responseBytes);
 01635                _logger.Warning($"Response Body: {responseBody}");
 1636
 1637                // Recreate the content so it can be read again by the OIDC handler
 01638                var newContent = new ByteArrayContent(responseBytes);
 1639
 1640                // Copy all original headers to the new content
 01641                foreach (var header in originalHeaders)
 1642                {
 01643                    _ = newContent.Headers.TryAddWithoutValidation(header.Key, header.Value);
 1644                }
 1645
 01646                response.Content = newContent;
 01647            }
 01648            else if (response.Content != null && isTokenEndpoint)
 1649            {
 01650                _logger.Warning("Token endpoint response - skipping body logging to let framework parse it");
 1651            }
 1652
 01653            return response;
 01654        }
 1655    }
 1656}

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()