158 lines
5.8 KiB
TypeScript
158 lines
5.8 KiB
TypeScript
import 'dotenv/config';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { randomBytes, randomUUID, createHash } from 'node:crypto';
|
|
import { PrismaClient } from '@prisma/client';
|
|
|
|
test(
|
|
'OAuth read-only requests offer all three ordinary levels; explicit owner consent determines token scopes',
|
|
{ skip: process.env.TEST_ISOLATED !== 'true' },
|
|
async () => {
|
|
const db = new PrismaClient(),
|
|
ids: string[] = [];
|
|
const base = process.env.TEST_API_URL!,
|
|
root = base.replace(/\/api$/, '');
|
|
const resource = process.env.MCP_PUBLIC_URL!,
|
|
redirect = 'http://127.0.0.1:47891/callback';
|
|
const clientId = randomUUID();
|
|
async function web(cookie: string, id: string, body?: unknown) {
|
|
const r = await fetch(base + '/agent/authorizations/' + id, {
|
|
method: body ? 'POST' : 'GET',
|
|
headers: {
|
|
Cookie: cookie,
|
|
Origin: process.env.WEB_ORIGIN!,
|
|
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
return { status: r.status, data: await r.json() };
|
|
}
|
|
async function request(scopes = ['read']) {
|
|
const id = randomUUID(),
|
|
verifier = randomBytes(32).toString('base64url');
|
|
await db.agentAuthorization.create({
|
|
data: {
|
|
id,
|
|
clientId,
|
|
expiresAt: new Date(Date.now() + 600000),
|
|
parameters: {
|
|
redirectUri: redirect,
|
|
resource,
|
|
scopes,
|
|
codeChallenge: createHash('sha256').update(verifier).digest('base64url'),
|
|
},
|
|
},
|
|
});
|
|
return { id, verifier };
|
|
}
|
|
try {
|
|
await db.agentClient.create({
|
|
data: {
|
|
id: clientId,
|
|
metadata: {
|
|
client_id: clientId,
|
|
client_name: 'Three permissions regression',
|
|
redirect_uris: [redirect],
|
|
token_endpoint_auth_method: 'none',
|
|
grant_types: ['authorization_code', 'refresh_token'],
|
|
},
|
|
},
|
|
});
|
|
for (const role of ['user', 'readonly'] as const) {
|
|
const u = await db.user.create({
|
|
data: { username: 'oauth_levels_' + randomUUID(), passwordHash: 'unused', role },
|
|
});
|
|
ids.push(u.id);
|
|
const session = randomBytes(32).toString('hex');
|
|
await db.session.create({
|
|
data: {
|
|
id: createHash('sha256').update(session).digest('hex'),
|
|
userId: u.id,
|
|
expiresAt: new Date(Date.now() + 3600000),
|
|
},
|
|
});
|
|
const cookie = 'wp_session=' + session;
|
|
for (const level of ['read', 'draft', 'write']) {
|
|
const a = await request();
|
|
const preview = await web(cookie, a.id);
|
|
assert.equal(preview.status, 200);
|
|
assert.deepEqual(preview.data.scopes, ['read']);
|
|
assert.deepEqual(preview.data.availableScopes, ['read', 'draft', 'write']);
|
|
assert.equal(preview.data.canWrite, role !== 'readonly');
|
|
const selected = level === 'read' ? ['read'] : ['read', level];
|
|
const approved = await web(cookie, a.id, { approve: true, scopes: selected, days: 7 });
|
|
if (role === 'readonly' && level !== 'read') {
|
|
assert.equal(approved.status, 403);
|
|
assert.equal(
|
|
(await db.agentAuthorization.findUniqueOrThrow({ where: { id: a.id } })).status,
|
|
'pending',
|
|
);
|
|
continue;
|
|
}
|
|
assert.equal(approved.status, 201, JSON.stringify(approved.data));
|
|
const code = new URL(approved.data.redirect).searchParams.get('code')!;
|
|
const response = await fetch(root + '/token', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: new URLSearchParams({
|
|
client_id: clientId,
|
|
grant_type: 'authorization_code',
|
|
code,
|
|
code_verifier: a.verifier,
|
|
redirect_uri: redirect,
|
|
resource,
|
|
}),
|
|
});
|
|
const token = await response.json();
|
|
assert.equal(response.status, 200, JSON.stringify(token));
|
|
assert.equal(token.scope, selected.join(' '));
|
|
const row = await db.agentGrant.findFirstOrThrow({
|
|
where: {
|
|
userId: u.id,
|
|
accessDigest: createHash('sha256').update(token.access_token).digest('hex'),
|
|
},
|
|
});
|
|
assert.deepEqual(row.scopes, selected);
|
|
}
|
|
if (role === 'user') {
|
|
const noHidden = await request();
|
|
assert.equal(
|
|
(
|
|
await web(cookie, noHidden.id, {
|
|
approve: true,
|
|
scopes: ['read', 'write', 'hidden_read'],
|
|
})
|
|
).status,
|
|
400,
|
|
);
|
|
const requestedHidden = await request(['read', 'hidden_read', 'hidden_write']);
|
|
assert.equal(
|
|
(
|
|
await web(cookie, requestedHidden.id, {
|
|
approve: true,
|
|
scopes: ['read', 'write', 'hidden_read', 'hidden_write'],
|
|
})
|
|
).status,
|
|
201,
|
|
);
|
|
const defaults = await request();
|
|
assert.equal((await web(cookie, defaults.id, { approve: true })).status, 201);
|
|
assert.deepEqual(
|
|
(await db.agentAuthorization.findUniqueOrThrow({ where: { id: defaults.id } }))
|
|
.parameters &&
|
|
(
|
|
(await db.agentAuthorization.findUniqueOrThrow({ where: { id: defaults.id } }))
|
|
.parameters as any
|
|
).scopes,
|
|
['read'],
|
|
);
|
|
}
|
|
}
|
|
} finally {
|
|
await db.user.deleteMany({ where: { id: { in: ids } } });
|
|
await db.agentClient.deleteMany({ where: { id: clientId } });
|
|
await db.$disconnect();
|
|
}
|
|
},
|
|
);
|