mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 14:11:31 +00:00
Merge Fix 09 (MED-5 GUI half): Test Connection panel on OIDC create + edit forms
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -39,6 +39,29 @@
|
|||||||
synthetic-admin fallback") is fully true. Operator runbook at
|
synthetic-admin fallback") is fully true. Operator runbook at
|
||||||
`docs/operator/security.md#demo-to-production-cutover-audit-2026-05-11-a-8`.
|
`docs/operator/security.md#demo-to-production-cutover-audit-2026-05-11-a-8`.
|
||||||
|
|
||||||
|
- **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).**
|
- **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
|
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`)
|
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 PageHeader from '../../components/PageHeader';
|
||||||
import ErrorState from '../../components/ErrorState';
|
import ErrorState from '../../components/ErrorState';
|
||||||
import { validateEmailDomain } from './OIDCProvidersPage';
|
import { validateEmailDomain } from './OIDCProvidersPage';
|
||||||
|
import OIDCTestConnectionPanel from './OIDCTestConnectionPanel';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Bundle 2 Phase 8 — OIDCProviderDetailPage.
|
// Bundle 2 Phase 8 — OIDCProviderDetailPage.
|
||||||
@@ -623,6 +624,17 @@ export default function OIDCProviderDetailPage() {
|
|||||||
)}
|
)}
|
||||||
{editing && (
|
{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
|
<button
|
||||||
onClick={saveEdit}
|
onClick={saveEdit}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { useAuthMe } from '../../hooks/useAuthMe';
|
import { useAuthMe } from '../../hooks/useAuthMe';
|
||||||
import PageHeader from '../../components/PageHeader';
|
import PageHeader from '../../components/PageHeader';
|
||||||
import ErrorState from '../../components/ErrorState';
|
import ErrorState from '../../components/ErrorState';
|
||||||
|
import OIDCTestConnectionPanel from './OIDCTestConnectionPanel';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Bundle 2 Phase 8 — OIDCProvidersPage.
|
// Bundle 2 Phase 8 — OIDCProvidersPage.
|
||||||
@@ -313,6 +314,16 @@ function CreateProviderModal({ isOpen, onClose, onSuccess }: CreateProviderModal
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<div className="flex justify-end gap-2 pt-3">
|
||||||
<button
|
<button
|
||||||
type="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