Files
nocobase-mcp/test/oauth.test.ts
T

151 lines
5.6 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import { type AuthorizationPrompt, loginWithBrowser, refreshOAuthCredentials } from "../src/oauth.js";
const discovery = {
issuer: "https://justconn.top/api",
device_authorization_endpoint: "https://justconn.top/api/idpOAuth/device/auth",
registration_endpoint: "https://justconn.top/api/idpOAuth/register",
token_endpoint: "https://justconn.top/api/idpOAuth/token",
grant_types_supported: [
"authorization_code",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
],
scopes_supported: ["openid", "offline_access", "api", "mcp"],
token_endpoint_auth_methods_supported: ["none", "client_secret_basic"],
};
test("uses NocoBase dynamic registration and browser device authorization without exposing tokens", async () => {
const calls: Array<{ body: string; method: string; url: string }> = [];
let tokenPolls = 0;
const fetchMock = (async (input: string | URL | Request, init: RequestInit = {}) => {
const url = String(input);
const method = init.method ?? "GET";
const body =
typeof init.body === "string" || init.body instanceof URLSearchParams ? String(init.body) : "";
calls.push({ body, method, url });
if (url.endsWith("/.well-known/openid-configuration")) return jsonResponse(discovery);
if (url.endsWith("/idpOAuth/register")) {
return jsonResponse(
{
client_id: "desktop-client",
registration_access_token: "must-not-be-stored",
token_endpoint_auth_method: "none",
},
201,
);
}
if (url.endsWith("/idpOAuth/device/auth")) {
return jsonResponse({
device_code: "private-device-code",
user_code: "ABCD-EFGH",
verification_uri: "https://justconn.top/idpOAuth/device",
verification_uri_complete: "https://justconn.top/idpOAuth/device?user_code=ABCD-EFGH",
expires_in: 600,
interval: 5,
});
}
if (url.endsWith("/idpOAuth/token")) {
tokenPolls += 1;
if (tokenPolls === 1) return jsonResponse({ error: "authorization_pending" }, 400);
return jsonResponse({
access_token: "oauth-access-token",
refresh_token: "oauth-refresh-token",
token_type: "Bearer",
scope: "api offline_access",
expires_in: 900,
});
}
throw new Error(`Unexpected request: ${method} ${url}`);
}) as typeof fetch;
let prompt: AuthorizationPrompt | undefined;
const sleeps: number[] = [];
const credentials = await loginWithBrowser({
fetch: fetchMock,
now: () => 1_000,
openBrowser: async () => true,
onAuthorization: (value) => {
prompt = value;
},
sleep: async (milliseconds) => {
sleeps.push(milliseconds);
},
});
assert.equal(credentials.clientId, "desktop-client");
assert.equal(credentials.accessToken, "oauth-access-token");
assert.equal(credentials.refreshToken, "oauth-refresh-token");
assert.equal(credentials.expiresAt, 901_000);
assert.doesNotMatch(JSON.stringify(credentials), /must-not-be-stored/);
assert.deepEqual(prompt, {
opened: true,
url: "https://justconn.top/idpOAuth/device?user_code=ABCD-EFGH",
userCode: "ABCD-EFGH",
expiresIn: 600,
});
assert.deepEqual(sleeps, [5_000, 5_000]);
const registration = JSON.parse(calls.find((call) => call.url.endsWith("/register"))?.body ?? "{}");
assert.equal(registration.application_type, "native");
assert.equal(registration.token_endpoint_auth_method, "none");
assert.deepEqual(registration.response_types, []);
assert.deepEqual(registration.redirect_uris, []);
assert.equal(registration.client_secret, undefined);
const deviceForm = new URLSearchParams(calls.find((call) => call.url.endsWith("/device/auth"))?.body);
assert.equal(deviceForm.get("scope"), "api offline_access");
assert.equal(deviceForm.get("resource"), "https://justconn.top/api/");
});
test("rejects OAuth discovery that points tokens at another origin", async () => {
const fetchMock = (async () =>
jsonResponse({ ...discovery, token_endpoint: "https://evil.example/token" })) as typeof fetch;
await assert.rejects(
loginWithBrowser({ fetch: fetchMock, openBrowser: async () => true }),
/untrusted endpoint/,
);
});
test("refreshes a public-client OAuth session and keeps a rotated refresh token optional", async () => {
const bodies: string[] = [];
const fetchMock = (async (input: string | URL | Request, init: RequestInit = {}) => {
const url = String(input);
if (url.endsWith("/.well-known/openid-configuration")) return jsonResponse(discovery);
bodies.push(String(init.body));
return jsonResponse({
access_token: "new-access",
token_type: "Bearer",
scope: "api offline_access",
expires_in: 600,
});
}) as typeof fetch;
const refreshed = await refreshOAuthCredentials(
{
type: "oauth",
issuer: "https://justconn.top/api",
clientId: "desktop-client",
accessToken: "old-access",
refreshToken: "old-refresh",
tokenType: "Bearer",
scope: "api offline_access",
expiresAt: 10,
},
{ fetch: fetchMock, now: () => 2_000 },
);
assert.equal(refreshed.accessToken, "new-access");
assert.equal(refreshed.refreshToken, "old-refresh");
assert.equal(refreshed.expiresAt, 602_000);
const form = new URLSearchParams(bodies[0]);
assert.equal(form.get("grant_type"), "refresh_token");
assert.equal(form.get("client_id"), "desktop-client");
assert.equal(form.get("refresh_token"), "old-refresh");
});
function jsonResponse(value: unknown, status = 200): Response {
return new Response(JSON.stringify(value), {
status,
headers: { "content-type": "application/json" },
});
}