diff --git a/packages/backend/test/e2e/api.ts b/packages/backend/test/e2e/api.ts index c6beec4f88..137ce5e695 100644 --- a/packages/backend/test/e2e/api.ts +++ b/packages/backend/test/e2e/api.ts @@ -1,7 +1,7 @@ process.env.NODE_ENV = 'test'; import * as assert from 'assert'; -import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream } from '../utils.js'; +import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch } from '../utils.js'; import type { INestApplicationContext } from '@nestjs/common'; import type * as misskey from 'misskey-js'; import { IncomingMessage } from 'http'; @@ -218,6 +218,44 @@ describe('API', () => { assert.ok(result.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="invalid_request", error_description')); }); + describe('invalid bearer format', () => { + test('No preceding bearer', async () => { + const result = await relativeFetch('api/notes/create', { + method: 'POST', + headers: { + Authorization: alice.token, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text: 'test' }), + }); + assert.strictEqual(result.status, 401); + }); + + test('Lowercase bearer', async () => { + const result = await relativeFetch('api/notes/create', { + method: 'POST', + headers: { + Authorization: `bearer ${alice.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text: 'test' }), + }); + assert.strictEqual(result.status, 401); + }); + + test('No space after bearer', async () => { + const result = await relativeFetch('api/notes/create', { + method: 'POST', + headers: { + Authorization: `Bearer${alice.token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ text: 'test' }), + }); + assert.strictEqual(result.status, 401); + }); + }); + // TODO: insufficient_scope test (authテストが全然なくて書けない) }); }); diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index a40b78be26..3762762ebc 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -10,7 +10,7 @@ import { AuthorizationCode, ResourceOwnerPassword, type AuthorizationTokenConfig import pkceChallenge from 'pkce-challenge'; import { JSDOM } from 'jsdom'; import Fastify, { type FastifyReply, type FastifyInstance } from 'fastify'; -import { port, relativeFetch, signup, startServer } from '../utils.js'; +import { api, port, signup, startServer } from '../utils.js'; import type * as misskey from 'misskey-js'; import type { INestApplicationContext } from '@nestjs/common'; @@ -220,18 +220,14 @@ describe('OAuth', () => { assert.strictEqual(token.token.token_type, 'Bearer'); assert.strictEqual(token.token.scope, 'write:notes'); - 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' }), + const createResult = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, }); - assert.strictEqual(createResponse.status, 200); + assert.strictEqual(createResult.status, 200); - const createResponseBody = await createResponse.json() as misskey.Endpoints['notes/create']['res']; - assert.strictEqual(createResponseBody.createdNote.text, 'test'); + const createResultBody = createResult.body as misskey.Endpoints['notes/create']['res']; + assert.strictEqual(createResultBody.createdNote.text, 'test'); }); test('Two concurrent flows', async () => { @@ -289,31 +285,23 @@ describe('OAuth', () => { code_verifier: pkceBob.code_verifier, } as AuthorizationTokenConfigExtended); - 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' }), + const createResultAlice = await api('notes/create', { text: 'test' }, { + token: tokenAlice.token.access_token as string, + bearer: true, }); - assert.strictEqual(createResponseAlice.status, 200); + assert.strictEqual(createResultAlice.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' }), + const createResultBob = await api('notes/create', { text: 'test' }, { + token: tokenBob.token.access_token as string, + bearer: true, }); - assert.strictEqual(createResponseAlice.status, 200); + assert.strictEqual(createResultAlice.status, 200); - const createResponseBodyAlice = await createResponseAlice.json() as misskey.Endpoints['notes/create']['res']; - assert.strictEqual(createResponseBodyAlice.createdNote.user.username, 'alice'); + const createResultBodyAlice = await createResultAlice.body as misskey.Endpoints['notes/create']['res']; + assert.strictEqual(createResultBodyAlice.createdNote.user.username, 'alice'); - const createResponseBodyBob = await createResponseBob.json() as misskey.Endpoints['notes/create']['res']; - assert.strictEqual(createResponseBodyBob.createdNote.user.username, 'bob'); + const createResultBodyBob = await createResultBob.body as misskey.Endpoints['notes/create']['res']; + assert.strictEqual(createResultBodyBob.createdNote.user.username, 'bob'); }); // https://datatracker.ietf.org/doc/html/rfc7636.html @@ -444,15 +432,11 @@ describe('OAuth', () => { code_verifier, } as AuthorizationTokenConfigExtended); - 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' }), + const createResult = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, }); - assert.strictEqual(createResponse.status, 200); + assert.strictEqual(createResult.status, 200); await assert.rejects(client.getToken({ code, @@ -463,15 +447,11 @@ describe('OAuth', () => { return true; }); - const createResponse2 = await relativeFetch('api/notes/create', { - method: 'POST', - headers: { - Authorization: `Bearer ${token.token.access_token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: 'test' }), + const createResult2 = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, }); - assert.strictEqual(createResponse2.status, 401); + assert.strictEqual(createResult2.status, 401); }); }); @@ -610,79 +590,15 @@ describe('OAuth', () => { } as AuthorizationTokenConfigExtended); 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' }), + const createResult = await api('notes/create', { text: 'test' }, { + token: token.token.access_token as string, + bearer: true, }); - assert.strictEqual(createResponse.status, 403); + assert.strictEqual(createResult.status, 403); + assert.ok(createResult.headers.get('WWW-Authenticate')?.startsWith('Bearer realm="Misskey", error="insufficient_scope", error_description')); }); }); - // https://datatracker.ietf.org/doc/html/rfc6750.html - test('Authorization header', async () => { - const { code_challenge, code_verifier } = await pkceChallenge(128); - - const { client, code } = await fetchAuthorizationCode(alice, 'write:notes', code_challenge); - - const token = await client.getToken({ - code, - redirect_uri, - code_verifier, - } as AuthorizationTokenConfigExtended); - - // 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' }), - }); - - // https://datatracker.ietf.org/doc/html/rfc6750.html#section-3.1 - // "The access token provided is expired, revoked, malformed, or - // invalid for other reasons. The resource SHOULD respond with - // the HTTP 401 (Unauthorized) status code." - assert.strictEqual(createResponse.status, 401); - - let wwwAuthenticate = createResponse.headers.get('WWW-Authenticate'); - assert.ok(wwwAuthenticate?.startsWith('Bearer realm="Misskey", error="invalid_token"')); - - // Pattern 3: No token - createResponse = await relativeFetch('api/notes/create', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ text: 'test' }), - }); - wwwAuthenticate = createResponse.headers.get('WWW-Authenticate'); - - // https://datatracker.ietf.org/doc/html/rfc6750.html#section-3.1 - // "If the request lacks any authentication information (e.g., the client - // was unaware that authentication is necessary or attempted using an - // unsupported authentication method), the resource server SHOULD NOT - // include an error code or other error information." - assert.strictEqual(createResponse.status, 401); - assert.strictEqual(wwwAuthenticate, 'Bearer realm="Misskey"'); - }); - // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.4 // "If an authorization request fails validation due to a missing, // invalid, or mismatching redirection URI, the authorization server