forked from baron/baron-sso
feat(headless-login): add jwks cache visibility and refresh flow
- replace inline headless jwks support with jwksUri-only validation - add cached jwks refresh worker, manual refresh/revoke endpoints, and parsed key summaries - expose allowed algorithms and key previews in DevFront with regression coverage
This commit is contained in:
@@ -8,8 +8,7 @@ import {
|
||||
} from "./helpers/devfront-fixtures";
|
||||
|
||||
const appNamePlaceholder = /My Awesome Application|예: 멋진 애플리케이션/i;
|
||||
const sshRsaPublicKey =
|
||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAABwECAwQFBgc= test@example";
|
||||
const jwksUri = "https://rp.example.com/.well-known/jwks.json";
|
||||
|
||||
test.describe("DevFront clients lifecycle", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
@@ -123,7 +122,7 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
).toHaveValue(/https:\/\/after\.example\.com\/callback/);
|
||||
});
|
||||
|
||||
test("pkce headless login with inline ssh-rsa key should persist mapped payload", async ({
|
||||
test("pkce headless login uses jwks uri only and shows cache actions", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state = {
|
||||
@@ -131,10 +130,44 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
makeClient("client-headless-login", {
|
||||
name: "Headless Login App",
|
||||
type: "pkce",
|
||||
headlessJwksCache: {
|
||||
clientId: "client-headless-login",
|
||||
jwksUri,
|
||||
cachedAt: "2026-03-31T00:00:00.000Z",
|
||||
expiresAt: "2026-04-01T00:00:00.000Z",
|
||||
lastCheckedAt: "2026-03-31T12:00:00.000Z",
|
||||
lastSuccessfulVerificationAt: "2026-03-31T12:00:00.000Z",
|
||||
lastRefreshStatus: "success",
|
||||
lastError: "",
|
||||
consecutiveFailures: 0,
|
||||
cachedKids: ["kid-1"],
|
||||
etag: 'W/"cache-etag"',
|
||||
lastModified: "Tue, 31 Mar 2026 00:00:00 GMT",
|
||||
parsedKeys: [
|
||||
{
|
||||
kid: "kid-1",
|
||||
kty: "RSA",
|
||||
use: "sig",
|
||||
alg: "RS256",
|
||||
n: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
consents: [] as Consent[],
|
||||
auditLogsByCursor: undefined,
|
||||
onRefreshHeadlessJwks(clientId: string) {
|
||||
this.clients[0].headlessJwksCache = {
|
||||
...this.clients[0].headlessJwksCache!,
|
||||
lastRefreshStatus: "success",
|
||||
lastCheckedAt: "2026-04-01T00:00:00.000Z",
|
||||
};
|
||||
expect(clientId).toBe("client-headless-login");
|
||||
},
|
||||
onRevokeHeadlessJwksCache(clientId: string) {
|
||||
expect(clientId).toBe("client-headless-login");
|
||||
},
|
||||
};
|
||||
await installDevApiMock(page, state);
|
||||
|
||||
@@ -147,16 +180,15 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", {
|
||||
name: /공개키 등록|Public Key Registration/i,
|
||||
}),
|
||||
page.getByRole("heading", { name: /공개키 등록|Public Key Registration/i }),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByRole("radio", { name: /Inline Public Key|Inline/i }),
|
||||
).toHaveCount(0);
|
||||
await page
|
||||
.getByPlaceholder(
|
||||
/ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i,
|
||||
)
|
||||
.fill(sshRsaPublicKey);
|
||||
.getByPlaceholder(/https:\/\/rp\.example\.com\/\.well-known\/jwks\.json/i)
|
||||
.fill(jwksUri);
|
||||
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
|
||||
|
||||
await expect
|
||||
@@ -171,25 +203,57 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
)
|
||||
.toBe("private_key_jwt");
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
(
|
||||
state.clients[0]?.metadata?.headless_jwks as {
|
||||
keys?: Array<{ kty?: string; alg?: string }>;
|
||||
}
|
||||
)?.keys?.[0]?.kty,
|
||||
)
|
||||
.toBe("RSA");
|
||||
.poll(() => state.clients[0]?.metadata?.headless_jwks_uri)
|
||||
.toBe(jwksUri);
|
||||
|
||||
await expect(
|
||||
page.getByText(/cached at|캐시됨|last refresh|마지막 갱신/i),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText(/Parsed Keys|파싱된 키/i)).toBeVisible();
|
||||
await expect(page.getByText("kid-1", { exact: true }).last()).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(/Allowed algorithms|허용 알고리즘/i),
|
||||
).toBeVisible();
|
||||
for (const algorithm of [
|
||||
"RS256",
|
||||
"RS384",
|
||||
"RS512",
|
||||
"PS256",
|
||||
"PS384",
|
||||
"PS512",
|
||||
"ES256",
|
||||
"ES384",
|
||||
"ES512",
|
||||
"EdDSA",
|
||||
]) {
|
||||
await expect(page.getByText(algorithm, { exact: true }).last()).toBeVisible();
|
||||
}
|
||||
await expect(
|
||||
page.getByText("abcdefghij...0123456789", { exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /refresh|새로고침/i }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: /^(캐시 삭제|Revoke Cache)$/i }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: /refresh|새로고침/i }).click();
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
(
|
||||
state.clients[0]?.metadata?.headless_jwks as {
|
||||
keys?: Array<{ kty?: string; alg?: string }>;
|
||||
}
|
||||
)?.keys?.[0]?.alg,
|
||||
)
|
||||
.toBe("RS256");
|
||||
.poll(() => state.clients[0]?.headlessJwksCache?.lastCheckedAt)
|
||||
.toBe("2026-04-01T00:00:00.000Z");
|
||||
|
||||
page.removeAllListeners("dialog");
|
||||
page.once("dialog", async (dialog) => {
|
||||
expect(dialog.message()).toMatch(/revoke|삭제|cache/i);
|
||||
await dialog.accept();
|
||||
});
|
||||
await page
|
||||
.getByRole("button", { name: /^(캐시 삭제|Revoke Cache)$/i })
|
||||
.click();
|
||||
await expect
|
||||
.poll(() => state.clients[0]?.headlessJwksCache)
|
||||
.toBeUndefined();
|
||||
|
||||
await page.reload();
|
||||
await expect(
|
||||
@@ -197,10 +261,6 @@ test.describe("DevFront clients lifecycle", () => {
|
||||
name: /공개키 등록|Public Key Registration/i,
|
||||
}),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByPlaceholder(
|
||||
/ssh-rsa AAA\.\.\.|Paste an 'ssh-rsa AAA\.\.\.' public key first/i,
|
||||
),
|
||||
).toHaveValue(/"kty": "RSA"/);
|
||||
await expect(page.getByRole("textbox", { name: /JWKS URI|JWKS URI/i })).toHaveValue(jwksUri);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user