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

@ -30,11 +30,8 @@ internal class Program
var builder = WebApplication.CreateBuilder(args);
builder.AddConfiguration("blogs");
var services = builder.Services;
var authority = builder.GetAuthority();
var audience = builder.GetAudience();
var services = builder.Services;
// builder.Services.AddDistributedMemoryCache();
@ -48,31 +45,15 @@ internal class Program
{
policy
.RequireAuthenticatedUser()
.RequireClaim(JwtClaimTypes.Scope, new string[] { "blog" });
});
})
.AddCors(options =>
{
// this defines a CORS policy called "default"
options.AddPolicy("default", policy =>
{
policy.WithOrigins(audience)
.AllowAnyHeader()
.AllowAnyMethod();
.RequireClaim(JwtClaimTypes.Scope, new string[] { "blogs" });
});
})
.AddYavscCors(builder.Configuration)
.AddControllers();
// accepts any access token issued by identity server
var authenticationBuilder = services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.IncludeErrorDetails = true;
options.Authority = authority;
options.TokenValidationParameters =
new() { ValidateAudience = false, RoleClaimType = YavscConstants.RoleClaimType };
options.MapInboundClaims = true;
});
services.AddAuthentication("Bearer")
.AddYavscJwtBearer(builder.Configuration);
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName)));