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

@ -590,7 +590,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
_siteSettings.Title,
callbackUrl,
_siteSettings.Slogan,
_siteSettings.Audience));
_siteSettings.ExternalUrl));
// No, wait for more than a login pass submission:
// do not await _signInManager.SignInAsync(user, isPersistent: false);
@ -641,7 +641,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
this._localizer["ConfirmYourAccountTitle"],
string.Format(this._localizer["ConfirmYourAccountBody"],
_siteSettings.Title, callbackUrl, _siteSettings.Slogan,
_siteSettings.Audience));
_siteSettings.ExternalUrl));
return new EmailSentViewModel { EMail = user.Email, Sent = true, MessageId = res };
}
@ -654,7 +654,7 @@ IHtmlLocalizerFactory htmlLocalizerFactory,
this._localizer["AccountEmailFactorTitle"],
string.Format(this._localizer["AccountEmailFactorBody"],
_siteSettings.Title, callbackUrl, _siteSettings.Slogan,
_siteSettings.Audience, code));
_siteSettings.ExternalUrl, code));
return new EmailSentViewModel { EMail = user.Email, Sent = true, MessageId = res }; ;
}
//

View file

@ -87,13 +87,13 @@ namespace Yavsc.Controllers
dbContext.ClientRedirectUris.Add(new ClientRedirectUri
{
ClientId = client.Id,
RedirectUri = siteSettings.Audience
RedirectUri = siteSettings.ExternalUrl
});
dbContext.ClientCorsOrigins.Add(new ClientCorsOrigin
{
ClientId = client.Id,
Origin = siteSettings.Audience
Origin = siteSettings.ExternalUrl
});
foreach (String credType in new String[] { "code", "client_credentials", "password" })

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)