concurrent flow test
This commit is contained in:
parent
a688bd1061
commit
027c5734a4
3 changed files with 85 additions and 33 deletions
|
@ -76,33 +76,6 @@ function validateClientId(raw: string): URL {
|
||||||
return 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 {
|
interface ClientInformation {
|
||||||
id: string;
|
id: string;
|
||||||
redirectUris: string[];
|
redirectUris: string[];
|
||||||
|
|
|
@ -4,9 +4,10 @@ import * as assert from 'assert';
|
||||||
import { AuthorizationCode } from 'simple-oauth2';
|
import { AuthorizationCode } from 'simple-oauth2';
|
||||||
import pkceChallenge from 'pkce-challenge';
|
import pkceChallenge from 'pkce-challenge';
|
||||||
import { JSDOM } from 'jsdom';
|
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 { port, relativeFetch, signup, startServer } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
import Fastify, { type FastifyInstance } from 'fastify';
|
|
||||||
|
|
||||||
const host = `http://127.0.0.1:${port}`;
|
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), {
|
return fetch(new URL('/oauth/decision', host), {
|
||||||
method: 'post',
|
method: 'post',
|
||||||
body: new URLSearchParams({
|
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 cookie = response.headers.get('set-cookie');
|
||||||
const { transactionId } = getMeta(await response.text());
|
const { transactionId } = getMeta(await response.text());
|
||||||
|
|
||||||
|
@ -64,11 +65,13 @@ describe('OAuth', () => {
|
||||||
let app: INestApplicationContext;
|
let app: INestApplicationContext;
|
||||||
let fastify: FastifyInstance;
|
let fastify: FastifyInstance;
|
||||||
|
|
||||||
let alice: any;
|
let alice: misskey.entities.MeSignup;
|
||||||
|
let bob: misskey.entities.MeSignup;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
app = await startServer();
|
app = await startServer();
|
||||||
alice = await signup({ username: 'alice' });
|
alice = await signup({ username: 'alice' });
|
||||||
|
bob = await signup({ username: 'bob' });
|
||||||
}, 1000 * 60 * 2);
|
}, 1000 * 60 * 2);
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
@ -145,6 +148,81 @@ describe('OAuth', () => {
|
||||||
assert.strictEqual(createResponseBody.createdNote.text, 'test');
|
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', () => {
|
describe('PKCE', () => {
|
||||||
test('Require PKCE', async () => {
|
test('Require PKCE', async () => {
|
||||||
const client = getClient();
|
const client = getClient();
|
||||||
|
@ -213,6 +291,8 @@ describe('OAuth', () => {
|
||||||
code_verifier: code_verifier + 'x',
|
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
|
// Pattern 2: clipped code
|
||||||
await assert.rejects(client.getToken({
|
await assert.rejects(client.getToken({
|
||||||
code,
|
code,
|
||||||
|
@ -776,7 +856,5 @@ describe('OAuth', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: authorizing two users concurrently
|
|
||||||
|
|
||||||
// TODO: Error format required by OAuth spec
|
// TODO: Error format required by OAuth spec
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
import { entities } from '../src/postgres.js';
|
import { entities } from '../src/postgres.js';
|
||||||
import { loadConfig } from '../src/config.js';
|
import { loadConfig } from '../src/config.js';
|
||||||
import type * as misskey from 'misskey-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';
|
export { server as startServer } from '@/boot/common.js';
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue