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
|
@ -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};`;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue