diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index 68056587e..76b88e35c 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -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[]; diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index bc8e62ec7..5670a5be4 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -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 { +function fetchDecision(cookie: string, transactionId: string, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { 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 { +async function fetchDecisionFromResponse(response: Response, user: misskey.entities.MeSignup, { cancel }: { cancel?: boolean } = {}): Promise { 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 }); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index 37c1474be..8de6e4d7e 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -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';