summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-12-27 07:10:24 +0100
committerGitHub <noreply@github.com>2023-12-27 15:10:24 +0900
commitad346b6f368f1da2874c9c575884107630f6e5c8 (patch)
treea737724d2218927c26e8c56a21d1683743f23e3e /packages
parentMerge pull request from GHSA-7pxq-6xx9-xpgm (diff)
downloadsharkey-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.json2
-rw-r--r--packages/backend/src/server/ServerService.ts3
-rw-r--r--packages/backend/src/server/WellKnownServerService.ts6
-rw-r--r--packages/backend/src/server/oauth/OAuth2ProviderService.ts71
-rw-r--r--packages/backend/test/e2e/nodeinfo.ts40
-rw-r--r--packages/backend/test/e2e/oauth.ts20
-rw-r--r--packages/backend/test/e2e/well-known.ts111
-rw-r--r--packages/backend/test/utils.ts2
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};`;