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
This commit is contained in:
		
							parent
							
								
									82822e29d9
								
							
						
					
					
						commit
						544b8106b2
					
				
					 10 changed files with 238 additions and 9 deletions
				
			
		
							
								
								
									
										11
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -12,6 +12,17 @@
 | 
			
		|||
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
## 2023.x.x (unreleased)
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
-
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
- 
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Enhance: `oauth/token`エンドポイントのCORS対応
 | 
			
		||||
 | 
			
		||||
## 2023.12.1
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -66,7 +66,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",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -110,7 +110,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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) => {
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -31,6 +31,22 @@ export class OAuth2ProviderService {
 | 
			
		|||
		private config: Config,
 | 
			
		||||
	) { }
 | 
			
		||||
 | 
			
		||||
	// 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
 | 
			
		||||
| 
						 | 
				
			
			@ -151,4 +167,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());
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								packages/backend/test/e2e/nodeinfo.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								packages/backend/test/e2e/nodeinfo.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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');
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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'));
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										111
									
								
								packages/backend/test/e2e/well-known.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								packages/backend/test/e2e/well-known.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -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`);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -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};`;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -80,8 +80,8 @@ importers:
 | 
			
		|||
        specifier: 9.2.0
 | 
			
		||||
        version: 9.2.0
 | 
			
		||||
      '@fastify/cors':
 | 
			
		||||
        specifier: 8.4.2
 | 
			
		||||
        version: 8.4.2
 | 
			
		||||
        specifier: 8.5.0
 | 
			
		||||
        version: 8.5.0
 | 
			
		||||
      '@fastify/express':
 | 
			
		||||
        specifier: 2.3.0
 | 
			
		||||
        version: 2.3.0
 | 
			
		||||
| 
						 | 
				
			
			@ -4230,11 +4230,11 @@ packages:
 | 
			
		|||
      fastify-plugin: 4.5.0
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@fastify/cors@8.4.2:
 | 
			
		||||
    resolution: {integrity: sha512-IVynbcPG9eWiJ0P/A1B+KynmiU/yTYbu3ooBUSIeHfca/N1XLb9nIJVCws+YTr2q63MA8Y6QLeXQczEv4npM9g==}
 | 
			
		||||
  /@fastify/cors@8.5.0:
 | 
			
		||||
    resolution: {integrity: sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      fastify-plugin: 4.5.0
 | 
			
		||||
      mnemonist: 0.39.5
 | 
			
		||||
      mnemonist: 0.39.6
 | 
			
		||||
    dev: false
 | 
			
		||||
 | 
			
		||||
  /@fastify/deepmerge@1.3.0:
 | 
			
		||||
| 
						 | 
				
			
			@ -7221,7 +7221,11 @@ packages:
 | 
			
		|||
      ts-dedent: 2.2.0
 | 
			
		||||
      type-fest: 2.19.0
 | 
			
		||||
      vue: 3.3.12(typescript@5.3.3)
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
      vue-component-type-helpers: 1.8.26
 | 
			
		||||
=======
 | 
			
		||||
      vue-component-type-helpers: 1.8.27
 | 
			
		||||
>>>>>>> ad346b6f3 (feat(backend/oauth): allow CORS for token endpoint (#12814))
 | 
			
		||||
    transitivePeerDependencies:
 | 
			
		||||
      - encoding
 | 
			
		||||
      - supports-color
 | 
			
		||||
| 
						 | 
				
			
			@ -15138,8 +15142,8 @@ packages:
 | 
			
		|||
      ufo: 1.1.2
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /mnemonist@0.39.5:
 | 
			
		||||
    resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==}
 | 
			
		||||
  /mnemonist@0.39.6:
 | 
			
		||||
    resolution: {integrity: sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==}
 | 
			
		||||
    dependencies:
 | 
			
		||||
      obliterator: 2.0.4
 | 
			
		||||
    dev: false
 | 
			
		||||
| 
						 | 
				
			
			@ -19628,8 +19632,13 @@ packages:
 | 
			
		|||
  /vscode-textmate@8.0.0:
 | 
			
		||||
    resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
 | 
			
		||||
 | 
			
		||||
<<<<<<< HEAD
 | 
			
		||||
  /vue-component-type-helpers@1.8.26:
 | 
			
		||||
    resolution: {integrity: sha512-CIwb7s8cqUuPpHDk+0DY8EJ/x8tzdzqw8ycX8hhw1GnbngTgSsIceHAqrrLjmv8zXi+j5XaiqYRQMw8sKyyjkw==}
 | 
			
		||||
=======
 | 
			
		||||
  /vue-component-type-helpers@1.8.27:
 | 
			
		||||
    resolution: {integrity: sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==}
 | 
			
		||||
>>>>>>> ad346b6f3 (feat(backend/oauth): allow CORS for token endpoint (#12814))
 | 
			
		||||
    dev: true
 | 
			
		||||
 | 
			
		||||
  /vue-component-type-helpers@1.8.4:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue