using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; namespace Yavsc.Server.Helpers; /// /// Shared service registration helpers for Yavsc runtime services (Api, Blogs, Org, ...). /// /// Conventions: every service reads its CORS origin allow-list from /// Site:CorsAllowedOrigins as a JSON array, and its JWT Bearer authority /// from Site:Authority. These helpers enforce that contract so a service /// only needs to opt-in via a single line. /// public static class ServiceExtensions { /// /// Default policy name used across all Yavsc runtime services. /// public const string DefaultCorsPolicyName = "default"; /// /// Register the shared "default" CORS policy, sourcing the allow-list /// from Site:CorsAllowedOrigins. Fails closed (no origins registered) /// when the array is missing or empty. /// /// The service collection to add CORS to. /// Configuration root, used to read Site:CorsAllowedOrigins. /// Optional policy name override (defaults to ). /// The same instance for chaining. public static IServiceCollection AddYavscCors( this IServiceCollection services, IConfiguration configuration, string policyName = DefaultCorsPolicyName) { var allowedOrigins = configuration .GetSection("Site:CorsAllowedOrigins") .Get() ?? Array.Empty(); services.AddCors(options => { options.AddPolicy(policyName, policy => { if (allowedOrigins.Length == 0) { // Fail closed: with no origins configured, don't fall back to "*". // The policy ends up effectively denying cross-origin requests, // which is the safe default. return; } policy.WithOrigins(allowedOrigins) .AllowAnyHeader() .AllowAnyMethod(); }); }); return services; } /// /// Register the standard Yavsc JWT Bearer authentication scheme, sourcing /// the authority from Site:Authority. Throws at startup if the /// configuration is missing — this is intentional, we'd rather fail to /// boot than accept tokens from an unconfigured issuer. /// /// The authentication builder to extend. /// Configuration root, used to read Site:Authority. /// Optional callback for service-specific options /// (e.g. setting options.Audience in Yavsc.Org). /// Optional scheme name override (defaults to "Bearer"). /// The same authentication builder, for chaining. public static AuthenticationBuilder AddYavscJwtBearer( this AuthenticationBuilder builder, IConfiguration configuration, Action? configure = null, string schemeName = "Bearer") { var authority = configuration.GetSection("Site")["Authority"] ?? throw new InvalidOperationException( "Site:Authority is required to configure Yavsc JWT Bearer authentication."); return builder.AddJwtBearer(schemeName, options => { options.IncludeErrorDetails = true; options.Authority = authority; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, RoleClaimType = YavscConstants.RoleClaimType }; options.MapInboundClaims = true; // Dev: every Yavsc resource service (Yavsc.Api, Yavsc.Blogs, // Yavsc.Org itself) validates JWTs against the OP that runs // on https://localhost:5001 with a self-signed dev cert. // The default .NET HttpClient rejects self-signed certs, so // JwtBearer's backchannel silently fails to fetch the OIDC // discovery + JWKS. With an empty ValidIssuer, every token // is rejected with IDX10204 ("ValidIssuer is null or // whitespace"). Telling the backchannel to skip TLS // validation unblocks discovery in dev. Production uses a // real CA-signed cert and the default validation path; the // override is gated on HostingEnvironment == Development // and only fires when the consumer opt-in via the // 'Yavsc:Dev:TlsInsecure' configuration flag (default // false), so a misconfigured production environment cannot // silently downgrade TLS. if (configuration.GetValue("ASPNETCORE_ENVIRONMENT") == "Development") { options.BackchannelHttpHandler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (_, _, _, _) => true }; } configure?.Invoke(options); }); } }