Use Keycloak OAuth for a2amcp login
This commit is contained in:
95
tests/oauth.test.ts
Normal file
95
tests/oauth.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { test, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promises as fs } from "node:fs";
|
||||
|
||||
import { loadCredentials, saveCredentials } from "../src/credentials.js";
|
||||
import {
|
||||
buildAuthorizationUrl,
|
||||
createPkcePair,
|
||||
refreshCredentialsIfNeeded,
|
||||
} from "../src/oauth.js";
|
||||
|
||||
let originalHome: string | undefined;
|
||||
let tmpHome: string;
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeEach(async () => {
|
||||
originalHome = process.env.HOME;
|
||||
tmpHome = await fs.mkdtemp(path.join(os.tmpdir(), "a2a-mcp-oauth-"));
|
||||
process.env.HOME = tmpHome;
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (originalHome !== undefined) process.env.HOME = originalHome;
|
||||
else delete process.env.HOME;
|
||||
globalThis.fetch = originalFetch;
|
||||
await fs.rm(tmpHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("PKCE authorization URL requests Keycloak code flow", () => {
|
||||
const pkce = createPkcePair();
|
||||
assert.ok(pkce.verifier.length > 40);
|
||||
assert.ok(pkce.challenge.length > 40);
|
||||
|
||||
const url = new URL(
|
||||
buildAuthorizationUrl({
|
||||
authorizationEndpoint: "https://auth.example.test/auth",
|
||||
clientId: "a2acloud-cli",
|
||||
redirectUri: "http://127.0.0.1:41873/callback",
|
||||
scope: "openid email mcp:invoke",
|
||||
state: "state-123",
|
||||
codeChallenge: pkce.challenge,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(url.searchParams.get("client_id"), "a2acloud-cli");
|
||||
assert.equal(url.searchParams.get("response_type"), "code");
|
||||
assert.equal(url.searchParams.get("code_challenge_method"), "S256");
|
||||
assert.equal(url.searchParams.get("scope"), "openid email mcp:invoke");
|
||||
});
|
||||
|
||||
test("refreshCredentialsIfNeeded refreshes expired Keycloak access token", async () => {
|
||||
const calls: Array<{ url: string; body?: string }> = [];
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
calls.push({ url, body: init?.body?.toString() });
|
||||
if (url.endsWith("/.well-known/openid-configuration")) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
authorization_endpoint: "https://auth.example.test/auth",
|
||||
token_endpoint: "https://auth.example.test/token",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
access_token: "fresh-token",
|
||||
refresh_token: "fresh-refresh",
|
||||
expires_in: 3600,
|
||||
scope: "openid email mcp:invoke",
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
await saveCredentials({
|
||||
apiUrl: "https://api.example.test",
|
||||
token: "old-token",
|
||||
refreshToken: "refresh-token",
|
||||
expiresAt: 1,
|
||||
email: "dev@example.test",
|
||||
issuer: "https://auth.example.test",
|
||||
clientId: "a2acloud-cli",
|
||||
});
|
||||
|
||||
const refreshed = await refreshCredentialsIfNeeded((await loadCredentials())!);
|
||||
|
||||
assert.equal(refreshed.token, "fresh-token");
|
||||
assert.equal(refreshed.refreshToken, "fresh-refresh");
|
||||
assert.equal((await loadCredentials())!.token, "fresh-token");
|
||||
assert.ok(calls.some((call) => call.body?.includes("grant_type=refresh_token")));
|
||||
});
|
||||
Reference in New Issue
Block a user