concurrent flow test

This commit is contained in:
Kagami Sascha Rosylight 2023-04-10 20:29:11 +02:00
parent a688bd1061
commit 027c5734a4
3 changed files with 85 additions and 33 deletions

View file

@ -76,33 +76,6 @@ function validateClientId(raw: string): URL {
return url;
}
// const grantable = new Set([
// 'AccessToken',
// 'AuthorizationCode',
// 'RefreshToken',
// 'DeviceCode',
// 'BackchannelAuthenticationRequest',
// ]);
// const consumable = new Set([
// 'AuthorizationCode',
// 'RefreshToken',
// 'DeviceCode',
// 'BackchannelAuthenticationRequest',
// ]);
// function grantKeyFor(id: string): string {
// return `grant:${id}`;
// }
// function userCodeKeyFor(userCode: string): string {
// return `userCode:${userCode}`;
// }
// function uidKeyFor(uid: string): string {
// return `uid:${uid}`;
// }
interface ClientInformation {
id: string;
redirectUris: string[];

View file

@ -4,9 +4,10 @@ import * as assert from 'assert';
import { AuthorizationCode } from 'simple-oauth2';
import pkceChallenge from 'pkce-challenge';
import { JSDOM } from 'jsdom';
import * as misskey from 'misskey-js';
import Fastify, { type FastifyInstance } from 'fastify';
import { port, relativeFetch, signup, startServer } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import Fastify, { type FastifyInstance } from 'fastify';
const host = `http://127.0.0.1:${port}`;
@ -37,7 +38,7 @@ function getMeta(html: string): { transactionId: string | undefined, clientName:
};
}
function fetchDecision(cookie: string, transactionId: string, user: any, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
function fetchDecision(cookie: string, transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
return fetch(new URL('/oauth/decision', host), {
method: 'post',
body: new URLSearchParams({
@ -53,7 +54,7 @@ function fetchDecision(cookie: string, transactionId: string, user: any, { cance
});
}
async function fetchDecisionFromResponse(response: Response, user: any, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise<Response> {
const cookie = response.headers.get('set-cookie');
const { transactionId } = getMeta(await response.text());
@ -64,11 +65,13 @@ describe('OAuth', () => {
let app: INestApplicationContext;
let fastify: FastifyInstance;
let alice: any;
let alice: misskey.entities.MeSignup;
let bob: misskey.entities.MeSignup;
beforeAll(async () => {
app = await startServer();
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
beforeEach(async () => {
@ -145,6 +148,81 @@ describe('OAuth', () => {
assert.strictEqual(createResponseBody.createdNote.text, 'test');
});
test('Two concurrent flows', async () => {
const client = getClient();
const pkceAlice = pkceChallenge.default(128);
const pkceBob = pkceChallenge.default(128);
const responseAlice = await fetch(client.authorizeURL({
redirect_uri,
scope: 'write:notes',
state: 'state',
code_challenge: pkceAlice.code_challenge,
code_challenge_method: 'S256',
}));
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',
}));
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,
});
const tokenBob = await client.getToken({
code: locationBob.searchParams.get('code')!,
redirect_uri,
code_verifier: pkceBob.code_verifier,
});
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');
});
describe('PKCE', () => {
test('Require PKCE', async () => {
const client = getClient();
@ -213,6 +291,8 @@ describe('OAuth', () => {
code_verifier: code_verifier + 'x',
}));
// TODO: The following patterns may fail only because of pattern 1's failure. Let's split them.
// Pattern 2: clipped code
await assert.rejects(client.getToken({
code,
@ -776,7 +856,5 @@ describe('OAuth', () => {
});
});
// TODO: authorizing two users concurrently
// TODO: Error format required by OAuth spec
});

View file

@ -10,6 +10,7 @@ import { DEFAULT_POLICIES } from '@/core/RoleService.js';
import { entities } from '../src/postgres.js';
import { loadConfig } from '../src/config.js';
import type * as misskey from 'misskey-js';
import type { MeSignup } from 'misskey-js/built/entities.js';
export { server as startServer } from '@/boot/common.js';