diff options
| author | Kagami Sascha Rosylight <saschanaz@outlook.com> | 2023-12-27 07:10:24 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-12-27 15:10:24 +0900 |
| commit | ad346b6f368f1da2874c9c575884107630f6e5c8 (patch) | |
| tree | a737724d2218927c26e8c56a21d1683743f23e3e /packages | |
| parent | Merge pull request from GHSA-7pxq-6xx9-xpgm (diff) | |
| download | sharkey-ad346b6f368f1da2874c9c575884107630f6e5c8.tar.gz sharkey-ad346b6f368f1da2874c9c575884107630f6e5c8.tar.bz2 sharkey-ad346b6f368f1da2874c9c575884107630f6e5c8.zip | |
feat(backend/oauth): allow CORS for token endpoint (#12814)
* feat(backend/oauth): allow CORS for token endpoint
* no need to explicitly set origin to `*`
* Update CHANGELOG.md
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/backend/package.json | 2 | ||||
| -rw-r--r-- | packages/backend/src/server/ServerService.ts | 3 | ||||
| -rw-r--r-- | packages/backend/src/server/WellKnownServerService.ts | 6 | ||||
| -rw-r--r-- | packages/backend/src/server/oauth/OAuth2ProviderService.ts | 71 | ||||
| -rw-r--r-- | packages/backend/test/e2e/nodeinfo.ts | 40 | ||||
| -rw-r--r-- | packages/backend/test/e2e/oauth.ts | 20 | ||||
| -rw-r--r-- | packages/backend/test/e2e/well-known.ts | 111 | ||||
| -rw-r--r-- | packages/backend/test/utils.ts | 2 |
8 files changed, 221 insertions, 34 deletions
diff --git a/packages/backend/package.json b/packages/backend/package.json index 6848d88e03..4d1e9936aa 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -68,7 +68,7 @@ "@discordapp/twemoji": "15.0.2", "@fastify/accepts": "4.3.0", "@fastify/cookie": "9.2.0", - "@fastify/cors": "8.4.2", + "@fastify/cors": "8.5.0", "@fastify/express": "2.3.0", "@fastify/http-proxy": "9.3.0", "@fastify/multipart": "8.0.0", diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index bb41ab0e42..632a7692cd 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -107,7 +107,8 @@ export class ServerService implements OnApplicationShutdown { fastify.register(this.activityPubServerService.createServer); fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.wellKnownServerService.createServer); - fastify.register(this.oauth2ProviderService.createServer); + fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); + fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' }); fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { const path = request.params.path; diff --git a/packages/backend/src/server/WellKnownServerService.ts b/packages/backend/src/server/WellKnownServerService.ts index 8fc3c96de6..c3eaf53a14 100644 --- a/packages/backend/src/server/WellKnownServerService.ts +++ b/packages/backend/src/server/WellKnownServerService.ts @@ -16,6 +16,7 @@ import * as Acct from '@/misc/acct.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; +import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; import type { FindOptionsWhere } from 'typeorm'; import type { FastifyInstance, FastifyPluginOptions } from 'fastify'; @@ -30,6 +31,7 @@ export class WellKnownServerService { private nodeinfoServerService: NodeinfoServerService, private userEntityService: UserEntityService, + private oauth2ProviderService: OAuth2ProviderService, ) { //this.createServer = this.createServer.bind(this); } @@ -87,6 +89,10 @@ export class WellKnownServerService { return { links: this.nodeinfoServerService.getLinks() }; }); + fastify.get('/.well-known/oauth-authorization-server', async () => { + return this.oauth2ProviderService.generateRFC8414(); + }); + /* TODO fastify.get('/.well-known/change-password', async (request, reply) => { }); diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index 5c18f452ce..2253078582 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -11,6 +11,7 @@ import httpLinkHeader from 'http-link-header'; import ipaddr from 'ipaddr.js'; import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize'; import oauth2Pkce from 'oauth2orize-pkce'; +import fastifyCors from '@fastify/cors'; import fastifyView from '@fastify/view'; import pug from 'pug'; import bodyParser from 'body-parser'; @@ -348,25 +349,25 @@ export class OAuth2ProviderService { })); } + // https://datatracker.ietf.org/doc/html/rfc8414.html + // https://indieauth.spec.indieweb.org/#indieauth-server-metadata + public generateRFC8414() { + return { + issuer: this.config.url, + authorization_endpoint: new URL('/oauth/authorize', this.config.url), + token_endpoint: new URL('/oauth/token', this.config.url), + scopes_supported: kinds, + response_types_supported: ['code'], + grant_types_supported: ['authorization_code'], + service_documentation: 'https://misskey-hub.net', + code_challenge_methods_supported: ['S256'], + authorization_response_iss_parameter_supported: true, + }; + } + @bindThis public async createServer(fastify: FastifyInstance): Promise<void> { - // https://datatracker.ietf.org/doc/html/rfc8414.html - // https://indieauth.spec.indieweb.org/#indieauth-server-metadata - fastify.get('/.well-known/oauth-authorization-server', async (_request, reply) => { - reply.send({ - issuer: this.config.url, - authorization_endpoint: new URL('/oauth/authorize', this.config.url), - token_endpoint: new URL('/oauth/token', this.config.url), - scopes_supported: kinds, - response_types_supported: ['code'], - grant_types_supported: ['authorization_code'], - service_documentation: 'https://misskey-hub.net', - code_challenge_methods_supported: ['S256'], - authorization_response_iss_parameter_supported: true, - }); - }); - - fastify.get('/oauth/authorize', async (request, reply) => { + fastify.get('/authorize', async (request, reply) => { const oauth2 = (request.raw as MiddlewareRequest).oauth2; if (!oauth2) { throw new Error('Unexpected lack of authorization information'); @@ -381,8 +382,7 @@ export class OAuth2ProviderService { scope: oauth2.req.scope.join(' '), }); }); - fastify.post('/oauth/decision', async () => { }); - fastify.post('/oauth/token', async () => { }); + fastify.post('/decision', async () => { }); fastify.register(fastifyView, { root: fileURLToPath(new URL('../web/views', import.meta.url)), @@ -394,7 +394,7 @@ export class OAuth2ProviderService { }); await fastify.register(fastifyExpress); - fastify.use('/oauth/authorize', this.#server.authorize(((areq, done) => { + fastify.use('/authorize', this.#server.authorize(((areq, done) => { (async (): Promise<Parameters<typeof done>> => { // This should return client/redirectURI AND the error, or // the handler can't send error to the redirection URI @@ -448,30 +448,24 @@ export class OAuth2ProviderService { return [null, clientInfo, redirectURI]; })().then(args => done(...args), err => done(err)); }) as ValidateFunctionArity2)); - fastify.use('/oauth/authorize', this.#server.errorHandler({ + fastify.use('/authorize', this.#server.errorHandler({ mode: 'indirect', modes: getQueryMode(this.config.url), })); - fastify.use('/oauth/authorize', this.#server.errorHandler()); + fastify.use('/authorize', this.#server.errorHandler()); - fastify.use('/oauth/decision', bodyParser.urlencoded({ extended: false })); - fastify.use('/oauth/decision', this.#server.decision((req, done) => { + fastify.use('/decision', bodyParser.urlencoded({ extended: false })); + fastify.use('/decision', this.#server.decision((req, done) => { const { body } = req as OAuth2DecisionRequest; this.#logger.info(`Received the decision. Cancel: ${!!body.cancel}`); req.user = body.login_token; done(null, undefined); })); - fastify.use('/oauth/decision', this.#server.errorHandler()); - - // Clients may use JSON or urlencoded - fastify.use('/oauth/token', bodyParser.urlencoded({ extended: false })); - fastify.use('/oauth/token', bodyParser.json({ strict: true })); - fastify.use('/oauth/token', this.#server.token()); - fastify.use('/oauth/token', this.#server.errorHandler()); + fastify.use('/decision', this.#server.errorHandler()); // Return 404 for any unknown paths under /oauth so that clients can know // whether a certain endpoint is supported or not. - fastify.all('/oauth/*', async (_request, reply) => { + fastify.all('/*', async (_request, reply) => { reply.code(404); reply.send({ error: { @@ -483,4 +477,17 @@ export class OAuth2ProviderService { }); }); } + + @bindThis + public async createTokenServer(fastify: FastifyInstance): Promise<void> { + fastify.register(fastifyCors); + fastify.post('', async () => { }); + + await fastify.register(fastifyExpress); + // Clients may use JSON or urlencoded + fastify.use('', bodyParser.urlencoded({ extended: false })); + fastify.use('', bodyParser.json({ strict: true })); + fastify.use('', this.#server.token()); + fastify.use('', this.#server.errorHandler()); + } } diff --git a/packages/backend/test/e2e/nodeinfo.ts b/packages/backend/test/e2e/nodeinfo.ts new file mode 100644 index 0000000000..7eed39c5ed --- /dev/null +++ b/packages/backend/test/e2e/nodeinfo.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { relativeFetch, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; + +describe('nodeinfo', () => { + let app: INestApplicationContext; + + beforeAll(async () => { + app = await startServer(); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + test('nodeinfo 2.1', async () => { + const res = await relativeFetch('nodeinfo/2.1'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const nodeInfo = await res.json() as any; + assert.strictEqual(nodeInfo.software.name, 'misskey'); + }); + + test('nodeinfo 2.0', async () => { + const res = await relativeFetch('nodeinfo/2.0'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const nodeInfo = await res.json() as any; + assert.strictEqual(nodeInfo.software.name, 'misskey'); + }); +}); diff --git a/packages/backend/test/e2e/oauth.ts b/packages/backend/test/e2e/oauth.ts index a029a0d4be..3a5e4ebdae 100644 --- a/packages/backend/test/e2e/oauth.ts +++ b/packages/backend/test/e2e/oauth.ts @@ -941,4 +941,24 @@ describe('OAuth', () => { const response = await fetch(new URL('/oauth/foo', host)); assert.strictEqual(response.status, 404); }); + + describe('CORS', () => { + test('Token endpoint should support CORS', async () => { + const response = await fetch(new URL('/oauth/token', host), { method: 'POST' }); + assert.ok(!response.ok); + assert.strictEqual(response.headers.get('Access-Control-Allow-Origin'), '*'); + }); + + test('Authorize endpoint should not support CORS', async () => { + const response = await fetch(new URL('/oauth/authorize', host), { method: 'GET' }); + assert.ok(!response.ok); + assert.ok(!response.headers.has('Access-Control-Allow-Origin')); + }); + + test('Decision endpoint should not support CORS', async () => { + const response = await fetch(new URL('/oauth/decision', host), { method: 'POST' }); + assert.ok(!response.ok); + assert.ok(!response.headers.has('Access-Control-Allow-Origin')); + }); + }); }); diff --git a/packages/backend/test/e2e/well-known.ts b/packages/backend/test/e2e/well-known.ts new file mode 100644 index 0000000000..14e32e1627 --- /dev/null +++ b/packages/backend/test/e2e/well-known.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +process.env.NODE_ENV = 'test'; + +import * as assert from 'assert'; +import { host, origin, relativeFetch, signup, startServer } from '../utils.js'; +import type { INestApplicationContext } from '@nestjs/common'; +import type * as misskey from 'misskey-js'; + +describe('.well-known', () => { + let app: INestApplicationContext; + let alice: misskey.entities.User; + + beforeAll(async () => { + app = await startServer(); + + alice = await signup({ username: 'alice' }); + }, 1000 * 60 * 2); + + afterAll(async () => { + await app.close(); + }); + + test('nodeinfo', async () => { + const res = await relativeFetch('.well-known/nodeinfo'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const nodeInfo = await res.json(); + assert.deepStrictEqual(nodeInfo, { + links: [{ + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', + href: `${origin}/nodeinfo/2.1`, + }, { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: `${origin}/nodeinfo/2.0`, + }], + }); + }); + + test('webfinger', async () => { + const preflight = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`, { + method: 'options', + headers: { + 'Access-Control-Request-Method': 'GET', + Origin: 'http://example.com', + }, + }); + assert.ok(preflight.ok); + assert.strictEqual(preflight.headers.get('Access-Control-Allow-Headers'), 'Accept'); + + const res = await relativeFetch(`.well-known/webfinger?resource=acct:alice@${host}`); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + assert.strictEqual(res.headers.get('Access-Control-Expose-Headers'), 'Vary'); + assert.strictEqual(res.headers.get('Vary'), 'Accept'); + + const webfinger = await res.json(); + + assert.deepStrictEqual(webfinger, { + subject: `acct:alice@${host}`, + links: [{ + rel: 'self', + type: 'application/activity+json', + href: `${origin}/users/${alice.id}`, + }, { + rel: 'http://webfinger.net/rel/profile-page', + type: 'text/html', + href: `${origin}/@alice`, + }, { + rel: 'http://ostatus.org/schema/1.0/subscribe', + template: `${origin}/authorize-follow?acct={uri}`, + }], + }); + }); + + test('host-meta', async () => { + const res = await relativeFetch('.well-known/host-meta'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + }); + + test('host-meta.json', async () => { + const res = await relativeFetch('.well-known/host-meta.json'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const hostMeta = await res.json(); + assert.deepStrictEqual(hostMeta, { + links: [{ + rel: 'lrdd', + type: 'application/jrd+json', + template: `${origin}/.well-known/webfinger?resource={uri}`, + }], + }); + }); + + test('oauth-authorization-server', async () => { + const res = await relativeFetch('.well-known/oauth-authorization-server'); + assert.ok(res.ok); + assert.strictEqual(res.headers.get('Access-Control-Allow-Origin'), '*'); + + const serverInfo = await res.json() as any; + assert.strictEqual(serverInfo.issuer, origin); + assert.strictEqual(serverInfo.authorization_endpoint, `${origin}/oauth/authorize`); + assert.strictEqual(serverInfo.token_endpoint, `${origin}/oauth/token`); + }); +}); diff --git a/packages/backend/test/utils.ts b/packages/backend/test/utils.ts index db7629d2c4..46b8ea9cdd 100644 --- a/packages/backend/test/utils.ts +++ b/packages/backend/test/utils.ts @@ -26,6 +26,8 @@ interface UserToken { const config = loadConfig(); export const port = config.port; +export const origin = config.url; +export const host = new URL(config.url).host; export const cookie = (me: UserToken): string => { return `token=${me.token};`; |