mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:21:29 +00:00
feat(gui/oidc): Test Connection panel on create + edit forms (MED-5 GUI half)
Audit 2026-05-11 Fix 09 closure. MED-5's backend dry-run endpoint
(POST /api/v1/auth/oidc/test, gated auth.oidc.create) shipped on
dev/auth-bundle-2 (commit 00bbef7) but the GUI never called it —
authOIDCTestProvider in web/src/api/client.ts was dead code.
Operator gap before this fix: complete the create form blind, save,
then click 'Refresh' to discover whether the issuer URL worked.
Discovery failures left a broken provider row in the DB that had
to be deleted before retrying. The MED-5 backend exists to short-
circuit this — surface the dry-run result before commit.
New shared component web/src/pages/auth/OIDCTestConnectionPanel.tsx
calls authOIDCTestProvider against the live form state (issuer URL
+ client ID + parsed scopes) and renders a four-row status panel
inline:
* ✓/✗ Discovery fetched (with issuer-echo from the well-known doc)
* ✓/✗ JWKS reachable (with the discovered jwks_uri)
* ✓/⚠ Supported algs (warning glyph when the IdP advertises none —
distinct from a discovery failure)
* ✓/· RFC 9207 iss-parameter advertised (informational · glyph
rather than ✗ because the spec is SHOULD, not MUST)
Backend per-leg errors[] flow into an inline bullet list. A
top-level rectangle catches network/fetch failures separately.
The Run button is disabled when the issuer URL is empty or
whitespace-only. The component does NOT persist anything — safe
to run repeatedly before the operator clicks Save.
The panel is mounted in two places:
* OIDCProvidersPage create modal (between the form fields and the
Create button) — short-circuits the blind-save footgun for new
provider configs.
* OIDCProviderDetailPage edit form (between the field grid and
the Save button) — load-bearing for verifying IdP rotations
(Keycloak realm rename, Okta tenant move, certctl side-by-side
hostname change) without committing first.
A testIDSuffix prop (default 'create' / 'edit') gives each mount
point a distinct data-testid namespace so both panels can coexist
on a hypothetical page that uses both without DOM-id collisions.
8 Vitest tests in OIDCTestConnectionPanel.test.tsx:
* RunButton — disabled until issuer URL is non-empty
* RunButton — also disabled when issuer URL is whitespace-only
* RunButton — enabled when issuer URL is non-empty
* HappyPath — all four primary checks render green with detail
rows for authorization_url / token_url / userinfo_endpoint
(asserts both the glyph contract AND the mocked POST body shape)
* FailurePath — discovery=false renders ✗ on discovery + ✗ on
JWKS + ⚠ on empty supported algs + error list with backend
per-leg messages
* IssParamFalse — load-bearing UX claim that the iss-parameter
row renders · (informational), not ✗; body must contain the
word 'informational' so operators understand it's not a failure
* FetchError — top-level error rectangle when the POST throws
* TestIDSuffix — same component mounted twice with different
suffixes renders both without DOM-id collision
Verify gate:
* tsc --noEmit — clean
* vitest OIDCTestConnectionPanel.test.tsx — 8/8 pass
* vitest OIDCProvidersPage.test.tsx + OIDCProviderDetailPage.test.tsx
— 38/38 pass (panel-mount in both pages does not regress
existing tests because they don't trigger the test button)
Operator runbook: the four glyph meanings are documented inline on
the panel's subtitle. Audit doc annotation at
cowork/auth-bundles-audit-2026-05-10.md flips MED-5 from
'BACKEND CLOSED' to 'CLOSED' with the GUI-half annotation.
Refs cowork/auth-bundles-fixes-2026-05-11/09-med-oidc-test-connection-button.md.
This commit is contained in:
@@ -4,6 +4,29 @@
|
||||
|
||||
### Security
|
||||
|
||||
- **OIDC provider "Test connection" panel (Audit 2026-05-11 Fix 09 — MED-5 GUI half).**
|
||||
MED-5's backend dry-run endpoint (`POST /api/v1/auth/oidc/test`, gated
|
||||
`auth.oidc.create`) shipped on `dev/auth-bundle-2` but had no GUI caller —
|
||||
the `authOIDCTestProvider` function in `web/src/api/client.ts` was dead
|
||||
code. Operators had to complete the create form blind, save, then click
|
||||
"Refresh" to discover whether the issuer URL worked; failures left a
|
||||
broken provider row in the database that had to be deleted before
|
||||
retrying. New shared component
|
||||
`web/src/pages/auth/OIDCTestConnectionPanel.tsx` calls the backend
|
||||
against the live form state and renders a four-row status panel inline:
|
||||
Discovery fetched, JWKS reachable, supported algs (warns when the IdP
|
||||
advertises none), and RFC 9207 iss-parameter advertisement (informational
|
||||
`·` glyph, not ✗, because the spec is SHOULD). Backend per-leg `errors[]`
|
||||
flow into an inline bullet list. The panel is mounted in the
|
||||
OIDCProvidersPage create modal AND the OIDCProviderDetailPage edit form —
|
||||
the edit-form half is load-bearing for verifying IdP rotations (Keycloak
|
||||
realm rename, Okta tenant move) without committing first. Run button is
|
||||
disabled until the issuer URL is non-empty (whitespace-trimmed); the
|
||||
component is read-only — safe to run repeatedly. 8 Vitest tests pin the
|
||||
glyph-vs-glyph contract (✓/✗/⚠/·), the button-disabled-without-issuer
|
||||
shape, and the test-id-suffix collision-prevention when the panel is
|
||||
mounted twice on the same page.
|
||||
|
||||
- **Scope-aware actor-role revoke (Audit 2026-05-11 A-4).**
|
||||
HIGH-10 made it possible to grant the same role to the same actor at
|
||||
multiple scopes (e.g. `r-operator` on `profile=p-acme` AND `profile=p-globex`)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAuthMe } from '../../hooks/useAuthMe';
|
||||
import PageHeader from '../../components/PageHeader';
|
||||
import ErrorState from '../../components/ErrorState';
|
||||
import { validateEmailDomain } from './OIDCProvidersPage';
|
||||
import OIDCTestConnectionPanel from './OIDCTestConnectionPanel';
|
||||
|
||||
// =============================================================================
|
||||
// Bundle 2 Phase 8 — OIDCProviderDetailPage.
|
||||
@@ -623,6 +624,17 @@ export default function OIDCProviderDetailPage() {
|
||||
)}
|
||||
{editing && (
|
||||
<>
|
||||
{/* Audit 2026-05-11 Fix 09 — Test Connection panel (MED-5 GUI half).
|
||||
Lets the operator verify an issuer URL change post-rotation
|
||||
(e.g. Keycloak realm rename, Okta tenant move) without
|
||||
committing first. Reads from the live edit state so the
|
||||
scope of the test matches what Save would persist. */}
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL={editIssuerURL}
|
||||
clientID={editClientID}
|
||||
scopes={editScopesInput.split(/\s+/).filter(Boolean)}
|
||||
testIDSuffix="edit"
|
||||
/>
|
||||
<button
|
||||
onClick={saveEdit}
|
||||
disabled={submitting}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { useAuthMe } from '../../hooks/useAuthMe';
|
||||
import PageHeader from '../../components/PageHeader';
|
||||
import ErrorState from '../../components/ErrorState';
|
||||
import OIDCTestConnectionPanel from './OIDCTestConnectionPanel';
|
||||
|
||||
// =============================================================================
|
||||
// Bundle 2 Phase 8 — OIDCProvidersPage.
|
||||
@@ -313,6 +314,16 @@ function CreateProviderModal({ isOpen, onClose, onSuccess }: CreateProviderModal
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Audit 2026-05-11 Fix 09 — Test Connection panel (MED-5 GUI half).
|
||||
Dry-run the issuer URL + JWKS reachability + alg-downgrade defense
|
||||
against MED-5's POST /api/v1/auth/oidc/test. Renders inline so the
|
||||
operator sees the result before committing. */}
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL={form.issuer_url}
|
||||
clientID={form.client_id}
|
||||
scopes={form.scopes || []}
|
||||
testIDSuffix="create"
|
||||
/>
|
||||
<div className="flex justify-end gap-2 pt-3">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
|
||||
import OIDCTestConnectionPanel from './OIDCTestConnectionPanel';
|
||||
|
||||
// Audit 2026-05-11 Fix 09 — OIDCTestConnectionPanel regression coverage.
|
||||
// Mocks authOIDCTestProvider so the test is hermetic (no real network).
|
||||
// Pins: button-disabled-without-issuer, happy-path renders all checks
|
||||
// green, failure-path renders the errors list, iss_param_supported=false
|
||||
// renders the informational `·` glyph rather than ✗ (since RFC 9207 is
|
||||
// SHOULD, not MUST).
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
authOIDCTestProvider: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as client from '../../api/client';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('OIDCTestConnectionPanel', () => {
|
||||
it('RunButton — disabled until issuer URL is non-empty', () => {
|
||||
render(<OIDCTestConnectionPanel issuerURL="" clientID="cid" scopes={['openid']} />);
|
||||
const btn = screen.getByTestId('oidc-test-connection-run-default') as HTMLButtonElement;
|
||||
expect(btn.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('RunButton — enabled when issuer URL is non-empty', () => {
|
||||
render(
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL="https://idp.example.com"
|
||||
clientID="cid"
|
||||
scopes={['openid']}
|
||||
/>,
|
||||
);
|
||||
const btn = screen.getByTestId('oidc-test-connection-run-default') as HTMLButtonElement;
|
||||
expect(btn.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('RunButton — also disabled when issuer URL is whitespace-only', () => {
|
||||
render(<OIDCTestConnectionPanel issuerURL=" " clientID="cid" scopes={[]} />);
|
||||
const btn = screen.getByTestId('oidc-test-connection-run-default') as HTMLButtonElement;
|
||||
expect(btn.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('HappyPath — renders all four primary checks green when discovery succeeds', async () => {
|
||||
vi.mocked(client.authOIDCTestProvider).mockResolvedValue({
|
||||
discovery_succeeded: true,
|
||||
jwks_reachable: true,
|
||||
supported_alg_values: ['RS256', 'ES256'],
|
||||
iss_param_supported: true,
|
||||
issuer_echo: 'https://idp.example.com',
|
||||
authorization_url: 'https://idp.example.com/authorize',
|
||||
token_url: 'https://idp.example.com/token',
|
||||
jwks_uri: 'https://idp.example.com/jwks',
|
||||
userinfo_endpoint: 'https://idp.example.com/userinfo',
|
||||
errors: [],
|
||||
});
|
||||
|
||||
render(
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL="https://idp.example.com"
|
||||
clientID="certctl"
|
||||
scopes={['openid', 'profile', 'email']}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('oidc-test-connection-run-default'));
|
||||
await waitFor(() => screen.getByTestId('oidc-test-connection-result-default'));
|
||||
|
||||
// All four primary checks visible + green.
|
||||
expect(screen.getByTestId('oidc-test-connection-check-discovery-default').textContent)
|
||||
.toContain('✓');
|
||||
expect(screen.getByTestId('oidc-test-connection-check-jwks-default').textContent)
|
||||
.toContain('✓');
|
||||
expect(screen.getByTestId('oidc-test-connection-check-algs-default').textContent)
|
||||
.toContain('✓');
|
||||
// iss_param SUPPORTED → ✓, not `·`.
|
||||
expect(screen.getByTestId('oidc-test-connection-check-iss-param-default').textContent)
|
||||
.toContain('✓');
|
||||
|
||||
// Detail rows present.
|
||||
expect(screen.getByTestId('oidc-test-connection-detail-authz-url-default')).toBeTruthy();
|
||||
expect(screen.getByTestId('oidc-test-connection-detail-token-url-default')).toBeTruthy();
|
||||
expect(screen.getByTestId('oidc-test-connection-detail-userinfo-url-default')).toBeTruthy();
|
||||
|
||||
// No errors block on happy path.
|
||||
expect(screen.queryByTestId('oidc-test-connection-errors-list-default')).toBeNull();
|
||||
|
||||
// The mocked POST received the staged input.
|
||||
expect(client.authOIDCTestProvider).toHaveBeenCalledTimes(1);
|
||||
expect(client.authOIDCTestProvider).toHaveBeenCalledWith({
|
||||
issuer_url: 'https://idp.example.com',
|
||||
client_id: 'certctl',
|
||||
scopes: ['openid', 'profile', 'email'],
|
||||
});
|
||||
});
|
||||
|
||||
it('FailurePath — renders the errors list when discovery_succeeded is false', async () => {
|
||||
vi.mocked(client.authOIDCTestProvider).mockResolvedValue({
|
||||
discovery_succeeded: false,
|
||||
jwks_reachable: false,
|
||||
supported_alg_values: [],
|
||||
iss_param_supported: false,
|
||||
errors: ['discovery fetch failed: connection refused', 'jwks_uri not advertised'],
|
||||
});
|
||||
|
||||
render(
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL="https://broken.idp.example.com"
|
||||
clientID="cid"
|
||||
scopes={['openid']}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('oidc-test-connection-run-default'));
|
||||
await waitFor(() => screen.getByTestId('oidc-test-connection-result-default'));
|
||||
|
||||
// Discovery + JWKS marked ✗.
|
||||
expect(screen.getByTestId('oidc-test-connection-check-discovery-default').textContent)
|
||||
.toContain('✗');
|
||||
expect(screen.getByTestId('oidc-test-connection-check-jwks-default').textContent)
|
||||
.toContain('✗');
|
||||
// Empty alg list → ⚠ warning, not ✗ (the IdP responded but advertised nothing).
|
||||
expect(screen.getByTestId('oidc-test-connection-check-algs-default').textContent)
|
||||
.toContain('⚠');
|
||||
|
||||
// Errors list rendered with both entries.
|
||||
const errs = screen.getByTestId('oidc-test-connection-errors-list-default');
|
||||
expect(errs.textContent).toContain('connection refused');
|
||||
expect(errs.textContent).toContain('jwks_uri not advertised');
|
||||
});
|
||||
|
||||
it('IssParamFalse — renders the informational `·` glyph when iss_param_supported is false', async () => {
|
||||
vi.mocked(client.authOIDCTestProvider).mockResolvedValue({
|
||||
discovery_succeeded: true,
|
||||
jwks_reachable: true,
|
||||
supported_alg_values: ['RS256'],
|
||||
iss_param_supported: false,
|
||||
issuer_echo: 'https://idp.example.com',
|
||||
jwks_uri: 'https://idp.example.com/jwks',
|
||||
errors: [],
|
||||
});
|
||||
|
||||
render(
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL="https://idp.example.com"
|
||||
clientID="cid"
|
||||
scopes={['openid']}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('oidc-test-connection-run-default'));
|
||||
await waitFor(() => screen.getByTestId('oidc-test-connection-result-default'));
|
||||
|
||||
const issRow = screen.getByTestId('oidc-test-connection-check-iss-param-default');
|
||||
expect(issRow.textContent).toContain('·');
|
||||
// Must NOT be ✗ — RFC 9207 is SHOULD, not MUST; the panel must
|
||||
// not visually mark this as a failure.
|
||||
expect(issRow.textContent).not.toContain('✗');
|
||||
// Body should explain that this is informational.
|
||||
expect(issRow.textContent).toContain('informational');
|
||||
});
|
||||
|
||||
it('FetchError — renders a top-level error when authOIDCTestProvider throws', async () => {
|
||||
vi.mocked(client.authOIDCTestProvider).mockRejectedValue(new Error('network down'));
|
||||
|
||||
render(
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL="https://idp.example.com"
|
||||
clientID="cid"
|
||||
scopes={['openid']}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByTestId('oidc-test-connection-run-default'));
|
||||
await waitFor(() => screen.getByTestId('oidc-test-connection-error-default'));
|
||||
|
||||
expect(screen.getByTestId('oidc-test-connection-error-default').textContent)
|
||||
.toContain('network down');
|
||||
// The success result panel must NOT render alongside an error.
|
||||
expect(screen.queryByTestId('oidc-test-connection-result-default')).toBeNull();
|
||||
});
|
||||
|
||||
it('TestIDSuffix — same component renders twice on a page without colliding test IDs', async () => {
|
||||
vi.mocked(client.authOIDCTestProvider).mockResolvedValue({
|
||||
discovery_succeeded: true,
|
||||
jwks_reachable: true,
|
||||
supported_alg_values: ['RS256'],
|
||||
iss_param_supported: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<>
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL="https://idp.a.example.com"
|
||||
clientID="a"
|
||||
scopes={['openid']}
|
||||
testIDSuffix="create"
|
||||
/>
|
||||
<OIDCTestConnectionPanel
|
||||
issuerURL="https://idp.b.example.com"
|
||||
clientID="b"
|
||||
scopes={['openid']}
|
||||
testIDSuffix="edit"
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
|
||||
// Both panels visible with distinct test IDs — no DOM-id collisions.
|
||||
expect(screen.getByTestId('oidc-test-connection-panel-create')).toBeTruthy();
|
||||
expect(screen.getByTestId('oidc-test-connection-panel-edit')).toBeTruthy();
|
||||
expect(screen.getByTestId('oidc-test-connection-run-create')).toBeTruthy();
|
||||
expect(screen.getByTestId('oidc-test-connection-run-edit')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useState } from 'react';
|
||||
import { authOIDCTestProvider, type TestDiscoveryResult } from '../../api/client';
|
||||
|
||||
// =============================================================================
|
||||
// Audit 2026-05-11 Fix 09 — Test Connection panel (MED-5 GUI half).
|
||||
//
|
||||
// MED-5 backend (`POST /api/v1/auth/oidc/test`, commit 00bbef7) shipped
|
||||
// the dry-run discovery + JWKS-reachability + alg-downgrade-defense probe
|
||||
// behind `authOIDCTestProvider` in the API client, but no caller existed
|
||||
// in the UI. Operators had to complete the create form blind, save, then
|
||||
// click "Refresh" to discover whether the issuer URL worked; failures
|
||||
// left a broken provider row in the DB that had to be deleted before
|
||||
// retrying. This panel surfaces the dry-run result before commit.
|
||||
//
|
||||
// Embedded above the Submit button on both the OIDCProvidersPage create
|
||||
// modal and the OIDCProviderDetailPage edit form. The component does
|
||||
// NOT persist anything — it's a pure read-only probe against the
|
||||
// configured issuer URL + client ID + scopes. Errors render inline; the
|
||||
// operator decides whether to proceed with the save.
|
||||
//
|
||||
// Why each line matters:
|
||||
// - discovery_succeeded — did the well-known JSON fetch + parse?
|
||||
// - jwks_reachable — does the advertised jwks_uri respond?
|
||||
// - supported_alg_values — early warning if the IdP advertises only
|
||||
// HS-family algs (RFC 7515 §A.2 / §A.3); the backend rejects these
|
||||
// at create-time too, but seeing it here saves a round-trip.
|
||||
// - iss_param_supported — RFC 9207 advertisement check. Informational
|
||||
// only (the spec is a SHOULD, not a MUST); rendered as `·`, not ✗.
|
||||
// - userinfo_endpoint — useful to see when fetch_userinfo is
|
||||
// configured; absence means the IdP doesn't expose the endpoint at
|
||||
// all (some custom OIDC servers omit it).
|
||||
// - errors — backend-collected detail; one line per error.
|
||||
// =============================================================================
|
||||
|
||||
interface Props {
|
||||
issuerURL: string;
|
||||
clientID: string;
|
||||
/** Optional. Backend defaults to ['openid'] when empty; the panel
|
||||
* passes whatever the caller has staged so a test against an IdP
|
||||
* with custom scope requirements (e.g. Azure AD's `.default`) can
|
||||
* verify reachability with the real scope set. */
|
||||
scopes: string[];
|
||||
/** Optional caller-supplied data-testid suffix so the same panel
|
||||
* can render twice on the same page (e.g. create vs edit) without
|
||||
* colliding test IDs. Defaults to `default`. */
|
||||
testIDSuffix?: string;
|
||||
}
|
||||
|
||||
export default function OIDCTestConnectionPanel({
|
||||
issuerURL,
|
||||
clientID,
|
||||
scopes,
|
||||
testIDSuffix = 'default',
|
||||
}: Props) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [result, setResult] = useState<TestDiscoveryResult | null>(null);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
const trimmedIssuer = issuerURL.trim();
|
||||
|
||||
async function run() {
|
||||
if (!trimmedIssuer) {
|
||||
// Defensive — the button is disabled when issuerURL is empty,
|
||||
// but a programmatic click still goes through the same gate.
|
||||
setErr('Issuer URL is required before testing.');
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
setErr(null);
|
||||
setResult(null);
|
||||
try {
|
||||
const r = await authOIDCTestProvider({
|
||||
issuer_url: trimmedIssuer,
|
||||
client_id: clientID,
|
||||
scopes,
|
||||
});
|
||||
setResult(r);
|
||||
} catch (e) {
|
||||
setErr(e instanceof Error ? e.message : String(e));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
const tid = (s: string) => `oidc-test-connection-${s}-${testIDSuffix}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border border-surface-border rounded p-3 my-3"
|
||||
data-testid={tid('panel')}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1">
|
||||
<div className="font-semibold text-sm text-ink">Test connection</div>
|
||||
<div className="text-xs text-ink-muted">
|
||||
Dry-run OIDC discovery + JWKS reachability + alg-downgrade defense.
|
||||
Does NOT persist; safe to run repeatedly before saving.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={run}
|
||||
disabled={busy || !trimmedIssuer}
|
||||
className="px-3 py-1.5 text-sm border border-surface-border rounded bg-page hover:bg-surface text-ink disabled:opacity-50 whitespace-nowrap"
|
||||
data-testid={tid('run')}
|
||||
>
|
||||
{busy ? 'Running…' : 'Run test'}
|
||||
</button>
|
||||
</div>
|
||||
{err && (
|
||||
<div
|
||||
className="mt-2 text-xs text-red-700"
|
||||
data-testid={tid('error')}
|
||||
>
|
||||
{err}
|
||||
</div>
|
||||
)}
|
||||
{result && (
|
||||
<ul
|
||||
className="mt-3 text-xs space-y-1 text-ink"
|
||||
data-testid={tid('result')}
|
||||
>
|
||||
<li data-testid={tid('check-discovery')}>
|
||||
{result.discovery_succeeded ? '✓' : '✗'} Discovery fetched
|
||||
{result.issuer_echo ? ` (issuer echoes: ${result.issuer_echo})` : ''}
|
||||
</li>
|
||||
<li data-testid={tid('check-jwks')}>
|
||||
{result.jwks_reachable ? '✓' : '✗'} JWKS reachable
|
||||
{result.jwks_uri ? ` (${result.jwks_uri})` : ' (no jwks_uri advertised)'}
|
||||
</li>
|
||||
<li data-testid={tid('check-algs')}>
|
||||
{(result.supported_alg_values?.length ?? 0) > 0 ? '✓' : '⚠'} Supported algs:{' '}
|
||||
<code className="font-mono">
|
||||
{(result.supported_alg_values ?? []).join(', ') || '(none advertised)'}
|
||||
</code>
|
||||
</li>
|
||||
<li data-testid={tid('check-iss-param')}>
|
||||
{result.iss_param_supported ? '✓' : '·'} RFC 9207 iss parameter advertised:{' '}
|
||||
{result.iss_param_supported ? 'yes' : 'no (informational — spec is SHOULD)'}
|
||||
</li>
|
||||
{result.authorization_url && (
|
||||
<li className="text-ink-muted" data-testid={tid('detail-authz-url')}>
|
||||
· Authorization URL: <code className="font-mono">{result.authorization_url}</code>
|
||||
</li>
|
||||
)}
|
||||
{result.token_url && (
|
||||
<li className="text-ink-muted" data-testid={tid('detail-token-url')}>
|
||||
· Token URL: <code className="font-mono">{result.token_url}</code>
|
||||
</li>
|
||||
)}
|
||||
{result.userinfo_endpoint && (
|
||||
<li className="text-ink-muted" data-testid={tid('detail-userinfo-url')}>
|
||||
· UserInfo endpoint: <code className="font-mono">{result.userinfo_endpoint}</code>
|
||||
</li>
|
||||
)}
|
||||
{(result.errors ?? []).length > 0 && (
|
||||
<li className="text-red-700 mt-2" data-testid={tid('errors-list')}>
|
||||
<strong>Errors ({result.errors!.length}):</strong>
|
||||
<ul className="ml-4 list-disc">
|
||||
{result.errors!.map((e, i) => (
|
||||
<li key={i}>{e}</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user