yavsc/doc/dev-tracking/client-editor-overhaul.md
Paul Schneider ecac359344 yavsc-org: OAuth2 client admin editor overhaul — per-collection pages + missing fields
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).
2026-06-21 16:53:34 +01:00

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):

  • Description
  • ClientUri
  • LogoUri
  • RequireConsent
  • RequirePkce
  • RequireRequestObject
  • RequireClientSecret
  • AllowPlainTextPkce
  • AllowOfflineAccess
  • AllowRememberConsent
  • AlwaysIncludeUserClaimsInIdToken
  • AlwaysSendClientClaims
  • AuthorizationCodeLifetime
  • BackChannelLogoutUri
  • BackChannelLogoutSessionRequired
  • CibaLifetime
  • ClientClaimsPrefix
  • ConsentLifetime
  • Created
  • DeviceCodeLifetime
  • EnableLocalLogin
  • Enabled
  • FrontChannelLogoutSessionRequired
  • IncludeJwtId
  • LastAccessed
  • LogoUri
  • NonEditable
  • PairwiseSubjectSalt
  • PollingInterval
  • ProtocolType
  • RefreshTokenExpiration
  • RefreshTokenUsage
  • SlidingRefreshTokenLifetime
  • UpdateAccessTokenClaimsOnRefresh
  • Updated
  • UserCodeType
  • UserSsoLifetime

Collections (8 missing — currently either not exposed at all, or jammed into a single-line text input that doesn't work for an IEnumerable):

  • AllowedGrantTypesClientGrantType (GrantType)
  • AllowedScopesClientScope (Scope)
  • RedirectUrisClientRedirectUri (RedirectUri) — exposed but broken
  • PostLogoutRedirectUrisClientPostLogoutRedirectUri (PostLogoutRedirectUri)
  • AllowedCorsOriginsClientCorsOrigin (Origin)
  • IdentityProviderRestrictionsClientIdPRestriction (Provider)
  • ClaimsClientClaim (Type, Value)
  • PropertiesClientProperty (Key, Value)
  • ClientSecretsClientSecret (Type, Value, Description, Created, Expiration) — exposed but broken
  • AllowedSigningAlgorithms → 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 page
  • POST AddFoo(int id, …) — append a row, redirect to EditFoo
  • POST 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.Tests exercise the controller today (per find … -name "ClientController*" -not -path "*/bin/*"). Smoke-test by logging in as admin, hitting /Client/Edit/1, then each Edit*/1 page, and verifying the add/remove POSTs.
  • Existing seed flow (MigratePostItClientToPublic in HostingExtensions.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 RedirectUris text 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.cshtml
  • src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml
  • src/Yavsc.Org/Views/Client/EditScopes.cshtml
  • src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml
  • src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml
  • src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml
  • src/Yavsc.Org/Views/Client/EditClaims.cshtml
  • src/Yavsc.Org/Views/Client/EditProperties.cshtml
  • src/Yavsc.Org/Views/Client/EditSecrets.cshtml
  • src/Yavsc.Org/Views/Client/_EditableStringList.cshtml — partial consumed by the single-string-field collection pages.

Files modified:

  • src/Yavsc.Org/Controllers/Administration/ClientController.csclasspartial class; the Edit(int id) GET now uses LoadClientAsync to 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.

  1. Edit.cshtml:249PairwiseSubjectSalt doesn't exist on IdentityServer8.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.
  2. Edit.cshtml:221CibaLifetime doesn't exist on Client. Fix: same as above. CIBA flow may be configured elsewhere (resource-level) or via a different property.
  3. ClientController.Collections.cs lines 181, 217, 253, 304 — Localizer is not available in the partial class. Fix: inject IStringLocalizer<ClientController> via the constructor, or inline the strings ("BothTypeAndValueRequired", "KeyRequired", "ValueRequired", "SecretValueRequired").
  4. EditSecrets.cshtml:44s.Expiration?.ToString("u") on a DateTime?. Fix: just s.Expiration?.ToString("u") works if you write s.Expiration.Value.ToString("u"), or use (s.Expiration is null ? "" : s.Expiration.Value.ToString("u")), or s.Expiration?.ToString("u") ?? string.Empty.

Suggested next session

Once the 4 compile errors are fixed and the pages render:

  1. Smoke test by logging in as admin, hitting /Client/Edit/1, then each Edit*/1 page, and verifying add/remove POSTs.
  2. Add a confirmation prompt (or 2-step form) for Remove actions — removing a Redirect URI is destructive and one click is too easy.
  3. Wire up some collection-level validation (e.g. redirect URI must be a valid URL) at the controller level.
  4. Add tests — the project doesn't have MVC test infrastructure today; consider adding a Yavsc.Org.Tests project that drives the controller via WebApplicationFactory<Program>.