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).
This commit is contained in:
parent
d62e59ba30
commit
ecac359344
14 changed files with 1255 additions and 31 deletions
209
doc/dev-tracking/client-editor-overhaul.md
Normal file
209
doc/dev-tracking/client-editor-overhaul.md
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
# 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):
|
||||||
|
|
||||||
|
- `AllowedGrantTypes` → `ClientGrantType` (GrantType)
|
||||||
|
- `AllowedScopes` → `ClientScope` (Scope)
|
||||||
|
- `RedirectUris` → `ClientRedirectUri` (RedirectUri) — exposed but broken
|
||||||
|
- `PostLogoutRedirectUris` → `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 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.cs`
|
||||||
|
— `class` → `partial 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:249` — `PairwiseSubjectSalt` 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:221` — `CibaLifetime` 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:44` — `s.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>`.
|
||||||
|
|
||||||
|
|
@ -0,0 +1,347 @@
|
||||||
|
using IdentityServer8.EntityFramework.Entities;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Localization;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Yavsc.Server.Helpers;
|
||||||
|
|
||||||
|
namespace Yavsc.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Partial class that adds the per-collection edit pages for an OAuth2
|
||||||
|
/// client. See <c>ClientController.cs</c> for the scalar edit flow and
|
||||||
|
/// the seed/secret management. The collection pages are deliberately
|
||||||
|
/// factored into a separate file so the controller stays navigable.
|
||||||
|
///
|
||||||
|
/// Each collection has three actions:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><c>GET Edit{Collection}(int id)</c> — render the page</item>
|
||||||
|
/// <item><c>POST Add{Collection}(int id, …)</c> — append a row</item>
|
||||||
|
/// <item><c>POST Remove{Collection}(int id, int rowId)</c> — delete a row</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
[Authorize("AdministratorOnly")]
|
||||||
|
public partial class ClientController
|
||||||
|
{
|
||||||
|
// ---- Redirect URIs ------------------------------------------------
|
||||||
|
|
||||||
|
readonly IHtmlLocalizer _localizer;
|
||||||
|
|
||||||
|
public ClientController(
|
||||||
|
IHtmlLocalizer<ClientController> localizer
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_localizer = localizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditRedirectUris(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.RedirectUris.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddRedirectUri(int id, string redirectUri)
|
||||||
|
{
|
||||||
|
return await AddCollectionRowAsync<ClientRedirectUri>(
|
||||||
|
id, redirectUri,
|
||||||
|
(client, uri) => new ClientRedirectUri { ClientId = client.Id, RedirectUri = uri });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveRedirectUri(int id, int rowId)
|
||||||
|
=> await RemoveCollectionRowAsync<ClientRedirectUri>(id, rowId, "EditRedirectUris");
|
||||||
|
|
||||||
|
// ---- Post-logout Redirect URIs -----------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditPostLogoutRedirectUris(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.PostLogoutRedirectUris.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddPostLogoutRedirectUri(int id, string postLogoutRedirectUri)
|
||||||
|
{
|
||||||
|
return await AddCollectionRowAsync<ClientPostLogoutRedirectUri>(
|
||||||
|
id, postLogoutRedirectUri,
|
||||||
|
(client, uri) => new ClientPostLogoutRedirectUri { ClientId = client.Id, PostLogoutRedirectUri = uri });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemovePostLogoutRedirectUri(int id, int rowId)
|
||||||
|
=> await RemoveCollectionRowAsync<ClientPostLogoutRedirectUri>(id, rowId, "EditPostLogoutRedirectUris");
|
||||||
|
|
||||||
|
// ---- Allowed Scopes ----------------------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditScopes(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.AllowedScopes.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddScope(int id, string scope)
|
||||||
|
{
|
||||||
|
return await AddCollectionRowAsync<ClientScope>(
|
||||||
|
id, scope,
|
||||||
|
(client, s) => new ClientScope { ClientId = client.Id, Scope = s });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveScope(int id, int rowId)
|
||||||
|
=> await RemoveCollectionRowAsync<ClientScope>(id, rowId, "EditScopes");
|
||||||
|
|
||||||
|
// ---- Allowed Grant Types -----------------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditGrantTypes(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.AllowedGrantTypes.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddGrantType(int id, string grantType)
|
||||||
|
{
|
||||||
|
return await AddCollectionRowAsync<ClientGrantType>(
|
||||||
|
id, grantType,
|
||||||
|
(client, g) => new ClientGrantType { ClientId = client.Id, GrantType = g });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveGrantType(int id, int rowId)
|
||||||
|
=> await RemoveCollectionRowAsync<ClientGrantType>(id, rowId, "EditGrantTypes");
|
||||||
|
|
||||||
|
// ---- Allowed CORS Origins ----------------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditCorsOrigins(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.AllowedCorsOrigins.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddCorsOrigin(int id, string origin)
|
||||||
|
{
|
||||||
|
return await AddCollectionRowAsync<ClientCorsOrigin>(
|
||||||
|
id, origin,
|
||||||
|
(client, o) => new ClientCorsOrigin { ClientId = client.Id, Origin = o });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveCorsOrigin(int id, int rowId)
|
||||||
|
=> await RemoveCollectionRowAsync<ClientCorsOrigin>(id, rowId, "EditCorsOrigins");
|
||||||
|
|
||||||
|
// ---- IdentityProvider Restrictions -------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditIdPRestrictions(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.IdentityProviderRestrictions.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddIdPRestriction(int id, string provider)
|
||||||
|
{
|
||||||
|
return await AddCollectionRowAsync<ClientIdPRestriction>(
|
||||||
|
id, provider,
|
||||||
|
(client, p) => new ClientIdPRestriction { ClientId = client.Id, Provider = p });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveIdPRestriction(int id, int rowId)
|
||||||
|
=> await RemoveCollectionRowAsync<ClientIdPRestriction>(id, rowId, "EditIdPRestrictions");
|
||||||
|
|
||||||
|
// ---- Claims ------------------------------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditClaims(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.Claims.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddClaim(int id, string type, string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(type) || string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
TempData["Error"] = _localizer["BothTypeAndValueRequired"].Value;
|
||||||
|
return RedirectToAction("EditClaims", new { id });
|
||||||
|
}
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
dbContext.Set<ClientClaim>().Add(new ClientClaim { ClientId = client.Id, Type = type, Value = value });
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("EditClaims", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveClaim(int id, int rowId)
|
||||||
|
{
|
||||||
|
var row = await dbContext.Set<ClientClaim>().FindAsync(rowId);
|
||||||
|
if (row is null || row.ClientId != id) return NotFound();
|
||||||
|
dbContext.Set<ClientClaim>().Remove(row);
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("EditClaims", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Properties (key/value) --------------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditProperties(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.Properties.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddProperty(int id, string key, string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key))
|
||||||
|
{
|
||||||
|
TempData["Error"] = _localizer["KeyRequired"].Value;
|
||||||
|
return RedirectToAction("EditProperties", new { id });
|
||||||
|
}
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
dbContext.Set<ClientProperty>().Add(new ClientProperty { ClientId = client.Id, Key = key, Value = value });
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("EditProperties", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveProperty(int id, int rowId)
|
||||||
|
{
|
||||||
|
var row = await dbContext.Set<ClientProperty>().FindAsync(rowId);
|
||||||
|
if (row is null || row.ClientId != id) return NotFound();
|
||||||
|
dbContext.Set<ClientProperty>().Remove(row);
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("EditProperties", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Secrets -----------------------------------------------------
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> EditSecrets(int id)
|
||||||
|
{
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
SetAppTypesInputValues();
|
||||||
|
return View(client.ClientSecrets?.ToList() ?? new List<ClientSecret>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> AddSecret(int id, string value, string description, DateTime? expiration)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
TempData["Error"] = _localizer["SecretValueRequired"].Value;
|
||||||
|
return RedirectToAction("EditSecrets", new { id });
|
||||||
|
}
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
dbContext.ClientSecrets.Add(new ClientSecret
|
||||||
|
{
|
||||||
|
ClientId = client.Id,
|
||||||
|
Type = "SharedSecret",
|
||||||
|
Value = value,
|
||||||
|
Description = description,
|
||||||
|
Created = DateTime.UtcNow,
|
||||||
|
Expiration = expiration
|
||||||
|
});
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("EditSecrets", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
|
public async Task<IActionResult> RemoveSecret(int id, int rowId)
|
||||||
|
{
|
||||||
|
var row = await dbContext.ClientSecrets.FindAsync(rowId);
|
||||||
|
if (row is null || row.ClientId != id) return NotFound();
|
||||||
|
dbContext.ClientSecrets.Remove(row);
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction("EditSecrets", new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Helpers -----------------------------------------------------
|
||||||
|
|
||||||
|
private async Task<Client?> LoadClientAsync(int id)
|
||||||
|
=> await dbContext.Clients
|
||||||
|
.Include(c => c.RedirectUris)
|
||||||
|
.Include(c => c.PostLogoutRedirectUris)
|
||||||
|
.Include(c => c.AllowedScopes)
|
||||||
|
.Include(c => c.AllowedGrantTypes)
|
||||||
|
.Include(c => c.AllowedCorsOrigins)
|
||||||
|
.Include(c => c.IdentityProviderRestrictions)
|
||||||
|
.Include(c => c.Claims)
|
||||||
|
.Include(c => c.Properties)
|
||||||
|
.Include(c => c.ClientSecrets)
|
||||||
|
.SingleOrDefaultAsync(c => c.Id == id);
|
||||||
|
|
||||||
|
private async Task<IActionResult> AddCollectionRowAsync<TEntity>(
|
||||||
|
int id,
|
||||||
|
string value,
|
||||||
|
Func<Client, string, TEntity> factory)
|
||||||
|
where TEntity : class
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
TempData["Error"] = _localizer["ValueRequired"].Value;
|
||||||
|
return RedirectToAction(RedirectTargetFor<TEntity>(), new { id });
|
||||||
|
}
|
||||||
|
var client = await LoadClientAsync(id);
|
||||||
|
if (client is null) return NotFound();
|
||||||
|
dbContext.Add(factory(client, value));
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction(RedirectTargetFor<TEntity>(), new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> RemoveCollectionRowAsync<TEntity>(int id, int rowId, string redirectAction)
|
||||||
|
where TEntity : class
|
||||||
|
{
|
||||||
|
var row = await dbContext.FindAsync<TEntity>(rowId);
|
||||||
|
if (row is null) return NotFound();
|
||||||
|
// IdentityServer8 navigation properties are not always populated
|
||||||
|
// by FindAsync; rely on the FK check on the caller side.
|
||||||
|
var fk = (row as dynamic).ClientId as int?;
|
||||||
|
if (fk is null || fk != id) return NotFound();
|
||||||
|
dbContext.Remove(row);
|
||||||
|
await dbContext.SaveChangesAsync(User.GetUserId());
|
||||||
|
return RedirectToAction(redirectAction, new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RedirectTargetFor<TEntity>() => typeof(TEntity).Name switch
|
||||||
|
{
|
||||||
|
nameof(ClientRedirectUri) => nameof(EditRedirectUris),
|
||||||
|
nameof(ClientPostLogoutRedirectUri) => nameof(EditPostLogoutRedirectUris),
|
||||||
|
nameof(ClientScope) => nameof(EditScopes),
|
||||||
|
nameof(ClientGrantType) => nameof(EditGrantTypes),
|
||||||
|
nameof(ClientCorsOrigin) => nameof(EditCorsOrigins),
|
||||||
|
nameof(ClientIdPRestriction) => nameof(EditIdPRestrictions),
|
||||||
|
_ => "Edit",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ using Yavsc.Server.Helpers;
|
||||||
namespace Yavsc.Controllers
|
namespace Yavsc.Controllers
|
||||||
{
|
{
|
||||||
[Authorize("AdministratorOnly")]
|
[Authorize("AdministratorOnly")]
|
||||||
public class ClientController : Controller
|
public partial class ClientController : Controller
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext dbContext;
|
private readonly ApplicationDbContext dbContext;
|
||||||
private readonly ClientStore clientStore;
|
private readonly ClientStore clientStore;
|
||||||
|
|
@ -137,8 +137,8 @@ namespace Yavsc.Controllers
|
||||||
// GET: Client/Edit/5
|
// GET: Client/Edit/5
|
||||||
public async Task<IActionResult> Edit(int id)
|
public async Task<IActionResult> Edit(int id)
|
||||||
{
|
{
|
||||||
Client client = await dbContext.Clients.SingleOrDefaultAsync(m => m.Id == id);
|
Client? client = await LoadClientAsync(id);
|
||||||
if (client == null)
|
if (client is null)
|
||||||
{
|
{
|
||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,12 @@
|
||||||
|
|
||||||
<form asp-action="Edit">
|
<form asp-action="Edit">
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
<h4>Client</h4>
|
<h4>Client <code>@Model.ClientId</code></h4>
|
||||||
<hr />
|
<hr />
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
|
<input type="hidden" asp-for="Id" />
|
||||||
<input type="hidden" asp-for="ClientId" />
|
<input type="hidden" asp-for="ClientId" />
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-offset-2 col-md-10">
|
<div class="col-md-offset-2 col-md-10">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
|
|
@ -20,60 +22,285 @@
|
||||||
<label asp-for="ClientName" class="col-md-2 control-label"></label>
|
<label asp-for="ClientName" class="col-md-2 control-label"></label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input asp-for="ClientName" class="form-control" />
|
<input asp-for="ClientName" class="form-control" />
|
||||||
<span asp-validation-for="ClientName" class="text-danger" ></span>
|
<span asp-validation-for="ClientName" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="FrontChannelLogoutUri" class="col-md-2 control-label"></label>
|
<label asp-for="Description" class="col-md-2 control-label"></label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input asp-for="FrontChannelLogoutUri" class="form-control" />
|
<input asp-for="Description" class="form-control" />
|
||||||
<span asp-validation-for="FrontChannelLogoutUri" class="text-danger" ></span>
|
<span asp-validation-for="Description" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="RedirectUris" class="col-md-2 control-label"></label>
|
<label asp-for="ClientUri" class="col-md-2 control-label"></label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input asp-for="RedirectUris" class="form-control" />
|
<input asp-for="ClientUri" class="form-control" />
|
||||||
<span asp-validation-for="RedirectUris" class="text-danger" ></span>
|
<span asp-validation-for="ClientUri" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="IdentityTokenLifetime" class="col-md-2 control-label"></label>
|
<label asp-for="LogoUri" class="col-md-2 control-label"></label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input asp-for="IdentityTokenLifetime" class="form-control" />
|
<input asp-for="LogoUri" class="form-control" />
|
||||||
<span asp-validation-for="IdentityTokenLifetime" class="text-danger" ></span>
|
<span asp-validation-for="LogoUri" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="AbsoluteRefreshTokenLifetime" class="col-md-2 control-label"></label>
|
<label asp-for="ProtocolType" class="col-md-2 control-label"></label>
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<input asp-for="AbsoluteRefreshTokenLifetime" class="form-control" />
|
<input asp-for="ProtocolType" class="form-control" />
|
||||||
<span asp-validation-for="AbsoluteRefreshTokenLifetime" class="text-danger" ></span>
|
<span asp-validation-for="ProtocolType" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="ClientSecrets" class="col-md-2 control-label"></label>
|
<fieldset>
|
||||||
<div class="col-md-10">
|
<legend>Security</legend>
|
||||||
<input asp-for="ClientSecrets" class="form-control" />
|
<div class="form-group">
|
||||||
<span asp-validation-for="ClientSecrets" class="text-danger" ></span>
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="RequireConsent" />
|
||||||
|
<label asp-for="RequireConsent"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="RequirePkce" />
|
||||||
|
<label asp-for="RequirePkce"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="RequireRequestObject" />
|
||||||
|
<label asp-for="RequireRequestObject"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="RequireClientSecret" />
|
||||||
|
<label asp-for="RequireClientSecret"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="AllowPlainTextPkce" />
|
||||||
|
<label asp-for="AllowPlainTextPkce"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="AllowOfflineAccess" />
|
||||||
|
<label asp-for="AllowOfflineAccess"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="AllowRememberConsent" />
|
||||||
|
<label asp-for="AllowRememberConsent"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="EnableLocalLogin" />
|
||||||
|
<label asp-for="EnableLocalLogin"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="AlwaysIncludeUserClaimsInIdToken" />
|
||||||
|
<label asp-for="AlwaysIncludeUserClaimsInIdToken"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="AlwaysSendClientClaims" />
|
||||||
|
<label asp-for="AlwaysSendClientClaims"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="IncludeJwtId" />
|
||||||
|
<label asp-for="IncludeJwtId"></label>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="UpdateAccessTokenClaimsOnRefresh" />
|
||||||
|
<label asp-for="UpdateAccessTokenClaimsOnRefresh"></label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</fieldset>
|
||||||
<div class="form-group">
|
|
||||||
<label asp-for="AccessTokenType" class="col-md-2 control-label"></label>
|
<fieldset>
|
||||||
<div class="col-md-10">
|
<legend>Logout</legend>
|
||||||
@Html.DropDownList("AccessTokenType")
|
<div class="form-group">
|
||||||
<span asp-validation-for="AccessTokenType" class="text-danger" ></span>
|
<label asp-for="FrontChannelLogoutUri" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="FrontChannelLogoutUri" class="form-control" />
|
||||||
|
<span asp-validation-for="FrontChannelLogoutUri" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="FrontChannelLogoutSessionRequired" />
|
||||||
|
<label asp-for="FrontChannelLogoutSessionRequired"></label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="form-group">
|
||||||
|
<label asp-for="BackChannelLogoutUri" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="BackChannelLogoutUri" class="form-control" />
|
||||||
|
<span asp-validation-for="BackChannelLogoutUri" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="BackChannelLogoutSessionRequired" />
|
||||||
|
<label asp-for="BackChannelLogoutSessionRequired"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Tokens</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="AccessTokenType" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
@Html.DropDownList("AccessTokenType")
|
||||||
|
<span asp-validation-for="AccessTokenType" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="IdentityTokenLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="IdentityTokenLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="IdentityTokenLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="AccessTokenLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="AccessTokenLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="AccessTokenLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="AuthorizationCodeLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="AuthorizationCodeLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="AuthorizationCodeLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="AbsoluteRefreshTokenLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="AbsoluteRefreshTokenLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="AbsoluteRefreshTokenLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="SlidingRefreshTokenLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="SlidingRefreshTokenLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="SlidingRefreshTokenLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="RefreshTokenUsage" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="RefreshTokenUsage" class="form-control" />
|
||||||
|
<span asp-validation-for="RefreshTokenUsage" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="RefreshTokenExpiration" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="RefreshTokenExpiration" class="form-control" />
|
||||||
|
<span asp-validation-for="RefreshTokenExpiration" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ConsentLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="ConsentLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="ConsentLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="UserSsoLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="UserSsoLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="UserSsoLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Device / CIBA</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="DeviceCodeLifetime" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="DeviceCodeLifetime" class="form-control" />
|
||||||
|
<span asp-validation-for="DeviceCodeLifetime" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="UserCodeType" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="UserCodeType" class="form-control" />
|
||||||
|
<span asp-validation-for="UserCodeType" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Tokens (extra)</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="AllowedIdentityTokenSigningAlgorithms" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="AllowedIdentityTokenSigningAlgorithms" class="form-control" />
|
||||||
|
<span asp-validation-for="AllowedIdentityTokenSigningAlgorithms" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-offset-2 col-md-10 checkbox">
|
||||||
|
<input asp-for="AllowAccessTokensViaBrowser" />
|
||||||
|
<label asp-for="AllowAccessTokensViaBrowser"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="ClientClaimsPrefix" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="ClientClaimsPrefix" class="form-control" />
|
||||||
|
<span asp-validation-for="ClientClaimsPrefix" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="PairWiseSubjectSalt" class="col-md-2 control-label"></label>
|
||||||
|
<div class="col-md-10">
|
||||||
|
<input asp-for="PairWiseSubjectSalt" class="form-control" />
|
||||||
|
<span asp-validation-for="PairWiseSubjectSalt" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-md-offset-2 col-md-10">
|
<div class="col-md-offset-2 col-md-10">
|
||||||
<input type="submit" value="Save" class="btn btn-default" />
|
<input type="submit" value="Save" class="btn btn-primary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h3>Collections</h3>
|
||||||
|
<p class="text-muted">
|
||||||
|
The following pages edit collections of related rows for this client.
|
||||||
|
</p>
|
||||||
|
<div class="list-group">
|
||||||
|
<a asp-action="EditRedirectUris" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Redirect URIs <span class="badge">@Model.RedirectUris.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditPostLogoutRedirectUris" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Post-logout Redirect URIs <span class="badge">@Model.PostLogoutRedirectUris.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditScopes" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Allowed Scopes <span class="badge">@Model.AllowedScopes.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditGrantTypes" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Allowed Grant Types <span class="badge">@Model.AllowedGrantTypes.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditCorsOrigins" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Allowed CORS Origins <span class="badge">@Model.AllowedCorsOrigins.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditIdPRestrictions" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Identity Provider Restrictions <span class="badge">@Model.IdentityProviderRestrictions.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditClaims" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Client Claims <span class="badge">@Model.Claims.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditProperties" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Client Properties <span class="badge">@Model.Properties.Count</span>
|
||||||
|
</a>
|
||||||
|
<a asp-action="EditSecrets" asp-route-id="@Model.Id" class="list-group-item">
|
||||||
|
Client Secrets <span class="badge">@(Model.ClientSecrets?.Count ?? 0)</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a asp-action="Index">@Localizer["Back to List"]</a>
|
<a asp-action="Index">@Localizer["Back to List"]</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
64
src/Yavsc.Org/Views/Client/EditClaims.cshtml
Normal file
64
src/Yavsc.Org/Views/Client/EditClaims.cshtml
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientClaim>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Client Claims";
|
||||||
|
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Client Claims</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Claims issued by IdentityServer on this client's behalf. Use sparingly:
|
||||||
|
these claims are emitted on every token, regardless of the user's
|
||||||
|
identity. For user-derived claims, prefer the API resource scope.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (TempData["Error"] is string err)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@err</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Type</th><th>Value</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if (!Model.Any())
|
||||||
|
{
|
||||||
|
<tr><td colspan="3" class="text-muted">—</td></tr>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var c in Model)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@c.Type</td>
|
||||||
|
<td>@c.Value</td>
|
||||||
|
<td>
|
||||||
|
<form asp-action="RemoveClaim" method="post" class="form-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<input type="hidden" name="rowId" value="@c.Id" />
|
||||||
|
<button type="submit" class="btn btn-link btn-sm text-danger">Remove</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form asp-action="AddClaim" method="post" class="form-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<div class="form-group mr-2">
|
||||||
|
<input type="text" name="type" class="form-control" placeholder="role" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group mr-2">
|
||||||
|
<input type="text" name="value" class="form-control" placeholder="admin" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||||
|
</p>
|
||||||
26
src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml
Normal file
26
src/Yavsc.Org/Views/Client/EditCorsOrigins.cshtml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientCorsOrigin>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Allowed CORS Origins";
|
||||||
|
ViewData["addAction"] = "AddCorsOrigin";
|
||||||
|
ViewData["removeAction"] = "RemoveCorsOrigin";
|
||||||
|
ViewData["rowKey"] = "CORS Origin";
|
||||||
|
ViewData["valueField"] = "Origin";
|
||||||
|
ViewData["inputName"] = "origin";
|
||||||
|
ViewData["placeholder"] = "https://app.example.com";
|
||||||
|
ViewData["clientId"] = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Allowed CORS Origins</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Origins allowed to call the token and discovery endpoints from a
|
||||||
|
browser via CORS. Only required for JavaScript clients running in
|
||||||
|
the user's browser. Origin = scheme + host + port, no trailing
|
||||||
|
slash.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@await Html.PartialAsync("_EditableStringList", Model)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||||
|
</p>
|
||||||
27
src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml
Normal file
27
src/Yavsc.Org/Views/Client/EditGrantTypes.cshtml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientGrantType>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Allowed Grant Types";
|
||||||
|
ViewData["addAction"] = "AddGrantType";
|
||||||
|
ViewData["removeAction"] = "RemoveGrantType";
|
||||||
|
ViewData["rowKey"] = "Grant Type";
|
||||||
|
ViewData["valueField"] = "GrantType";
|
||||||
|
ViewData["inputName"] = "grantType";
|
||||||
|
ViewData["placeholder"] = "authorization_code";
|
||||||
|
ViewData["clientId"] = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Allowed Grant Types</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
OAuth2 grant types this client is allowed to use. Common values:
|
||||||
|
<code>authorization_code</code> (web/native apps with PKCE),
|
||||||
|
<code>client_credentials</code> (server-to-server),
|
||||||
|
<code>refresh_token</code> (used implicitly alongside the others),
|
||||||
|
<code>password</code> (legacy, avoid).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@await Html.PartialAsync("_EditableStringList", Model)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||||
|
</p>
|
||||||
24
src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml
Normal file
24
src/Yavsc.Org/Views/Client/EditIdPRestrictions.cshtml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientIdPRestriction>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Identity Provider Restrictions";
|
||||||
|
ViewData["addAction"] = "AddIdPRestriction";
|
||||||
|
ViewData["removeAction"] = "RemoveIdPRestriction";
|
||||||
|
ViewData["rowKey"] = "Provider";
|
||||||
|
ViewData["valueField"] = "Provider";
|
||||||
|
ViewData["inputName"] = "provider";
|
||||||
|
ViewData["placeholder"] = "Google";
|
||||||
|
ViewData["clientId"] = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Identity Provider Restrictions</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Optional allow-list of external identity providers this client may
|
||||||
|
use. Empty list = any configured IdP is allowed.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@await Html.PartialAsync("_EditableStringList", Model)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||||
|
</p>
|
||||||
25
src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml
Normal file
25
src/Yavsc.Org/Views/Client/EditPostLogoutRedirectUris.cshtml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientPostLogoutRedirectUri>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Post-logout Redirect URIs";
|
||||||
|
ViewData["addAction"] = "AddPostLogoutRedirectUri";
|
||||||
|
ViewData["removeAction"] = "RemovePostLogoutRedirectUri";
|
||||||
|
ViewData["rowKey"] = "Post-logout URI";
|
||||||
|
ViewData["valueField"] = "PostLogoutRedirectUri";
|
||||||
|
ViewData["inputName"] = "postLogoutRedirectUri";
|
||||||
|
ViewData["placeholder"] = "https://app.example.com/";
|
||||||
|
ViewData["clientId"] = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Post-logout Redirect URIs</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
URIs the authorization server will redirect the browser to after a
|
||||||
|
front-channel logout. Use these when the client participates in the
|
||||||
|
OIDC front-channel logout flow.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@await Html.PartialAsync("_EditableStringList", Model)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||||
|
</p>
|
||||||
65
src/Yavsc.Org/Views/Client/EditProperties.cshtml
Normal file
65
src/Yavsc.Org/Views/Client/EditProperties.cshtml
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientProperty>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Client Properties";
|
||||||
|
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Client Properties</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Free-form key/value bag attached to the client. IdentityServer does
|
||||||
|
not interpret these; they're surfaced via the introspection endpoint
|
||||||
|
and read by your custom code. Useful for tagging, ownership, feature
|
||||||
|
flags, etc.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (TempData["Error"] is string err)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@err</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Key</th><th>Value</th><th></th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if (!Model.Any())
|
||||||
|
{
|
||||||
|
<tr><td colspan="3" class="text-muted">—</td></tr>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var p in Model)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@p.Key</td>
|
||||||
|
<td>@p.Value</td>
|
||||||
|
<td>
|
||||||
|
<form asp-action="RemoveProperty" method="post" class="form-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<input type="hidden" name="rowId" value="@p.Id" />
|
||||||
|
<button type="submit" class="btn btn-link btn-sm text-danger">Remove</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form asp-action="AddProperty" method="post" class="form-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<div class="form-group mr-2">
|
||||||
|
<input type="text" name="key" class="form-control" placeholder="owner-team" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group mr-2">
|
||||||
|
<input type="text" name="value" class="form-control" placeholder="platform" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||||
|
</p>
|
||||||
25
src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml
Normal file
25
src/Yavsc.Org/Views/Client/EditRedirectUris.cshtml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientRedirectUri>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Redirect URIs";
|
||||||
|
ViewData["addAction"] = "AddRedirectUri";
|
||||||
|
ViewData["removeAction"] = "RemoveRedirectUri";
|
||||||
|
ViewData["rowKey"] = "Redirect URI";
|
||||||
|
ViewData["valueField"] = "RedirectUri";
|
||||||
|
ViewData["inputName"] = "redirectUri";
|
||||||
|
ViewData["placeholder"] = "https://app.example.com/callback";
|
||||||
|
ViewData["clientId"] = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Redirect URIs</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
URIs the authorization server will redirect the browser to after a
|
||||||
|
successful login. Must match exactly the URL your client uses to
|
||||||
|
catch the response.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@await Html.PartialAsync("_EditableStringList", Model)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||||
|
</p>
|
||||||
25
src/Yavsc.Org/Views/Client/EditScopes.cshtml
Normal file
25
src/Yavsc.Org/Views/Client/EditScopes.cshtml
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientScope>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Allowed Scopes";
|
||||||
|
ViewData["addAction"] = "AddScope";
|
||||||
|
ViewData["removeAction"] = "RemoveScope";
|
||||||
|
ViewData["rowKey"] = "Scope";
|
||||||
|
ViewData["valueField"] = "Scope";
|
||||||
|
ViewData["inputName"] = "scope";
|
||||||
|
ViewData["placeholder"] = "openid";
|
||||||
|
ViewData["clientId"] = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Allowed Scopes</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
OAuth2 scopes this client is allowed to request. Each scope must be
|
||||||
|
defined in the IdentityServer resource store. <code>openid</code> is
|
||||||
|
mandatory for OIDC clients.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@await Html.PartialAsync("_EditableStringList", Model)
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@ViewData["clientId"]">Back to client</a>
|
||||||
|
</p>
|
||||||
83
src/Yavsc.Org/Views/Client/EditSecrets.cshtml
Normal file
83
src/Yavsc.Org/Views/Client/EditSecrets.cshtml
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
@model IEnumerable<IdentityServer8.EntityFramework.Entities.ClientSecret>
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Client Secrets";
|
||||||
|
var clientId = Model.FirstOrDefault()?.ClientId ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>Client Secrets</h2>
|
||||||
|
|
||||||
|
<p class="text-muted">
|
||||||
|
Shared secrets the client uses to authenticate to IdentityServer.
|
||||||
|
Required for confidential clients; leave empty for public clients
|
||||||
|
(rely on PKCE instead). Secrets are stored hashed — you only see
|
||||||
|
the value at creation time. Use <em>Regenerate</em> from the
|
||||||
|
Details page to issue a fresh one and view it once.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@if (TempData["Error"] is string err)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@err</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Created (UTC)</th>
|
||||||
|
<th>Expiration (UTC)</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if (!Model.Any())
|
||||||
|
{
|
||||||
|
<tr><td colspan="5" class="text-muted">— no secrets configured —</td></tr>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var s in Model)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>@s.Type</td>
|
||||||
|
<td>@s.Description</td>
|
||||||
|
<td>@s.Created.ToString("u")</td>
|
||||||
|
<td>@s.Expiration?.ToString("u")</td>
|
||||||
|
<td>
|
||||||
|
<form asp-action="RemoveSecret" method="post" class="form-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<input type="hidden" name="rowId" value="@s.Id" />
|
||||||
|
<button type="submit" class="btn btn-link btn-sm text-danger">Remove</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Add a new secret</h3>
|
||||||
|
<form asp-action="AddSecret" method="post" class="form">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Value</label>
|
||||||
|
<input type="text" name="value" class="form-control" />
|
||||||
|
<small class="text-muted">Hashed on save. Make sure you've copied it elsewhere first.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Description</label>
|
||||||
|
<input type="text" name="description" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Expiration (UTC, optional)</label>
|
||||||
|
<input type="datetime-local" name="expiration" class="form-control" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a asp-action="Edit" asp-route-id="@clientId">Back to client</a>
|
||||||
|
<a asp-action="RegenerateSecret" asp-route-id="@clientId" class="ml-3">Regenerate a single secret</a>
|
||||||
|
</p>
|
||||||
77
src/Yavsc.Org/Views/Client/_EditableStringList.cshtml
Normal file
77
src/Yavsc.Org/Views/Client/_EditableStringList.cshtml
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
@model IEnumerable<object>
|
||||||
|
@using System.Reflection
|
||||||
|
@*
|
||||||
|
Shared partial for client collection pages whose row type carries a
|
||||||
|
single string field plus an `Id` (used for Remove).
|
||||||
|
|
||||||
|
Inputs (via ViewData):
|
||||||
|
- addAction: name of the Add action (e.g. "AddRedirectUri")
|
||||||
|
- removeAction: name of the Remove action
|
||||||
|
- rowKey: display label for the column header
|
||||||
|
- valueField: name of the string property to display (e.g. "RedirectUri")
|
||||||
|
- inputName: name attribute on the Add form's text input
|
||||||
|
- clientId: passed through as a hidden field on both forms
|
||||||
|
- placeholder: optional placeholder text
|
||||||
|
|
||||||
|
Each row is rendered with an "X" button that POSTs to removeAction with
|
||||||
|
the row's `Id`. Add is a separate form with a single text input.
|
||||||
|
*@
|
||||||
|
|
||||||
|
@{
|
||||||
|
var addAction = ViewData["addAction"] as string ?? "Add";
|
||||||
|
var removeAction = ViewData["removeAction"] as string ?? "Remove";
|
||||||
|
var rowKey = ViewData["rowKey"] as string ?? "Value";
|
||||||
|
var valueField = ViewData["valueField"] as string ?? "Value";
|
||||||
|
var inputName = ViewData["inputName"] as string ?? "value";
|
||||||
|
var clientId = ViewData["clientId"];
|
||||||
|
var placeholder = ViewData["placeholder"] as string ?? string.Empty;
|
||||||
|
var idProp = typeof(object).GetProperty("Id");
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (TempData["Error"] is string err)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger">@err</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>@rowKey</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if (!Model.Any())
|
||||||
|
{
|
||||||
|
<tr><td colspan="2" class="text-muted">—</td></tr>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var row in Model)
|
||||||
|
{
|
||||||
|
var value = row.GetType().GetProperty(valueField)?.GetValue(row) as string ?? string.Empty;
|
||||||
|
var rowId = row.GetType().GetProperty("Id")?.GetValue(row);
|
||||||
|
<tr>
|
||||||
|
<td>@value</td>
|
||||||
|
<td>
|
||||||
|
<form asp-action="@removeAction" method="post" class="form-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<input type="hidden" name="rowId" value="@rowId" />
|
||||||
|
<button type="submit" class="btn btn-link btn-sm text-danger">Remove</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form asp-action="@addAction" method="post" class="form-inline">
|
||||||
|
@Html.AntiForgeryToken()
|
||||||
|
<input type="hidden" name="id" value="@clientId" />
|
||||||
|
<div class="form-group mr-2">
|
||||||
|
<input type="text" name="@inputName" class="form-control" placeholder="@placeholder" />
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
|
</form>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue