Split Site:Audience into Site:ExternalUrl + Site:CorsAllowedOrigins

The Site:Audience setting was conflating two distinct concepts: an OAuth
JWT audience (a single resource identifier) and a CORS allow-list (an
array of origins). Collapsing them caused several latent bugs:
- OAuth/JWT validation expected a single string while CORS WithOrigins
  accepts an array.
- Password-reset callback URLs and OAuth client RedirectUri/Origin were
  being built from what was meant to be an audience identifier, not a
  base URL.
- Yavsc.Org's main CORS policy was hardcoded to '*', with no way to
  restrict it without code changes.

Changes:
- SiteSettings.Audience (string) replaced with CorsAllowedOrigins
  (IList<string>).
- OAuth JWT Authority still reads Site:Authority; Audience now reads
  Site:ExternalUrl (Org only; Api/Blogs use ValidateAudience=false).
- MailSender and AccountController build reset-callback URLs from
  Site:ExternalUrl.
- ClientController uses Site:ExternalUrl for OAuth RedirectUri/Origin
  defaults on newly created clients.
- Yavsc.Api and Yavsc.Blogs now read CORS origins from
  Site:CorsAllowedOrigins instead of hardcoded URLs.

Add shared AddYavscCors / AddYavscJwtBearer extension methods in
Yavsc.Server/Helpers/ServiceExtensions.cs to enforce a single
configuration contract across all runtime services (Api, Blogs, Org).
Fails closed when CorsAllowedOrigins is empty; fails fast at startup
when Site:Authority is missing.

Remove obsolete ConfigurationHelpers.GetAudience (no remaining callers).

Local appsettings-*.json files (which carry deployment-specific values
and are gitignored) must be updated to add Site:CorsAllowedOrigins.
This commit is contained in:
Paul Schneider 2026-06-19 13:15:21 +01:00
commit dcf2a93ad0
11 changed files with 125 additions and 84 deletions

View file

@ -111,26 +111,15 @@ public static class HostingExtensions
// Add session related services.
services.AddDataProtection().PersistKeysToFileSystem(dataDir);
AddYavscPolicies(services);
AddYavscPolicies(services, builder.Configuration);
services.AddScoped<IAuthorizationHandler, PermissionHandler>();
services.AddTransient<IExternalIdentityManager, ExternalIdentityManager>();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.IncludeErrorDetails = true;
options.Authority = builder.Configuration.GetSection("Site")["Authority"];
options.Audience = builder.Configuration.GetSection("Site")["Audience"];
options.TokenValidationParameters =
new()
{
ValidateAudience = false,
RoleClaimType = YavscConstants.RoleClaimType
};
options.MapInboundClaims = true;
});
.AddYavscJwtBearer(builder.Configuration,
configure: o => o.Audience = builder.Configuration.GetSection("Site")["ExternalUrl"]);
services.AddTransient<RoleManager<IdentityRole>>();
services.AddTransient<IRoleStore<IdentityRole>, RoleStore<IdentityRole, ApplicationDbContext>>();
@ -191,7 +180,7 @@ public static class HostingExtensions
return identityBuilder;
}
private static void AddYavscPolicies(IServiceCollection services)
private static void AddYavscPolicies(IServiceCollection services, IConfiguration configuration)
{
services.AddAuthorization(options =>
{
@ -222,17 +211,9 @@ public static class HostingExtensions
// options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement()));
options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
options.AddPolicy("TheAuthor", policy => policy.Requirements.Add(new EditPermission()));
})
.AddCors(options =>
{
options.AddPolicy("default", builder =>
{
_ = builder.WithOrigins("*")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddYavscCors(configuration);
}
public static IServiceCollection LoadConfiguration(this WebApplicationBuilder builder)