diff --git a/packages/backend/package.json b/packages/backend/package.json index f6912d094..2e4f2b532 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -61,6 +61,7 @@ "@fastify/accepts": "4.2.0", "@fastify/cookie": "8.3.0", "@fastify/cors": "8.3.0", + "@fastify/express": "^2.3.0", "@fastify/http-proxy": "9.2.1", "@fastify/multipart": "7.7.0", "@fastify/static": "6.10.2", @@ -78,6 +79,7 @@ "autwh": "0.1.0", "bcryptjs": "2.4.3", "blurhash": "2.0.5", + "body-parser": "^1.20.2", "bullmq": "4.1.0", "cacheable-lookup": "7.0.0", "cbor": "9.0.0", @@ -170,6 +172,7 @@ "@types/accepts": "1.3.5", "@types/archiver": "5.3.2", "@types/bcryptjs": "2.4.2", + "@types/body-parser": "^1.19.2", "@types/cbor": "6.0.0", "@types/color-convert": "2.0.0", "@types/content-disposition": "0.5.5", diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index e63bcf025..191f93609 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -5,7 +5,7 @@ import fastifyMiddie, { IncomingMessageExtended } from '@fastify/middie'; import { JSDOM } from 'jsdom'; import parseLinkHeader from 'parse-link-header'; import ipaddr from 'ipaddr.js'; -import oauth2orize from 'oauth2orize'; +import oauth2orize, { OAuth2 } from 'oauth2orize'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; @@ -23,6 +23,9 @@ import fastifyView from '@fastify/view'; import pug from 'pug'; import { fileURLToPath } from 'node:url'; import { MetaService } from '@/core/MetaService.js'; +import fastifyFormbody from '@fastify/formbody'; +import bodyParser from 'body-parser'; +import fastifyExpress from '@fastify/express'; // https://indieauth.spec.indieweb.org/#client-identifier function validateClientId(raw: string): URL { @@ -58,16 +61,11 @@ function validateClientId(raw: string): URL { throw new Error('client_id must not contain a username or a password'); } - // MUST NOT contain a port - if (url.port) { - throw new Error('client_id must not contain a port'); - } + // (MAY contain a port) // host names MUST be domain names or a loopback interface and MUST NOT be // IPv4 or IPv6 addresses except for IPv4 127.0.0.1 or IPv6 [::1]. - // (But in https://indieauth.spec.indieweb.org/#redirect-url we need to only - // fetch non-loopback URLs, so exclude them here.) - if (!url.hostname.match(/\.\w+$/)) { + if (!url.hostname.match(/\.\w+$/) && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) { throw new Error('client_id must have a domain name as a host name'); } @@ -351,6 +349,9 @@ export class OAuth2ProviderService { // this feature for some time, given that this is security related. fastify.get<{ Querystring: { code_challenge?: string, code_challenge_method?: string } }>('/oauth/authorize', async (request, reply) => { console.log('HIT /oauth/authorize', request.query); + const oauth2 = (request.raw as any).oauth2 as (OAuth2 | undefined); + console.log(oauth2); + if (typeof request.query.code_challenge !== 'string') { throw new Error('`code_challenge` parameter is required'); } @@ -358,17 +359,11 @@ export class OAuth2ProviderService { throw new Error('`code_challenge_method` parameter must be set as S256'); } - const meta = await this.metaService.fetch(); - return await reply.view('base', { - img: meta.bannerUrl, - title: meta.name ?? 'Misskey', - instanceName: meta.name ?? 'Misskey', - url: this.config.url, - desc: meta.description, - icon: meta.iconUrl, - themeColor: meta.themeColor, + return await reply.view('oauth', { + transactionId: oauth2?.transactionID, }); }); + fastify.post('/oauth/decision', async (request, reply) => { }); fastify.post('/oauth/token', async () => { }); // fastify.get('/oauth/interaction/:uid', async () => { }); // fastify.get('/oauth/interaction/:uid/login', async () => { }); @@ -382,7 +377,7 @@ export class OAuth2ProviderService { }, }); - await fastify.register(fastifyMiddie); + await fastify.register(fastifyExpress); fastify.use(expressSession({ secret: 'keyboard cat', resave: false, saveUninitialized: false }) as any); fastify.use('/oauth/authorize', this.#server.authorization((clientId, redirectUri, done) => { (async (): Promise>> => { @@ -392,13 +387,8 @@ export class OAuth2ProviderService { const clientUrl = validateClientId(clientId); const redirectUrl = new URL(redirectUri); - if (process.env.NODE_ENV !== 'test') { - const lookup = await dns.lookup(clientUrl.hostname); - if (ipaddr.parse(lookup.address).range() === 'loopback') { - throw new Error('client_id unexpectedly resolves to loopback IP.'); - } - } - + // https://indieauth.spec.indieweb.org/#authorization-request + // Allow same-origin redirection if (redirectUrl.protocol !== clientUrl.protocol || redirectUrl.host !== clientUrl.host) { // TODO: allow more redirect_uri by Client Information Discovery throw new Error('cross-origin redirect_uri is not supported yet.'); @@ -407,6 +397,13 @@ export class OAuth2ProviderService { return [clientId, redirectUri]; })().then(args => done(null, ...args), err => done(err)); })); + // for (const middleware of this.#server.decision()) { + + fastify.use('/oauth/decision', bodyParser.urlencoded({ + extend: false, + })); + fastify.use('/oauth/decision', this.#server.decision()); + // } // fastify.use('/oauth', this.#provider.callback()); } diff --git a/packages/backend/src/server/web/views/oauth.pug b/packages/backend/src/server/web/views/oauth.pug new file mode 100644 index 000000000..717336f5e --- /dev/null +++ b/packages/backend/src/server/web/views/oauth.pug @@ -0,0 +1,10 @@ +extends ./base + +block meta + //- Should be removed by the page when it loads, so that it won't leak when + //- user navigates away via the navigation bar + //- XXX: Remove navigation bar in auth page? + meta(name='misskey:oauth:transaction-id' content=transactionId) + meta(name='misskey:oauth:client-id' content=clientId) + meta(name='misskey:oauth:scope' content=scope) + meta(name='misskey:oauth:redirection-uri' content=redirectionUri) diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue index 16e44ec61..e40b655fb 100644 --- a/packages/frontend/src/components/MkButton.vue +++ b/packages/frontend/src/components/MkButton.vue @@ -4,6 +4,7 @@ ref="el" class="_button" :class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]" :type="type" + :name="name" @click="emit('click', $event)" @mousedown="onMousedown" > @@ -44,6 +45,7 @@ const props = defineProps<{ large?: boolean; transparent?: boolean; asLike?: boolean; + name?: string; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue new file mode 100644 index 000000000..0e708df5d --- /dev/null +++ b/packages/frontend/src/pages/oauth.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index fe9bc5938..c6d14d153 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -254,6 +254,9 @@ export const routes = [{ icon: 'icon', permission: 'permission', }, +}, { + path: '/oauth/authorize', + component: page(() => import('./pages/oauth.vue')), }, { path: '/tags/:tag', component: page(() => import('./pages/tag.vue')), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e52a4254c..2c21152d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,6 +98,9 @@ importers: '@fastify/cors': specifier: 8.3.0 version: 8.3.0 + '@fastify/express': + specifier: ^2.3.0 + version: 2.3.0 '@fastify/http-proxy': specifier: 9.2.1 version: 9.2.1(bufferutil@4.0.7)(utf-8-validate@6.0.3) @@ -149,6 +152,9 @@ importers: blurhash: specifier: 2.0.5 version: 2.0.5 + body-parser: + specifier: ^1.20.2 + version: 1.20.2 bullmq: specifier: 4.1.0 version: 4.1.0 @@ -502,6 +508,9 @@ importers: '@types/bcryptjs': specifier: 2.4.2 version: 2.4.2 + '@types/body-parser': + specifier: ^1.19.2 + version: 1.19.2 '@types/cbor': specifier: 6.0.0 version: 6.0.0 @@ -4820,6 +4829,15 @@ packages: resolution: {integrity: sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ==} dev: false + /@fastify/express@2.3.0: + resolution: {integrity: sha512-jvvjlPPCfJsSHfF6tQDyARJ3+c3xXiqcxVZu6bi3xMWCWB3fl07vrjFDeaqnwqKhLZ9+m6cog5dw7gIMKEsTnQ==} + dependencies: + express: 4.18.2 + fastify-plugin: 4.5.0 + transitivePeerDependencies: + - supports-color + dev: false + /@fastify/fast-json-stringify-compiler@4.3.0: resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} dependencies: @@ -8868,7 +8886,6 @@ packages: /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - dev: true /array-includes@3.1.6: resolution: {integrity: sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==} @@ -9464,7 +9481,26 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: true + + /body-parser@1.20.2: + resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false /boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -11730,7 +11766,6 @@ packages: /etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - dev: true /event-stream@3.3.4: resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} @@ -11926,7 +11961,6 @@ packages: vary: 1.1.2 transitivePeerDependencies: - supports-color - dev: true /ext-list@2.2.2: resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==} @@ -12262,7 +12296,6 @@ packages: unpipe: 1.0.0 transitivePeerDependencies: - supports-color - dev: true /find-cache-dir@2.1.0: resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} @@ -15570,7 +15603,6 @@ packages: /merge-descriptors@1.0.1: resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} - dev: true /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -15630,7 +15662,6 @@ packages: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} hasBin: true - dev: true /mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} @@ -16360,7 +16391,6 @@ packages: /object-inspect@1.12.2: resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} - dev: true /object-is@1.1.5: resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==} @@ -16860,7 +16890,6 @@ packages: /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} - dev: true /path-to-regexp@1.8.0: resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} @@ -17773,7 +17802,6 @@ packages: engines: {node: '>=0.6'} dependencies: side-channel: 1.0.4 - dev: true /qs@6.11.1: resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==} @@ -17848,7 +17876,6 @@ packages: /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - dev: true /ratelimiter@3.4.1: resolution: {integrity: sha512-5FJbRW/Jkkdk29ksedAfWFkQkhbUrMx3QJGwMKAypeIiQf4yrLW+gtPKZiaWt4zPrtw1uGufOjGO7UGM6VllsQ==} @@ -17862,7 +17889,6 @@ packages: http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - dev: true /raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} @@ -18710,7 +18736,6 @@ packages: statuses: 2.0.1 transitivePeerDependencies: - supports-color - dev: true /serve-favicon@2.5.0: resolution: {integrity: sha512-FMW2RvqNr03x+C0WxTyu6sOv21oOjkq5j8tjquWccwa6ScNyGFOGJVpuS1NmTVGBAHS07xnSKotgf2ehQmf9iA==} @@ -18733,7 +18758,6 @@ packages: send: 0.18.0 transitivePeerDependencies: - supports-color - dev: true /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -18839,7 +18863,6 @@ packages: call-bind: 1.0.2 get-intrinsic: 1.2.0 object-inspect: 1.12.2 - dev: true /siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}