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).
9.7 KiB
Client editor overhaul — Yavsc.Org administration
Goal
Bring the OAuth2 client administration UI (/Client/Edit/{id} and friends)
in Yavsc.Org to feature parity with the IdentityServer8 Client entity
model. Today the editor only exposes a handful of scalar fields and a few
single-line inputs for collections; the bulk of the entity and its
related collections are unreachable from the UI.
Inventory — current state
Properties exposed by Views/Client/Edit.cshtml
| Field | Type | Notes |
|---|---|---|
ClientId |
string | hidden, identifier |
Enabled |
bool | checkbox |
ClientName |
string | display name |
FrontChannelLogoutUri |
string | only front-channel, no back-channel |
RedirectUris |
collection | rendered as a single text input |
IdentityTokenLifetime |
int | seconds |
AbsoluteRefreshTokenLifetime |
int | seconds |
ClientSecrets |
collection | rendered as a single text input |
AccessTokenType |
enum | dropdown (custom SetAppTypesInputValues) |
Properties of IdentityServer8.EntityFramework.Entities.Client NOT in the editor
Core scalars (16 fields missing):
DescriptionClientUriLogoUriRequireConsentRequirePkceRequireRequestObjectRequireClientSecretAllowPlainTextPkceAllowOfflineAccessAllowRememberConsentAlwaysIncludeUserClaimsInIdTokenAlwaysSendClientClaimsAuthorizationCodeLifetimeBackChannelLogoutUriBackChannelLogoutSessionRequiredCibaLifetimeClientClaimsPrefixConsentLifetimeCreatedDeviceCodeLifetimeEnableLocalLoginEnabledFrontChannelLogoutSessionRequiredIncludeJwtIdLastAccessedLogoUriNonEditablePairwiseSubjectSaltPollingIntervalProtocolTypeRefreshTokenExpirationRefreshTokenUsageSlidingRefreshTokenLifetimeUpdateAccessTokenClaimsOnRefreshUpdatedUserCodeTypeUserSsoLifetime
Collections (8 missing — currently either not exposed at all, or jammed into a single-line text input that doesn't work for an IEnumerable):
AllowedGrantTypes→ClientGrantType(GrantType)AllowedScopes→ClientScope(Scope)RedirectUris→ClientRedirectUri(RedirectUri) — exposed but brokenPostLogoutRedirectUris→ClientPostLogoutRedirectUri(PostLogoutRedirectUri)AllowedCorsOrigins→ClientCorsOrigin(Origin)IdentityProviderRestrictions→ClientIdPRestriction(Provider)Claims→ClientClaim(Type, Value)Properties→ClientProperty(Key, Value)ClientSecrets→ClientSecret(Type, Value, Description, Created, Expiration) — exposed but brokenAllowedSigningAlgorithms→ scalar string collection on Client itself
Pages to add
Pattern: one Razor page per collection under
Views/Client/Edit{Collection}.cshtml. Each page lists existing rows,
offers an "Add" form with the relevant fields, and a per-row
remove button. The main Edit.cshtml becomes a hub page with links
to each subpage plus the scalar fields it already has.
| Page | Route | Form fields |
|---|---|---|
Edit.cshtml |
GET /Client/Edit/{id} (existing) |
scalar fields + nav links |
EditRedirectUris.cshtml |
GET /Client/EditRedirectUris/{id} |
RedirectUri |
EditPostLogoutRedirectUris.cshtml |
GET /Client/EditPostLogoutRedirectUris/{id} |
PostLogoutRedirectUri |
EditScopes.cshtml |
GET /Client/EditScopes/{id} |
Scope (with select of known scopes) |
EditGrantTypes.cshtml |
GET /Client/EditGrantTypes/{id} |
GrantType (with select of known types) |
EditCorsOrigins.cshtml |
GET /Client/EditCorsOrigins/{id} |
Origin |
EditIdPRestrictions.cshtml |
GET /Client/EditIdPRestrictions/{id} |
Provider |
EditClaims.cshtml |
GET /Client/EditClaims/{id} |
Type, Value |
EditProperties.cshtml |
GET /Client/EditProperties/{id} |
Key, Value |
EditSecrets.cshtml (replacement) |
GET /Client/EditSecrets/{id} |
Type, Value, Description, Expiration |
Partial view _EditableList.cshtml factored once and consumed by all
of the above.
Controller actions to add
For each collection Foo:
GET EditFoo(int id)— load the client, render the pagePOST AddFoo(int id, …)— append a row, redirect toEditFooPOST RemoveFoo(int id, int rowId)— delete a row, redirect
Verification
dotnet build src/Yavsc.Org/Yavsc.Org.csproj→ 0 errors- No tests in
Yavsc.Org.Testsexercise the controller today (perfind … -name "ClientController*" -not -path "*/bin/*"). Smoke-test by logging in as admin, hitting/Client/Edit/1, then eachEdit*/1page, and verifying the add/remove POSTs. - Existing seed flow (
MigratePostItClientToPublicinHostingExtensions.cs) must keep working — the editor changes are additive, not destructive.
Out of scope
- Tests (no MVC test infrastructure currently exists for this controller)
- Migration of existing collection fields (the broken
RedirectUristext input will simply be replaced by the new subpage) - Per-collection authorization policies (the controller is already
[Authorize("AdministratorOnly")]) - Client cloning / templating / JSON import-export
Status
2026-06-21 16:04 — kickoff. Inventory done. Pages not yet started.
2026-06-21 16:11 — first delivery, build does not compile by design (per Paul: "Tu peux même me laisser un travail qui ne compile pas"). The structural work is done; the residual errors are easy fixes Paul will do in a debug session.
Files added (working tree, not yet committed):
src/Yavsc.Org/Controllers/Administration/ClientController.Collections.cs— partial class with the per-collection GET / Add / Remove actions.src/Yavsc.Org/Views/Client/EditRedirectUris.cshtmlsrc/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtmlsrc/Yavsc.Org/Views/Client/EditScopes.cshtmlsrc/Yavsc.Org/Views/Client/EditGrantTypes.cshtmlsrc/Yavsc.Org/Views/Client/EditCorsOrigins.cshtmlsrc/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtmlsrc/Yavsc.Org/Views/Client/EditClaims.cshtmlsrc/Yavsc.Org/Views/Client/EditProperties.cshtmlsrc/Yavsc.Org/Views/Client/EditSecrets.cshtmlsrc/Yavsc.Org/Views/Client/_EditableStringList.cshtml— partial consumed by the single-string-field collection pages.
Files modified:
src/Yavsc.Org/Controllers/Administration/ClientController.cs—class→partial class; theEdit(int id)GET now usesLoadClientAsyncto load all navigations (so the new Edit.cshtml can render counts in its nav links).src/Yavsc.Org/Views/Client/Edit.cshtml— significantly enriched: nav links to the 9 sub-pages, all the scalar fields split into fieldsets (Security, Logout, Tokens, Device / CIBA, Tokens-extra), ClientId / Id hidden.
Known residual compile errors (4 errors total)
Paul is fixing these in a debug session. The structure is sound; the
errors are missing properties on the Client entity, a Razor
nullable quirk, and a Localizer injection miss.
Edit.cshtml:249—PairwiseSubjectSaltdoesn't exist onIdentityServer8.EntityFramework.Entities.Client. Fix: drop the field from Edit.cshtml; IdentityServer8 likely uses a different property name (e.g. on a related entity) or doesn't expose it.Edit.cshtml:221—CibaLifetimedoesn't exist onClient. Fix: same as above. CIBA flow may be configured elsewhere (resource-level) or via a different property.ClientController.Collections.cslines 181, 217, 253, 304 —Localizeris not available in the partial class. Fix: injectIStringLocalizer<ClientController>via the constructor, or inline the strings ("BothTypeAndValueRequired", "KeyRequired", "ValueRequired", "SecretValueRequired").EditSecrets.cshtml:44—s.Expiration?.ToString("u")on aDateTime?. Fix: justs.Expiration?.ToString("u")works if you writes.Expiration.Value.ToString("u"), or use(s.Expiration is null ? "" : s.Expiration.Value.ToString("u")), ors.Expiration?.ToString("u") ?? string.Empty.
Suggested next session
Once the 4 compile errors are fixed and the pages render:
- Smoke test by logging in as admin, hitting
/Client/Edit/1, then eachEdit*/1page, and verifying add/remove POSTs. - Add a confirmation prompt (or 2-step form) for Remove actions — removing a Redirect URI is destructive and one click is too easy.
- Wire up some collection-level validation (e.g. redirect URI must be a valid URL) at the controller level.
- Add tests — the project doesn't have MVC test infrastructure
today; consider adding a
Yavsc.Org.Testsproject that drives the controller viaWebApplicationFactory<Program>.