1
0
forked from baron/baron-sso
Files
baron-sso/devfront/tests/helpers/devfront-fixtures.ts
Lectom C Han 51f09bf53c fix(headless-login): show full parsed jwks key values
- return the full RSA n value in parsedKeys responses
- render parsed key fields with labels and multiline key material in DevFront
- lock the behavior with backend and Playwright regression tests
2026-04-01 18:51:39 +09:00

494 lines
16 KiB
TypeScript

import type { Page, Route } from "@playwright/test";
export type ClientStatus = "active" | "inactive";
export type ClientType = "private" | "pkce";
export type Client = {
id: string;
name: string;
type: ClientType;
status: ClientStatus;
redirectUris: string[];
scopes: string[];
createdAt: string;
clientSecret?: string;
tokenEndpointAuthMethod?: string;
jwksUri?: string;
jwks?: Record<string, unknown> | string;
headlessJwksCache?: {
clientId: string;
jwksUri: string;
cachedAt: string;
expiresAt: string;
lastCheckedAt?: string;
lastSuccessfulVerificationAt?: string;
lastRefreshStatus?: "success" | "failure" | "pending";
lastError?: string;
consecutiveFailures?: number;
cachedKids?: string[];
etag?: string;
lastModified?: string;
parsedKeys?: Array<{
kid?: string;
kty?: string;
use?: string;
alg?: string;
n?: string;
}>;
};
metadata?: Record<string, unknown>;
};
export type Consent = {
subject: string;
userName: string;
clientId: string;
clientName: string;
grantedScopes: string[];
authenticatedAt?: string;
createdAt: string;
deletedAt?: string;
status: "active" | "revoked";
tenantId: string;
tenantName: string;
};
export type AuditLog = {
event_id: string;
timestamp: string;
user_id: string;
event_type: string;
status: "success" | "failure";
ip_address: string;
user_agent: string;
details: string;
};
export type DevApiMockState = {
clients: Client[];
consents: Consent[];
auditLogsByCursor?: Record<
string,
{ items: AuditLog[]; next_cursor?: string }
>;
auditLogs?: AuditLog[];
onUpdateStatus?: (status: ClientStatus) => void;
onRotateSecret?: (newSecret: string) => void;
onRefreshHeadlessJwks?: (clientId: string) => void;
onRevokeHeadlessJwksCache?: (clientId: string) => void;
};
export function makeClient(
id: string,
overrides: Partial<Client> = {},
): Client {
return {
id,
name: `${id} app`,
type: "private",
status: "active",
redirectUris: [`https://${id}.example.com/callback`],
scopes: ["openid", "profile", "email"],
createdAt: "2026-03-03T00:00:00.000Z",
clientSecret: `${id}-secret`,
metadata: {},
...overrides,
};
}
export async function seedAuth(page: Page, role?: string) {
const nowInSeconds = Math.floor(Date.now() / 1000);
await page.addInitScript(
({ issuedAt, injectedRole }) => {
const mockOidcUser = {
id_token: "playwright-id-token",
session_state: "playwright-session",
access_token: "playwright-access-token",
refresh_token: "playwright-refresh-token",
token_type: "Bearer",
scope: "openid profile email",
profile: {
sub: "playwright-user",
email: "playwright@example.com",
name: "Playwright User",
...(injectedRole ? { role: injectedRole } : {}),
},
expires_at: issuedAt + 3600,
};
window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc:devfront",
JSON.stringify(mockOidcUser),
);
window.localStorage.setItem(
"oidc.user:http://localhost:5000/oidc/:devfront",
JSON.stringify(mockOidcUser),
);
window.localStorage.setItem("dev_tenant_id", "tenant-a");
},
{ issuedAt: nowInSeconds, injectedRole: role ?? "" },
);
await page.route("**/oidc/**", async (route) => {
const url = route.request().url();
if (url.includes(".well-known/openid-configuration")) {
await route.fulfill({
json: {
issuer: "http://localhost:5000/oidc",
authorization_endpoint: "http://localhost:5000/oidc/auth",
token_endpoint: "http://localhost:5000/oidc/token",
jwks_uri: "http://localhost:5000/oidc/jwks",
userinfo_endpoint: "http://localhost:5000/oidc/userinfo",
end_session_endpoint: "http://localhost:5000/oidc/session/end",
},
headers: { "Access-Control-Allow-Origin": "*" },
});
} else if (url.includes("/jwks")) {
await route.fulfill({
json: { keys: [] },
headers: { "Access-Control-Allow-Origin": "*" },
});
} else {
await route.fulfill({
status: 200,
body: "ok",
headers: { "Access-Control-Allow-Origin": "*" },
});
}
});
}
function json(route: Route, payload: unknown, status = 200) {
return route.fulfill({
status,
contentType: "application/json",
body: JSON.stringify(payload),
});
}
function parseClientId(pathname: string): string {
const parts = pathname.split("/").filter(Boolean);
return parts[parts.length - 1] ?? "";
}
export async function installDevApiMock(page: Page, state: DevApiMockState) {
const appendAuditLog = (
eventType: string,
action: string,
targetId: string,
status: "success" | "failure" = "success",
) => {
if (!state.auditLogs) return;
state.auditLogs.unshift({
event_id: `evt-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
timestamp: new Date().toISOString(),
user_id: "playwright-user",
event_type: eventType,
status,
ip_address: "127.0.0.1",
user_agent: "playwright",
details: JSON.stringify({
action,
target_id: targetId,
tenant_id: "tenant-a",
}),
});
};
await page.route("**/api/v1/dev/**", async (route) => {
const request = route.request();
const url = new URL(request.url());
const { pathname, searchParams } = url;
const method = request.method();
if (pathname === "/api/v1/dev/my-tenants" && method === "GET") {
return json(route, [
{ id: "tenant-a", name: "Tenant A", slug: "tenant-a" },
]);
}
if (pathname === "/api/v1/dev/stats" && method === "GET") {
const total = state.clients.length;
return json(route, {
total_clients: total,
active_sessions: Math.max(1, total),
auth_failures_24h: 0,
});
}
if (pathname === "/api/v1/dev/clients" && method === "GET") {
return json(route, {
items: state.clients.map((client) => ({
id: client.id,
name: client.name,
type: client.type,
status: client.status,
createdAt: client.createdAt,
redirectUris: client.redirectUris,
scopes: client.scopes,
})),
limit: 50,
offset: 0,
});
}
if (pathname === "/api/v1/dev/clients" && method === "POST") {
const payload = (request.postDataJSON() as {
name?: string;
type?: ClientType;
status?: ClientStatus;
redirectUris?: string[];
scopes?: string[];
tokenEndpointAuthMethod?: string;
jwksUri?: string;
jwks?: Record<string, unknown> | string;
metadata?: Record<string, unknown>;
}) || { name: "created app" };
const created = makeClient(`client-${state.clients.length + 1}`, {
name: payload.name ?? "created app",
type: payload.type ?? "private",
status: payload.status ?? "active",
redirectUris: payload.redirectUris ?? [],
scopes: payload.scopes ?? ["openid"],
tokenEndpointAuthMethod: payload.tokenEndpointAuthMethod,
jwksUri: payload.jwksUri,
jwks: payload.jwks,
metadata: payload.metadata ?? {},
});
state.clients.push(created);
appendAuditLog("CLIENT_CREATE", "CREATE_CLIENT", created.id);
return json(route, {
client: created,
endpoints: {
discovery: "https://issuer/.well-known/openid-configuration",
issuer: "https://issuer",
authorization: "https://issuer/oauth2/auth",
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
});
}
if (
pathname.startsWith("/api/v1/dev/clients/") &&
pathname.endsWith("/status") &&
method === "PATCH"
) {
const clientId = pathname.split("/")[5] ?? "";
const payload = request.postDataJSON() as { status: ClientStatus };
const found = state.clients.find((client) => client.id === clientId);
if (!found) return json(route, { error: "not found" }, 404);
found.status = payload.status;
appendAuditLog("CLIENT_UPDATE_STATUS", "UPDATE_CLIENT_STATUS", clientId);
state.onUpdateStatus?.(payload.status);
return json(route, {
client: found,
endpoints: {
discovery: "https://issuer/.well-known/openid-configuration",
issuer: "https://issuer",
authorization: "https://issuer/oauth2/auth",
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
});
}
if (
pathname.startsWith("/api/v1/dev/clients/") &&
pathname.endsWith("/secret/rotate") &&
method === "POST"
) {
const clientId = pathname.split("/")[5] ?? "";
const found = state.clients.find((client) => client.id === clientId);
if (!found) return json(route, { error: "not found" }, 404);
found.clientSecret = `${clientId}-rotated-secret`;
appendAuditLog("CLIENT_ROTATE_SECRET", "ROTATE_SECRET", clientId);
state.onRotateSecret?.(found.clientSecret);
return json(route, {
client: found,
endpoints: {
discovery: "https://issuer/.well-known/openid-configuration",
issuer: "https://issuer",
authorization: "https://issuer/oauth2/auth",
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
});
}
if (pathname.startsWith("/api/v1/dev/clients/") && method === "PUT") {
const clientId = parseClientId(pathname);
const payload = (request.postDataJSON() as {
name?: string;
type?: ClientType;
scopes?: string[];
redirectUris?: string[];
tokenEndpointAuthMethod?: string;
jwksUri?: string;
jwks?: Record<string, unknown> | string;
metadata?: Record<string, unknown>;
}) || { name: "updated app" };
const found = state.clients.find((client) => client.id === clientId);
if (!found) return json(route, { error: "not found" }, 404);
if (payload.name) found.name = payload.name;
if (payload.type) found.type = payload.type;
if (payload.scopes) found.scopes = payload.scopes;
if (payload.redirectUris) found.redirectUris = payload.redirectUris;
if (payload.tokenEndpointAuthMethod !== undefined) {
found.tokenEndpointAuthMethod = payload.tokenEndpointAuthMethod;
}
if (payload.jwksUri !== undefined) {
found.jwksUri = payload.jwksUri;
}
if (payload.jwks !== undefined) {
found.jwks = payload.jwks;
}
if (payload.metadata) found.metadata = payload.metadata;
appendAuditLog("CLIENT_UPDATE", "UPDATE_CLIENT", clientId);
return json(route, {
client: found,
endpoints: {
discovery: "https://issuer/.well-known/openid-configuration",
issuer: "https://issuer",
authorization: "https://issuer/oauth2/auth",
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
});
}
if (pathname.startsWith("/api/v1/dev/clients/") && method === "GET") {
const clientId = parseClientId(pathname);
const found = state.clients.find((client) => client.id === clientId);
if (!found) return json(route, { error: "forbidden" }, 403);
return json(route, {
client: found,
endpoints: {
discovery: "https://issuer/.well-known/openid-configuration",
issuer: "https://issuer",
authorization: "https://issuer/oauth2/auth",
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
headlessJwksCache: found.headlessJwksCache,
});
}
if (
pathname.startsWith("/api/v1/dev/clients/") &&
pathname.endsWith("/headless-jwks/cache") &&
method === "DELETE"
) {
const clientId = pathname.split("/")[5] ?? "";
const found = state.clients.find((client) => client.id === clientId);
if (!found) return json(route, { error: "not found" }, 404);
found.headlessJwksCache = undefined;
state.onRevokeHeadlessJwksCache?.(clientId);
return route.fulfill({ status: 204 });
}
if (
pathname.startsWith("/api/v1/dev/clients/") &&
pathname.endsWith("/headless-jwks/refresh") &&
method === "POST"
) {
const clientId = pathname.split("/")[5] ?? "";
const found = state.clients.find((client) => client.id === clientId);
if (!found) return json(route, { error: "not found" }, 404);
state.onRefreshHeadlessJwks?.(clientId);
return json(route, {
client: found,
endpoints: {
discovery: "https://issuer/.well-known/openid-configuration",
issuer: "https://issuer",
authorization: "https://issuer/oauth2/auth",
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
headlessJwksCache: found.headlessJwksCache,
});
}
if (
pathname.startsWith("/api/v1/dev/clients/") &&
!pathname.endsWith("/headless-jwks/cache") &&
method === "DELETE"
) {
const clientId = parseClientId(pathname);
state.clients = state.clients.filter((client) => client.id !== clientId);
appendAuditLog("CLIENT_DELETE", "DELETE_CLIENT", clientId);
return route.fulfill({ status: 204 });
}
if (pathname === "/api/v1/dev/consents" && method === "GET") {
const subject = searchParams.get("subject") || "";
const clientId = searchParams.get("client_id") || "";
const status = searchParams.get("status") || "";
const items = state.consents.filter((row) => {
const matchesSubject =
!subject ||
row.subject.includes(subject) ||
row.userName.includes(subject);
const matchesClientId = !clientId || row.clientId === clientId;
const matchesStatus = !status || row.status === status;
return matchesSubject && matchesClientId && matchesStatus;
});
return json(route, { items });
}
if (pathname === "/api/v1/dev/consents" && method === "DELETE") {
const subject = searchParams.get("subject") || "";
const clientId = searchParams.get("client_id") || "";
const target = state.consents.find(
(row) => row.subject === subject && row.clientId === clientId,
);
if (target) {
target.status = "revoked";
target.deletedAt = "2026-03-03T10:00:00.000Z";
}
return route.fulfill({ status: 204 });
}
if (pathname === "/api/v1/dev/audit-logs" && method === "GET") {
if (state.auditLogsByCursor) {
const cursor = searchParams.get("cursor") || "";
const pageSet = state.auditLogsByCursor[cursor] ?? { items: [] };
return json(route, {
items: pageSet.items,
limit: 50,
cursor: cursor || undefined,
next_cursor: pageSet.next_cursor,
});
}
if (state.auditLogs) {
const action = searchParams.get("action") || "";
const clientId = searchParams.get("client_id") || "";
const status = searchParams.get("status") || "";
const filtered = state.auditLogs.filter((item) => {
let parsedDetails: { action?: string; target_id?: string } = {};
try {
parsedDetails = JSON.parse(item.details) as {
action?: string;
target_id?: string;
};
} catch {}
const matchesAction = !action || parsedDetails.action === action;
const matchesClient =
!clientId || parsedDetails.target_id === clientId;
const matchesStatus = !status || item.status === status;
return matchesAction && matchesClient && matchesStatus;
});
return json(route, { items: filtered, limit: 50 });
}
return json(route, { items: [], limit: 50 });
}
return json(route, { error: `Unhandled ${method} ${pathname}` }, 404);
});
}