mizzkey/packages/backend/test/e2e/oauth.ts

895 lines
28 KiB
TypeScript
Raw Normal View History

2023-04-02 21:59:38 +02:00
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
2023-04-16 16:03:14 +02:00
import { AuthorizationCode, type AuthorizationTokenConfig } from 'simple-oauth2';
2023-04-02 21:59:38 +02:00
import pkceChallenge from 'pkce-challenge';
import { JSDOM } from 'jsdom';
2023-04-10 20:29:11 +02:00
import * as misskey from 'misskey-js';
import Fastify, { type FastifyInstance } from 'fastify';
2023-04-10 14:49:18 +02:00
import { port, relativeFetch, signup, startServer } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
2023-04-02 21:59:38 +02:00
2023-04-10 10:17:41 +02:00
const host = `http://127.0.0.1:${port}`;
2023-04-03 22:32:12 +02:00
const clientPort = port + 1;
const redirect_uri = `http://127.0.0.1:${clientPort}/redirect`;
2023-04-16 15:51:52 +02:00
interface OAuthErrorResponse {
2023-04-15 23:15:37 +02:00
error: string;
2023-04-16 15:51:52 +02:00
error_description: string;
2023-04-15 23:15:37 +02:00
}
2023-04-16 16:03:14 +02:00
interface AuthorizationParamsExtended {
redirect_uri: string;
scope: string | string[];
state: string;
code_challenge?: string;
code_challenge_method?: string;
}
interface AuthorizationTokenConfigExtended extends AuthorizationTokenConfig {
code_verifier: string;
}
2023-04-03 22:32:12 +02:00
function getClient(): AuthorizationCode<'client_id'> {
return new AuthorizationCode({
client: {
id: `http://127.0.0.1:${clientPort}/`,
2023-05-11 23:09:24 +02:00
secret: '',
2023-04-03 22:32:12 +02:00
},
auth: {
2023-04-10 10:17:41 +02:00
tokenHost: host,
2023-04-03 22:32:12 +02:00
tokenPath: '/oauth/token',
authorizePath: '/oauth/authorize',
},
options: {
authorizationMethod: 'body',
},
});
}
2023-04-10 17:48:45 +02:00
function getMeta(html: string): { transactionId: string | undefined, clientName: string | undefined } {
2023-04-03 22:32:12 +02:00
const fragment = JSDOM.fragment(html);
2023-04-10 14:49:18 +02:00
return {
transactionId: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]')?.content,
clientName: fragment.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:client-name"]')?.content,
};
2023-04-03 22:32:12 +02:00
}
2023-04-10 20:29:11 +02:00
function fetchDecision(cookie: string, transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
2023-04-10 10:17:41 +02:00
return fetch(new URL('/oauth/decision', host), {
2023-04-03 22:32:12 +02:00
method: 'post',
body: new URLSearchParams({
transaction_id: transactionId!,
login_token: user.token,
cancel: cancel ? 'cancel' : '',
}),
redirect: 'manual',
headers: {
'content-type': 'application/x-www-form-urlencoded',
cookie,
},
});
}
2023-04-10 20:29:11 +02:00
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
2023-04-03 22:32:12 +02:00
const cookie = response.headers.get('set-cookie');
2023-04-10 14:49:18 +02:00
const { transactionId } = getMeta(await response.text());
2023-04-03 22:32:12 +02:00
return await fetchDecision(cookie!, transactionId!, user, { cancel });
}
2023-04-02 21:59:38 +02:00
describe('OAuth', () => {
let app: INestApplicationContext;
2023-04-10 14:49:18 +02:00
let fastify: FastifyInstance;
2023-04-02 21:59:38 +02:00
2023-04-10 20:29:11 +02:00
let alice: misskey.entities.MeSignup;
let bob: misskey.entities.MeSignup;
2023-04-02 21:59:38 +02:00
beforeAll(async () => {
app = await startServer();
2023-04-10 17:48:45 +02:00
alice = await signup({ username: 'alice' });
2023-04-10 20:29:11 +02:00
bob = await signup({ username: 'bob' });
2023-04-10 17:48:45 +02:00
}, 1000 * 60 * 2);
beforeEach(async () => {
process.env.MISSKEY_TEST_DISALLOW_LOOPBACK = '';
2023-04-10 14:49:18 +02:00
fastify = Fastify();
fastify.get('/', async (request, reply) => {
reply.send(`
<!DOCTYPE html>
<link rel="redirect_uri" href="/redirect" />
<div class="h-app"><div class="p-name">Misklient
`);
});
2023-04-10 16:26:04 +02:00
await fastify.listen({ port: clientPort });
2023-04-10 17:48:45 +02:00
});
2023-04-02 21:59:38 +02:00
afterAll(async () => {
await app.close();
2023-04-10 17:48:45 +02:00
});
afterEach(async () => {
2023-04-10 14:49:18 +02:00
await fastify.close();
2023-04-02 21:59:38 +02:00
});
test('Full flow', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-02 21:59:38 +02:00
2023-04-03 22:32:12 +02:00
const client = getClient();
2023-04-02 21:59:38 +02:00
2023-04-03 22:32:12 +02:00
const response = await fetch(client.authorizeURL({
2023-04-02 21:59:38 +02:00
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-02 21:59:38 +02:00
assert.strictEqual(response.status, 200);
const cookie = response.headers.get('set-cookie');
assert.ok(cookie?.startsWith('connect.sid='));
2023-04-10 14:49:18 +02:00
const meta = getMeta(await response.text());
assert.strictEqual(typeof meta.transactionId, 'string');
2023-04-10 17:48:45 +02:00
assert.strictEqual(meta.clientName, 'Misklient');
2023-04-02 21:59:38 +02:00
2023-04-10 14:49:18 +02:00
const decisionResponse = await fetchDecision(cookie!, meta.transactionId!, alice);
2023-04-02 21:59:38 +02:00
assert.strictEqual(decisionResponse.status, 302);
assert.ok(decisionResponse.headers.has('location'));
const location = new URL(decisionResponse.headers.get('location')!);
assert.strictEqual(location.origin + location.pathname, redirect_uri);
assert.ok(location.searchParams.has('code'));
assert.strictEqual(location.searchParams.get('state'), 'state');
2023-04-09 18:49:58 +02:00
assert.strictEqual(location.searchParams.get('iss'), 'http://misskey.local'); // RFC 9207
2023-04-02 21:59:38 +02:00
const token = await client.getToken({
code: location.searchParams.get('code')!,
redirect_uri,
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended);
2023-04-02 21:59:38 +02:00
assert.strictEqual(typeof token.token.access_token, 'string');
assert.strictEqual(token.token.token_type, 'Bearer');
2023-04-09 21:21:10 +02:00
assert.strictEqual(token.token.scope, 'write:notes');
2023-04-07 10:06:07 +02:00
const createResponse = await relativeFetch('api/notes/create', {
method: 'POST',
headers: {
Authorization: `Bearer ${token.token.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: 'test' }),
});
assert.strictEqual(createResponse.status, 200);
const createResponseBody: any = await createResponse.json();
assert.strictEqual(createResponseBody.createdNote.text, 'test');
2023-04-02 21:59:38 +02:00
});
2023-04-03 22:32:12 +02:00
2023-04-10 20:29:11 +02:00
test('Two concurrent flows', async () => {
const client = getClient();
2023-05-11 23:09:24 +02:00
const pkceAlice = await pkceChallenge(128);
const pkceBob = await pkceChallenge(128);
2023-04-10 20:29:11 +02:00
const responseAlice = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: pkceAlice.code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 20:29:11 +02:00
assert.strictEqual(responseAlice.status, 200);
const responseBob = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: pkceBob.code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 20:29:11 +02:00
assert.strictEqual(responseBob.status, 200);
const decisionResponseAlice = await fetchDecisionFromResponse(responseAlice, alice);
assert.strictEqual(decisionResponseAlice.status, 302);
const decisionResponseBob = await fetchDecisionFromResponse(responseBob, bob);
assert.strictEqual(decisionResponseBob.status, 302);
const locationAlice = new URL(decisionResponseAlice.headers.get('location')!);
assert.ok(locationAlice.searchParams.has('code'));
const locationBob = new URL(decisionResponseBob.headers.get('location')!);
assert.ok(locationBob.searchParams.has('code'));
const tokenAlice = await client.getToken({
code: locationAlice.searchParams.get('code')!,
redirect_uri,
code_verifier: pkceAlice.code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended);
2023-04-10 20:29:11 +02:00
const tokenBob = await client.getToken({
code: locationBob.searchParams.get('code')!,
redirect_uri,
code_verifier: pkceBob.code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended);
2023-04-10 20:29:11 +02:00
const createResponseAlice = await relativeFetch('api/notes/create', {
method: 'POST',
headers: {
Authorization: `Bearer ${tokenAlice.token.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: 'test' }),
});
assert.strictEqual(createResponseAlice.status, 200);
const createResponseBob = await relativeFetch('api/notes/create', {
method: 'POST',
headers: {
Authorization: `Bearer ${tokenBob.token.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: 'test' }),
});
assert.strictEqual(createResponseAlice.status, 200);
const createResponseBodyAlice = await createResponseAlice.json() as { createdNote: misskey.entities.Note };
assert.strictEqual(createResponseBodyAlice.createdNote.user.username, 'alice');
const createResponseBodyBob = await createResponseBob.json() as { createdNote: misskey.entities.Note };
assert.strictEqual(createResponseBodyBob.createdNote.user.username, 'bob');
});
2023-04-08 15:52:43 +02:00
describe('PKCE', () => {
test('Require PKCE', async () => {
const client = getClient();
2023-04-08 16:03:20 +02:00
// Pattern 1: No PKCE fields at all
2023-04-08 15:52:43 +02:00
let response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
}));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-08 15:52:43 +02:00
2023-04-08 16:03:20 +02:00
// Pattern 2: Only code_challenge
2023-04-08 15:52:43 +02:00
response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-08 15:52:43 +02:00
2023-04-08 16:03:20 +02:00
// Pattern 2: Only code_challenge_method
2023-04-08 15:52:43 +02:00
response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-08 15:52:43 +02:00
2023-04-08 16:03:20 +02:00
// Pattern 3: Unsupported code_challenge_method
2023-04-08 15:52:43 +02:00
response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'SSSS',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-08 15:52:43 +02:00
});
2023-04-03 22:32:12 +02:00
2023-05-11 23:09:24 +02:00
// TODO: Use precomputed challenge/verifier set for this one for deterministic test
2023-04-08 15:52:43 +02:00
test('Verify PKCE', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-08 15:52:43 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-08 15:52:43 +02:00
assert.strictEqual(response.status, 200);
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const code = new URL(decisionResponse.headers.get('location')!).searchParams.get('code')!;
assert.ok(!!code);
// Pattern 1: code followed by some junk code
await assert.rejects(client.getToken({
code,
redirect_uri,
code_verifier: code_verifier + 'x',
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended));
2023-04-08 15:52:43 +02:00
2023-04-10 20:29:11 +02:00
// TODO: The following patterns may fail only because of pattern 1's failure. Let's split them.
2023-04-08 15:52:43 +02:00
// Pattern 2: clipped code
await assert.rejects(client.getToken({
code,
redirect_uri,
code_verifier: code_verifier.slice(0, 80),
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended));
2023-04-08 15:52:43 +02:00
// Pattern 3: Some part of code is replaced
await assert.rejects(client.getToken({
code,
redirect_uri,
code_verifier: code_verifier.slice(0, -10) + 'x'.repeat(10),
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended));
2023-04-08 15:52:43 +02:00
2023-04-17 09:26:45 +02:00
// TODO: pattern 4: no code_verifier
2023-04-08 15:52:43 +02:00
// And now the code is invalidated by the previous failures
await assert.rejects(client.getToken({
code,
redirect_uri,
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended));
2023-04-08 15:52:43 +02:00
});
2023-04-03 22:32:12 +02:00
});
test('Cancellation', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-03 22:32:12 +02:00
assert.strictEqual(response.status, 200);
const decisionResponse = await fetchDecisionFromResponse(response, alice, { cancel: true });
2023-04-08 15:52:43 +02:00
assert.strictEqual(decisionResponse.status, 302);
2023-04-03 22:32:12 +02:00
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(!location.searchParams.has('code'));
assert.ok(location.searchParams.has('error'));
});
2023-04-05 20:47:12 +02:00
2023-04-08 20:31:18 +02:00
describe('Scope', () => {
test('Missing scope', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-08 20:31:18 +02:00
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_scope');
2023-04-08 20:31:18 +02:00
});
2023-04-05 20:47:12 +02:00
2023-04-08 20:31:18 +02:00
test('Empty scope', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: '',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-08 20:31:18 +02:00
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_scope');
2023-04-08 20:31:18 +02:00
});
test('Unknown scopes', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'test:unknown test:unknown2',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-08 20:31:18 +02:00
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_scope');
2023-04-08 20:31:18 +02:00
});
test('Partially known scopes', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-09 21:21:10 +02:00
2023-04-08 20:31:18 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes test:unknown test:unknown2',
state: 'state',
2023-04-09 21:21:10 +02:00
code_challenge,
2023-04-08 20:31:18 +02:00
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-08 20:31:18 +02:00
// Just get the known scope for this case for backward compatibility
assert.strictEqual(response.status, 200);
2023-04-09 21:21:10 +02:00
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(location.searchParams.has('code'));
const code = new URL(decisionResponse.headers.get('location')!).searchParams.get('code')!;
assert.ok(!!code);
const token = await client.getToken({
code,
redirect_uri,
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended);
2023-04-09 21:21:10 +02:00
// OAuth2 requires returning `scope` in the token response if the resulting scope is different than the requested one
// (Although Misskey always return scope, which is also fine)
assert.strictEqual(token.token.scope, 'write:notes');
2023-04-08 20:31:18 +02:00
});
test('Known scopes', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes read:account',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-08 20:31:18 +02:00
assert.strictEqual(response.status, 200);
});
2023-04-09 21:21:10 +02:00
test('Duplicated scopes', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-09 21:21:10 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes write:notes read:account read:account',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-09 21:21:10 +02:00
assert.strictEqual(response.status, 200);
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(location.searchParams.has('code'));
const code = new URL(decisionResponse.headers.get('location')!).searchParams.get('code')!;
assert.ok(!!code);
const token = await client.getToken({
code,
redirect_uri,
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended);
2023-04-09 21:21:10 +02:00
assert.strictEqual(token.token.scope, 'write:notes read:account');
});
test('Scope check by API', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-09 21:21:10 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'read:account',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-09 21:21:10 +02:00
assert.strictEqual(response.status, 200);
2023-04-09 14:01:44 +02:00
2023-04-09 21:21:10 +02:00
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(location.searchParams.has('code'));
const token = await client.getToken({
code: location.searchParams.get('code')!,
redirect_uri,
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended);
2023-04-09 21:21:10 +02:00
assert.strictEqual(typeof token.token.access_token, 'string');
const createResponse = await relativeFetch('api/notes/create', {
method: 'POST',
headers: {
Authorization: `Bearer ${token.token.access_token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: 'test' }),
});
// XXX: PERMISSION_DENIED is not using kind: 'permission' and gives 400 instead of 403
assert.strictEqual(createResponse.status, 400);
});
2023-04-09 14:01:44 +02:00
});
test('Authorization header', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-09 14:01:44 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-09 14:01:44 +02:00
assert.strictEqual(response.status, 200);
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(location.searchParams.has('code'));
const token = await client.getToken({
code: location.searchParams.get('code')!,
redirect_uri,
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended);
2023-04-09 14:01:44 +02:00
// Pattern 1: No preceding "Bearer "
let createResponse = await relativeFetch('api/notes/create', {
method: 'POST',
headers: {
Authorization: token.token.access_token as string,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: 'test' }),
});
assert.strictEqual(createResponse.status, 401);
// Pattern 2: Incorrect token
createResponse = await relativeFetch('api/notes/create', {
method: 'POST',
headers: {
Authorization: `Bearer ${(token.token.access_token as string).slice(0, -1)}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text: 'test' }),
});
// RFC 6750 section 3.1 says 401 but it's SHOULD not MUST. 403 should be okay for now.
assert.strictEqual(createResponse.status, 403);
// TODO: error code (invalid_token)
2023-04-08 20:31:18 +02:00
});
2023-04-09 16:43:19 +02:00
describe('Redirection', () => {
test('Invalid redirect_uri at authorization endpoint', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri: 'http://127.0.0.2/',
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-09 16:43:19 +02:00
});
2023-04-10 14:49:18 +02:00
test('Invalid redirect_uri including the valid one at authorization endpoint', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri: 'http://127.0.0.1/redirection',
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-10 14:49:18 +02:00
});
2023-04-09 18:49:58 +02:00
test('No redirect_uri at authorization endpoint', async () => {
const client = getClient();
const response = await fetch(client.authorizeURL({
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-09 18:49:58 +02:00
});
2023-04-09 16:43:19 +02:00
test('Invalid redirect_uri at token endpoint', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-09 16:43:19 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-09 16:43:19 +02:00
assert.strictEqual(response.status, 200);
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(location.searchParams.has('code'));
await assert.rejects(client.getToken({
code: location.searchParams.get('code')!,
redirect_uri: 'http://127.0.0.2/',
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended));
2023-04-09 16:43:19 +02:00
});
2023-04-10 14:49:18 +02:00
test('Invalid redirect_uri including the valid one at token endpoint', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-09 18:49:58 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-09 18:49:58 +02:00
assert.strictEqual(response.status, 200);
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(location.searchParams.has('code'));
await assert.rejects(client.getToken({
code: location.searchParams.get('code')!,
2023-04-10 14:49:18 +02:00
redirect_uri: 'http://127.0.0.1/redirection',
2023-04-09 18:49:58 +02:00
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended));
2023-04-09 18:49:58 +02:00
});
2023-04-10 14:49:18 +02:00
test('No redirect_uri at token endpoint', async () => {
2023-05-11 23:09:24 +02:00
const { code_challenge, code_verifier } = await pkceChallenge(128);
2023-04-10 14:49:18 +02:00
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge,
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 14:49:18 +02:00
assert.strictEqual(response.status, 200);
const decisionResponse = await fetchDecisionFromResponse(response, alice);
assert.strictEqual(decisionResponse.status, 302);
const location = new URL(decisionResponse.headers.get('location')!);
assert.ok(location.searchParams.has('code'));
await assert.rejects(client.getToken({
code: location.searchParams.get('code')!,
code_verifier,
2023-04-16 16:03:14 +02:00
} as AuthorizationTokenConfigExtended));
2023-04-10 14:49:18 +02:00
});
2023-04-09 16:43:19 +02:00
});
2023-04-10 10:17:41 +02:00
test('Server metadata', async () => {
const response = await fetch(new URL('.well-known/oauth-authorization-server', host));
assert.strictEqual(response.status, 200);
const body = await response.json();
assert.strictEqual(body.issuer, 'http://misskey.local');
assert.ok(body.scopes_supported.includes('write:notes'));
});
2023-04-05 20:47:12 +02:00
2023-04-10 16:26:04 +02:00
describe('Client Information Discovery', () => {
2023-04-10 17:48:45 +02:00
describe('Redirection', () => {
test('Read HTTP header', async () => {
await fastify.close();
fastify = Fastify();
fastify.get('/', async (request, reply) => {
reply.header('Link', '</redirect>; rel="redirect_uri"');
reply.send(`
2023-04-10 16:26:04 +02:00
<!DOCTYPE html>
<div class="h-app"><div class="p-name">Misklient
`);
2023-04-10 17:48:45 +02:00
});
await fastify.listen({ port: clientPort });
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 17:48:45 +02:00
assert.strictEqual(response.status, 200);
2023-04-10 16:26:04 +02:00
});
2023-04-10 17:48:45 +02:00
test('Mixed links', async () => {
await fastify.close();
2023-04-10 16:26:04 +02:00
2023-04-10 17:48:45 +02:00
fastify = Fastify();
fastify.get('/', async (request, reply) => {
reply.header('Link', '</redirect>; rel="redirect_uri"');
reply.send(`
2023-04-10 16:26:04 +02:00
<!DOCTYPE html>
<link rel="redirect_uri" href="/redirect2" />
<div class="h-app"><div class="p-name">Misklient
`);
2023-04-10 17:48:45 +02:00
});
await fastify.listen({ port: clientPort });
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 17:48:45 +02:00
assert.strictEqual(response.status, 200);
2023-04-10 16:26:04 +02:00
});
2023-04-10 17:48:45 +02:00
test('Multiple items in Link header', async () => {
await fastify.close();
2023-04-10 16:26:04 +02:00
2023-04-10 17:48:45 +02:00
fastify = Fastify();
fastify.get('/', async (request, reply) => {
reply.header('Link', '</redirect2>; rel="redirect_uri",</redirect>; rel="redirect_uri"');
reply.send(`
2023-04-10 16:26:04 +02:00
<!DOCTYPE html>
<div class="h-app"><div class="p-name">Misklient
`);
2023-04-10 17:48:45 +02:00
});
await fastify.listen({ port: clientPort });
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 17:48:45 +02:00
assert.strictEqual(response.status, 200);
2023-04-10 16:26:04 +02:00
});
2023-04-10 17:48:45 +02:00
test('Multiple items in HTML', async () => {
await fastify.close();
2023-04-10 16:26:04 +02:00
2023-04-10 17:48:45 +02:00
fastify = Fastify();
fastify.get('/', async (request, reply) => {
reply.send(`
2023-04-10 16:26:04 +02:00
<!DOCTYPE html>
<link rel="redirect_uri" href="/redirect2" />
<link rel="redirect_uri" href="/redirect" />
<div class="h-app"><div class="p-name">Misklient
`);
2023-04-10 17:48:45 +02:00
});
await fastify.listen({ port: clientPort });
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 17:48:45 +02:00
assert.strictEqual(response.status, 200);
2023-04-10 16:26:04 +02:00
});
2023-04-10 17:48:45 +02:00
test('No item', async () => {
await fastify.close();
2023-04-10 16:26:04 +02:00
2023-04-10 17:48:45 +02:00
fastify = Fastify();
fastify.get('/', async (request, reply) => {
reply.send(`
<!DOCTYPE html>
<div class="h-app"><div class="p-name">Misklient
`);
});
await fastify.listen({ port: clientPort });
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-10 17:48:45 +02:00
});
});
test('Disallow loopback', async () => {
process.env.MISSKEY_TEST_DISALLOW_LOOPBACK = '1';
const client = getClient();
2023-04-10 16:26:04 +02:00
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-15 23:15:37 +02:00
assert.strictEqual(response.status, 400);
2023-04-16 15:51:52 +02:00
assert.strictEqual((await response.json() as OAuthErrorResponse).error, 'invalid_request');
2023-04-10 16:26:04 +02:00
});
2023-04-10 17:48:45 +02:00
test('Missing name', async () => {
2023-04-10 16:26:04 +02:00
await fastify.close();
fastify = Fastify();
fastify.get('/', async (request, reply) => {
2023-04-10 17:48:45 +02:00
reply.header('Link', '</redirect>; rel="redirect_uri"');
reply.send();
2023-04-10 16:26:04 +02:00
});
await fastify.listen({ port: clientPort });
const client = getClient();
const response = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: 'code',
code_challenge_method: 'S256',
2023-04-16 16:03:14 +02:00
} as AuthorizationParamsExtended));
2023-04-10 17:48:45 +02:00
assert.strictEqual(response.status, 200);
assert.strictEqual(getMeta(await response.text()).clientName, `http://127.0.0.1:${clientPort}/`);
2023-04-10 16:26:04 +02:00
});
});
2023-04-15 23:15:37 +02:00
// TODO: Invalid decision endpoint parameters
2023-04-16 15:43:32 +02:00
// TODO: Unknown OAuth endpoint
2023-04-17 09:26:45 +02:00
// TODO: successful token exchange should invalidate the grant token (spec?)
2023-04-02 21:59:38 +02:00
});