The partial class ClientController had two constructors declared
across ClientController.cs and ClientController.Collections.cs.
ASP.NET Core DI failed to pick one at request time with:
System.InvalidOperationException: Multiple constructors accepting
all given argument types have been found in type
'Yavsc.Controllers.ClientController'.
Move IHtmlLocalizer<ClientController> into the primary constructor
in ClientController.cs and drop the duplicate one in
ClientController.Collections.cs. The Collections partial now keeps
only its readonly field and action methods; the constructor and
field assignment are unified on the main file.
Also add the missing 'using Microsoft.AspNetCore.Mvc.Localization;'
to ClientController.cs so IHtmlLocalizer resolves.
The OAuth2 client editor at /Client/Edit/{id} previously exposed 8
fields out of ~30 scalars and 10 collections on the IdentityServer8
Client entity. Editing the collections (RedirectUris, Scopes, Grant
Types, Cors Origins, IdP Restrictions, Claims, Properties, Secrets)
was either impossible or jammed into a single broken text input that
bound against an IEnumerable<string> property.
Restructure into per-collection subpages, each with its own
list/add/remove flow:
- RedirectUris /Client/EditRedirectUris/{id}
- PostLogoutRedirectUris /Client/EditPostLogoutRedirectUris/{id}
- Scopes /Client/EditScopes/{id}
- GrantTypes /Client/EditGrantTypes/{id}
- CorsOrigins /Client/EditCorsOrigins/{id}
- IdPRestrictions /Client/EditIdPRestrictions/{id}
- Claims /Client/EditClaims/{id}
- Properties /Client/EditProperties/{id}
- Secrets /Client/EditSecrets/{id}
Implementation:
- New partial class ClientController.Collections.cs with one
GET/Add/Remove trio per collection. Add/Remove dispatch through
generic helpers that handle the EF row + ClientId check.
- Shared _EditableStringList.cshtml partial consumed by the six
single-string-field collection pages. Uses reflection to pull
the value field and the row Id off the entity — avoids six
nearly-identical table+form copies.
- Claims / Properties / Secrets each have their own view because
they carry 2+ fields (Type+Value, Key+Value, or
Type+Value+Description+Expiration).
- Main Edit.cshtml enriched: ClientId/Id hidden, all scalar
fields split into fieldsets (Core, Security, Logout, Tokens,
Device flow, Tokens extra), nav links to the 9 subpages with
current row counts as badges.
- ClientController.Edit(int) GET now loads the client with all
navigations via LoadClientAsync so the Edit.cshtml nav badges
render real counts.
Field-correctness notes (verified by disassembling HigginsSoft
IdentityServer8.EntityFramework.Entities.Client 8.0.5-preview-net9):
- The property is PairWiseSubjectSalt, not PairwiseSubjectSalt
(capital W on 'Wise').
- CibaLifetime and PollingInterval do NOT exist on Client in this
IdentityServer8 version — those properties were a guess. The
Device flow fieldset contains DeviceCodeLifetime + UserCodeType
instead.
- AllowedIdentityTokenSigningAlgorithms and AllowAccessTokensViaBrowser
were missing from the original form and are now exposed.
- ConsentLifetime and UserSsoLifetime are int? (nullable); the form
binds them as plain int fields which accept empty strings.
Security:
- All new actions stay under [Authorize('AdministratorOnly')].
- Each Add/Remove takes an explicit id (Client.Id) and the row's
ClientId is checked on the server before any delete; a rowId
from another client returns NotFound.
Docs:
- doc/dev-tracking/client-editor-overhaul.md — inventory, status,
follow-up ideas (confirmation prompts, validation, MVC tests).
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.