concurrent flow test
This commit is contained in:
		| @@ -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'; | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Kagami Sascha Rosylight
					Kagami Sascha Rosylight