tmp
This commit is contained in:
		| @@ -90,6 +90,7 @@ | ||||
| 		"date-fns": "2.30.0", | ||||
| 		"deep-email-validator": "0.1.21", | ||||
| 		"escape-regexp": "0.0.1", | ||||
| 		"express-session": "^1.17.3", | ||||
| 		"fastify": "4.18.0", | ||||
| 		"feed": "4.2.2", | ||||
| 		"file-type": "18.5.0", | ||||
| @@ -117,6 +118,8 @@ | ||||
| 		"nodemailer": "6.9.3", | ||||
| 		"nsfwjs": "2.4.2", | ||||
| 		"oauth": "0.10.0", | ||||
| 		"oauth2orize": "^1.11.1", | ||||
| 		"oauth2orize-pkce": "^0.1.2", | ||||
| 		"oidc-provider": "^8.1.1", | ||||
| 		"os-utils": "0.0.14", | ||||
| 		"otpauth": "9.1.2", | ||||
| @@ -171,6 +174,7 @@ | ||||
| 		"@types/color-convert": "2.0.0", | ||||
| 		"@types/content-disposition": "0.5.5", | ||||
| 		"@types/escape-regexp": "0.0.1", | ||||
| 		"@types/express-session": "^1.17.6", | ||||
| 		"@types/fluent-ffmpeg": "2.1.21", | ||||
| 		"@types/jest": "29.5.2", | ||||
| 		"@types/js-yaml": "4.0.5", | ||||
| @@ -183,6 +187,7 @@ | ||||
| 		"@types/node-fetch": "3.0.3", | ||||
| 		"@types/nodemailer": "6.4.8", | ||||
| 		"@types/oauth": "0.9.1", | ||||
| 		"@types/oauth2orize": "^1.8.11", | ||||
| 		"@types/pg": "8.10.2", | ||||
| 		"@types/pug": "2.0.6", | ||||
| 		"@types/punycode": "2.1.0", | ||||
|   | ||||
| @@ -1,17 +1,28 @@ | ||||
| import dns from 'node:dns/promises'; | ||||
| import { Inject, Injectable } from '@nestjs/common'; | ||||
| import Provider, { type Adapter, type Account, AdapterPayload } from 'oidc-provider'; | ||||
| import fastifyMiddie from '@fastify/middie'; | ||||
| 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 { bindThis } from '@/decorators.js'; | ||||
| import { DI } from '@/di-symbols.js'; | ||||
| import type { Config } from '@/config.js'; | ||||
| import { kinds } from '@/misc/api-permissions.js'; | ||||
| import { HttpRequestService } from '@/core/HttpRequestService.js'; | ||||
| import type { FastifyInstance } from 'fastify'; | ||||
| import fastifyCookie from '@fastify/cookie'; | ||||
| import fastifySession from '@fastify/session'; | ||||
| import type Redis from 'ioredis'; | ||||
| import oauth2Pkce from 'oauth2orize-pkce'; | ||||
| import { secureRndstr } from '@/misc/secure-rndstr.js'; | ||||
| import expressSession from 'express-session'; | ||||
| import http from 'node:http'; | ||||
| import fastifyView from '@fastify/view'; | ||||
| import pug from 'pug'; | ||||
| import { fileURLToPath } from 'node:url'; | ||||
| import { MetaService } from '@/core/MetaService.js'; | ||||
|  | ||||
| // https://indieauth.spec.indieweb.org/#client-identifier | ||||
| function validateClientId(raw: string): URL { | ||||
| @@ -63,32 +74,32 @@ function validateClientId(raw: string): URL { | ||||
| 	return url; | ||||
| } | ||||
|  | ||||
| const grantable = new Set([ | ||||
| 	'AccessToken', | ||||
| 	'AuthorizationCode', | ||||
| 	'RefreshToken', | ||||
| 	'DeviceCode', | ||||
| 	'BackchannelAuthenticationRequest', | ||||
| ]); | ||||
| // const grantable = new Set([ | ||||
| // 	'AccessToken', | ||||
| // 	'AuthorizationCode', | ||||
| // 	'RefreshToken', | ||||
| // 	'DeviceCode', | ||||
| // 	'BackchannelAuthenticationRequest', | ||||
| // ]); | ||||
|  | ||||
| const consumable = new Set([ | ||||
| 	'AuthorizationCode', | ||||
| 	'RefreshToken', | ||||
| 	'DeviceCode', | ||||
| 	'BackchannelAuthenticationRequest', | ||||
| ]); | ||||
| // const consumable = new Set([ | ||||
| // 	'AuthorizationCode', | ||||
| // 	'RefreshToken', | ||||
| // 	'DeviceCode', | ||||
| // 	'BackchannelAuthenticationRequest', | ||||
| // ]); | ||||
|  | ||||
| function grantKeyFor(id: string): string { | ||||
| 	return `grant:${id}`; | ||||
| } | ||||
| // function grantKeyFor(id: string): string { | ||||
| // 	return `grant:${id}`; | ||||
| // } | ||||
|  | ||||
| function userCodeKeyFor(userCode: string): string { | ||||
| 	return `userCode:${userCode}`; | ||||
| } | ||||
| // function userCodeKeyFor(userCode: string): string { | ||||
| // 	return `userCode:${userCode}`; | ||||
| // } | ||||
|  | ||||
| function uidKeyFor(uid: string): string { | ||||
| 	return `uid:${uid}`; | ||||
| } | ||||
| // function uidKeyFor(uid: string): string { | ||||
| // 	return `uid:${uid}`; | ||||
| // } | ||||
|  | ||||
| async function fetchFromClientId(httpRequestService: HttpRequestService, id: string): Promise<string | void> { | ||||
| 	try { | ||||
| @@ -107,179 +118,201 @@ async function fetchFromClientId(httpRequestService: HttpRequestService, id: str | ||||
| 	} | ||||
| } | ||||
|  | ||||
| class MisskeyAdapter implements Adapter { | ||||
| 	name = 'oauth2'; | ||||
| // class MisskeyAdapter implements Adapter { | ||||
| // 	name = 'oauth2'; | ||||
|  | ||||
| 	constructor(private redisClient: Redis.Redis, private httpRequestService: HttpRequestService) { } | ||||
| // 	constructor(private redisClient: Redis.Redis, private httpRequestService: HttpRequestService) { } | ||||
|  | ||||
| 	key(id: string): string { | ||||
| 		return `oauth2:${id}`; | ||||
| 	} | ||||
| // 	key(id: string): string { | ||||
| // 		return `oauth2:${id}`; | ||||
| // 	} | ||||
|  | ||||
| 	async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise<void> { | ||||
| 		console.log('oauth upsert', id, payload, expiresIn); | ||||
| // 	async upsert(id: string, payload: AdapterPayload, expiresIn: number): Promise<void> { | ||||
| // 		console.log('oauth upsert', id, payload, expiresIn); | ||||
|  | ||||
| 		const key = this.key(id); | ||||
| // 		const key = this.key(id); | ||||
|  | ||||
| 		const multi = this.redisClient.multi(); | ||||
| 		if (consumable.has(this.name)) { | ||||
| 			multi.hset(key, { payload: JSON.stringify(payload) }); | ||||
| 		} else { | ||||
| 			multi.set(key, JSON.stringify(payload)); | ||||
| 		} | ||||
| // 		const multi = this.redisClient.multi(); | ||||
| // 		if (consumable.has(this.name)) { | ||||
| // 			multi.hset(key, { payload: JSON.stringify(payload) }); | ||||
| // 		} else { | ||||
| // 			multi.set(key, JSON.stringify(payload)); | ||||
| // 		} | ||||
|  | ||||
| 		if (expiresIn) { | ||||
| 			multi.expire(key, expiresIn); | ||||
| 		} | ||||
| // 		if (expiresIn) { | ||||
| // 			multi.expire(key, expiresIn); | ||||
| // 		} | ||||
|  | ||||
| 		if (grantable.has(this.name) && payload.grantId) { | ||||
| 			const grantKey = grantKeyFor(payload.grantId); | ||||
| 			multi.rpush(grantKey, key); | ||||
| 			// if you're seeing grant key lists growing out of acceptable proportions consider using LTRIM | ||||
| 			// here to trim the list to an appropriate length | ||||
| 			const ttl = await this.redisClient.ttl(grantKey); | ||||
| 			if (expiresIn > ttl) { | ||||
| 				multi.expire(grantKey, expiresIn); | ||||
| 			} | ||||
| 		} | ||||
| // 		if (grantable.has(this.name) && payload.grantId) { | ||||
| // 			const grantKey = grantKeyFor(payload.grantId); | ||||
| // 			multi.rpush(grantKey, key); | ||||
| // 			// if you're seeing grant key lists growing out of acceptable proportions consider using LTRIM | ||||
| // 			// here to trim the list to an appropriate length | ||||
| // 			const ttl = await this.redisClient.ttl(grantKey); | ||||
| // 			if (expiresIn > ttl) { | ||||
| // 				multi.expire(grantKey, expiresIn); | ||||
| // 			} | ||||
| // 		} | ||||
|  | ||||
| 		if (payload.userCode) { | ||||
| 			const userCodeKey = userCodeKeyFor(payload.userCode); | ||||
| 			multi.set(userCodeKey, id); | ||||
| 			multi.expire(userCodeKey, expiresIn); | ||||
| 		} | ||||
| // 		if (payload.userCode) { | ||||
| // 			const userCodeKey = userCodeKeyFor(payload.userCode); | ||||
| // 			multi.set(userCodeKey, id); | ||||
| // 			multi.expire(userCodeKey, expiresIn); | ||||
| // 		} | ||||
|  | ||||
| 		if (payload.uid) { | ||||
| 			const uidKey = uidKeyFor(payload.uid); | ||||
| 			multi.set(uidKey, id); | ||||
| 			multi.expire(uidKey, expiresIn); | ||||
| 		} | ||||
| // 		if (payload.uid) { | ||||
| // 			const uidKey = uidKeyFor(payload.uid); | ||||
| // 			multi.set(uidKey, id); | ||||
| // 			multi.expire(uidKey, expiresIn); | ||||
| // 		} | ||||
|  | ||||
| 		await multi.exec(); | ||||
| 	} | ||||
| // 		await multi.exec(); | ||||
| // 	} | ||||
|  | ||||
| 	async find(id: string): Promise<void | AdapterPayload> { | ||||
| 		console.log('oauth find', id); | ||||
| // async find(id: string): Promise<void | AdapterPayload> { | ||||
| // 	console.log('oauth find', id); | ||||
|  | ||||
| 		// XXX: really? | ||||
| 		const fromRedis = await this.findRedis(id); | ||||
| 		if (fromRedis) { | ||||
| 			return fromRedis; | ||||
| 		} | ||||
| // 	// XXX: really? | ||||
| // 	const fromRedis = await this.findRedis(id); | ||||
| // 	if (fromRedis) { | ||||
| // 		return fromRedis; | ||||
| // 	} | ||||
|  | ||||
| 		// Find client information from the remote. | ||||
| 		const url = validateClientId(id); | ||||
| // 	// Find client information from the remote. | ||||
| // 	const url = validateClientId(id); | ||||
|  | ||||
| 		if (process.env.NODE_ENV !== 'test') { | ||||
| 			const lookup = await dns.lookup(url.hostname); | ||||
| 			if (ipaddr.parse(lookup.address).range() === 'loopback') { | ||||
| 				throw new Error('client_id unexpectedly resolves to loopback IP.'); | ||||
| 			} | ||||
| 		} | ||||
| // 	if (process.env.NODE_ENV !== 'test') { | ||||
| // 		const lookup = await dns.lookup(url.hostname); | ||||
| // 		if (ipaddr.parse(lookup.address).range() === 'loopback') { | ||||
| // 			throw new Error('client_id unexpectedly resolves to loopback IP.'); | ||||
| // 		} | ||||
| // 	} | ||||
|  | ||||
| 		const redirectUri = await fetchFromClientId(this.httpRequestService, id); | ||||
| 		if (!redirectUri) { | ||||
| 			// IndieAuth also implicitly allows any path under the same scheme+host, | ||||
| 			// but oidc-provider requires explicit list of uris. | ||||
| 			throw new Error('The URL of client_id must provide `redirect_uri` as HTTP Link header or HTML <link> element.'); | ||||
| 		} | ||||
| // 	const redirectUri = await fetchFromClientId(this.httpRequestService, id); | ||||
| // 	if (!redirectUri) { | ||||
| // 		// IndieAuth also implicitly allows any path under the same scheme+host, | ||||
| // 		// but oidc-provider requires explicit list of uris. | ||||
| // 		throw new Error('The URL of client_id must provide `redirect_uri` as HTTP Link header or HTML <link> element.'); | ||||
| // 	} | ||||
|  | ||||
| 		return { | ||||
| 			client_id: id, | ||||
| 			token_endpoint_auth_method: 'none', | ||||
| 			redirect_uris: [redirectUri], | ||||
| 		}; | ||||
| 	} | ||||
| // 	return { | ||||
| // 		client_id: id, | ||||
| // 		token_endpoint_auth_method: 'none', | ||||
| // 		redirect_uris: [redirectUri], | ||||
| // 	}; | ||||
| // } | ||||
|  | ||||
| 	async findRedis(id: string | null): Promise<void | AdapterPayload> { | ||||
| 		if (!id) { | ||||
| 			return; | ||||
| 		} | ||||
| // 	async findRedis(id: string | null): Promise<void | AdapterPayload> { | ||||
| // 		if (!id) { | ||||
| // 			return; | ||||
| // 		} | ||||
|  | ||||
| 		const data = consumable.has(this.name) | ||||
| 			? await this.redisClient.hgetall(this.key(id)) | ||||
| 			: await this.redisClient.get(this.key(id)); | ||||
| // 		const data = consumable.has(this.name) | ||||
| // 			? await this.redisClient.hgetall(this.key(id)) | ||||
| // 			: await this.redisClient.get(this.key(id)); | ||||
|  | ||||
| 		if (!data || (typeof data === 'object' && !Object.entries(data).length)) { | ||||
| 			return undefined; | ||||
| 		} | ||||
| // 		if (!data || (typeof data === 'object' && !Object.entries(data).length)) { | ||||
| // 			return undefined; | ||||
| // 		} | ||||
|  | ||||
| 		if (typeof data === 'string') { | ||||
| 			return JSON.parse(data); | ||||
| 		} | ||||
| 		const { payload, ...rest } = data as any; | ||||
| 		return { | ||||
| 			...rest, | ||||
| 			...JSON.parse(payload), | ||||
| 		}; | ||||
| 	} | ||||
| // 		if (typeof data === 'string') { | ||||
| // 			return JSON.parse(data); | ||||
| // 		} | ||||
| // 		const { payload, ...rest } = data as any; | ||||
| // 		return { | ||||
| // 			...rest, | ||||
| // 			...JSON.parse(payload), | ||||
| // 		}; | ||||
| // 	} | ||||
|  | ||||
| 	async findByUserCode(userCode: string): Promise<void | AdapterPayload> { | ||||
| 		console.log('oauth findByUserCode', userCode); | ||||
| 		const id = await this.redisClient.get(userCodeKeyFor(userCode)); | ||||
| 		return this.findRedis(id); | ||||
| 	} | ||||
| // 	async findByUserCode(userCode: string): Promise<void | AdapterPayload> { | ||||
| // 		console.log('oauth findByUserCode', userCode); | ||||
| // 		const id = await this.redisClient.get(userCodeKeyFor(userCode)); | ||||
| // 		return this.findRedis(id); | ||||
| // 	} | ||||
|  | ||||
| 	async findByUid(uid: string): Promise<void | AdapterPayload> { | ||||
| 		console.log('oauth findByUid', uid); | ||||
| 		const id = await this.redisClient.get(uidKeyFor(uid)); | ||||
| 		return this.findRedis(id); | ||||
| 	} | ||||
| // 	async findByUid(uid: string): Promise<void | AdapterPayload> { | ||||
| // 		console.log('oauth findByUid', uid); | ||||
| // 		const id = await this.redisClient.get(uidKeyFor(uid)); | ||||
| // 		return this.findRedis(id); | ||||
| // 	} | ||||
|  | ||||
| 	async consume(id: string): Promise<void> { | ||||
| 		console.log('oauth consume', id); | ||||
| 		await this.redisClient.hset(this.key(id), 'consumed', Math.floor(Date.now() / 1000)); | ||||
| 	} | ||||
| // 	async consume(id: string): Promise<void> { | ||||
| // 		console.log('oauth consume', id); | ||||
| // 		await this.redisClient.hset(this.key(id), 'consumed', Math.floor(Date.now() / 1000)); | ||||
| // 	} | ||||
|  | ||||
| 	async destroy(id: string): Promise<void | undefined> { | ||||
| 		console.log('oauth destroy', id); | ||||
| 		const key = this.key(id); | ||||
| 		await this.redisClient.del(key); | ||||
| 	} | ||||
| // 	async destroy(id: string): Promise<void | undefined> { | ||||
| // 		console.log('oauth destroy', id); | ||||
| // 		const key = this.key(id); | ||||
| // 		await this.redisClient.del(key); | ||||
| // 	} | ||||
|  | ||||
| 	async revokeByGrantId(grantId: string): Promise<void | undefined> { | ||||
| 		console.log('oauth revokeByGrandId', grantId); | ||||
| 		const multi = this.redisClient.multi(); | ||||
| 		const tokens = await this.redisClient.lrange(grantKeyFor(grantId), 0, -1); | ||||
| 		tokens.forEach((token) => multi.del(token)); | ||||
| 		multi.del(grantKeyFor(grantId)); | ||||
| 		await multi.exec(); | ||||
| 	} | ||||
| } | ||||
| // 	async revokeByGrantId(grantId: string): Promise<void | undefined> { | ||||
| // 		console.log('oauth revokeByGrandId', grantId); | ||||
| // 		const multi = this.redisClient.multi(); | ||||
| // 		const tokens = await this.redisClient.lrange(grantKeyFor(grantId), 0, -1); | ||||
| // 		tokens.forEach((token) => multi.del(token)); | ||||
| // 		multi.del(grantKeyFor(grantId)); | ||||
| // 		await multi.exec(); | ||||
| // 	} | ||||
| // } | ||||
|  | ||||
| // function promisify<T>(callback: T) { | ||||
| // 	return (...args: Parameters<T>) => { | ||||
|  | ||||
| // 		args[args.length - 1](); | ||||
| // 	}; | ||||
| // } | ||||
|  | ||||
| type OmitFirstElement<T extends unknown[]> = T extends [unknown, ...(infer R)] | ||||
| 	? R | ||||
| 	: []; | ||||
|  | ||||
| @Injectable() | ||||
| export class OAuth2ProviderService { | ||||
| 	#provider: Provider; | ||||
| 	// #provider: Provider; | ||||
| 	#server = oauth2orize.createServer(); | ||||
|  | ||||
| 	constructor( | ||||
| 		@Inject(DI.config) | ||||
| 		private config: Config, | ||||
| 		@Inject(DI.redis) redisClient: Redis.Redis, | ||||
| 		httpRequestService: HttpRequestService, | ||||
| 		@Inject(DI.redis) | ||||
| 		private redisClient: Redis.Redis, | ||||
| 		private httpRequestService: HttpRequestService, | ||||
| 		private metaService: MetaService, | ||||
| 	) { | ||||
| 		this.#provider = new Provider(config.url, { | ||||
| 			clientAuthMethods: ['none'], | ||||
| 			pkce: { | ||||
| 				// This is the default, but be explicit here as we announce it below | ||||
| 				methods: ['S256'], | ||||
| 			}, | ||||
| 			routes: { | ||||
| 				// defaults to '/auth' but '/authorize' is more consistent with many | ||||
| 				// other services eg. Mastodon/Twitter/Facebook/GitLab/GitHub/etc. | ||||
| 				authorization: '/authorize', | ||||
| 			}, | ||||
| 			scopes: kinds, | ||||
| 			async findAccount(ctx, id): Promise<Account | undefined> { | ||||
| 				console.log(id); | ||||
| 				return undefined; | ||||
| 			}, | ||||
| 			adapter(): MisskeyAdapter { | ||||
| 				return new MisskeyAdapter(redisClient, httpRequestService); | ||||
| 			}, | ||||
| 			async renderError(ctx, out, error): Promise<void> { | ||||
| 				console.log(error); | ||||
| 			}, | ||||
| 		}); | ||||
| 		// this.#provider = new Provider(config.url, { | ||||
| 		// 	clientAuthMethods: ['none'], | ||||
| 		// 	pkce: { | ||||
| 		// 		// This is the default, but be explicit here as we announce it below | ||||
| 		// 		methods: ['S256'], | ||||
| 		// 	}, | ||||
| 		// 	routes: { | ||||
| 		// 		// defaults to '/auth' but '/authorize' is more consistent with many | ||||
| 		// 		// other services eg. Mastodon/Twitter/Facebook/GitLab/GitHub/etc. | ||||
| 		// 		authorization: '/authorize', | ||||
| 		// 	}, | ||||
| 		// 	scopes: kinds, | ||||
| 		// 	async findAccount(ctx, id): Promise<Account | undefined> { | ||||
| 		// 		console.log(id); | ||||
| 		// 		return undefined; | ||||
| 		// 	}, | ||||
| 		// 	adapter(): MisskeyAdapter { | ||||
| 		// 		return new MisskeyAdapter(redisClient, httpRequestService); | ||||
| 		// 	}, | ||||
| 		// 	async renderError(ctx, out, error): Promise<void> { | ||||
| 		// 		console.log(error); | ||||
| 		// 	}, | ||||
| 		// }); | ||||
| 		this.#server.grant(oauth2Pkce.extensions()); | ||||
| 		this.#server.grant(oauth2orize.grant.code((client, redirectUri, user, ares, done) => { | ||||
| 			console.log(client, redirectUri, user, ares); | ||||
| 			const code = secureRndstr(32, true); | ||||
| 			done(null, code); | ||||
| 		})); | ||||
| 		this.#server.serializeClient((client, done) => done(null, client)); | ||||
| 		this.#server.deserializeClient((id, done) => done(null, id)); | ||||
| 	} | ||||
|  | ||||
| 	// Return 404 for any unknown paths under /oauth so that clients can know | ||||
| @@ -316,12 +349,65 @@ export class OAuth2ProviderService { | ||||
| 		// no way to turn it off. | ||||
| 		// For now only allow the basic OAuth endpoints, to start small and evaluate | ||||
| 		// this feature for some time, given that this is security related. | ||||
| 		fastify.get('/oauth/authorize', async () => { }); | ||||
| 		fastify.get<{ Querystring: { code_challenge?: string, code_challenge_method?: string } }>('/oauth/authorize', async (request, reply) => { | ||||
| 			console.log('HIT /oauth/authorize', request.query); | ||||
| 			if (typeof request.query.code_challenge !== 'string') { | ||||
| 				throw new Error('`code_challenge` parameter is required'); | ||||
| 			} | ||||
| 			if (request.query.code_challenge_method !== 'S256') { | ||||
| 				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, | ||||
| 			}); | ||||
| 		}); | ||||
| 		fastify.post('/oauth/token', async () => { }); | ||||
| 		fastify.get('/oauth/interaction/:uid', async () => { }); | ||||
| 		fastify.get('/oauth/interaction/:uid/login', async () => { }); | ||||
| 		// fastify.get('/oauth/interaction/:uid', async () => { }); | ||||
| 		// fastify.get('/oauth/interaction/:uid/login', async () => { }); | ||||
|  | ||||
| 		fastify.register(fastifyView, { | ||||
| 			root: fileURLToPath(new URL('../web/views', import.meta.url)), | ||||
| 			engine: { pug }, | ||||
| 			defaultContext: { | ||||
| 				version: this.config.version, | ||||
| 				config: this.config, | ||||
| 			}, | ||||
| 		}); | ||||
|  | ||||
| 		await fastify.register(fastifyMiddie); | ||||
| 		fastify.use('/oauth', this.#provider.callback()); | ||||
| 		fastify.use(expressSession({ secret: 'keyboard cat', resave: false, saveUninitialized: false }) as any); | ||||
| 		fastify.use('/oauth/authorize', this.#server.authorization((clientId, redirectUri, done) => { | ||||
| 			(async (): Promise<OmitFirstElement<Parameters<typeof done>>> => { | ||||
| 				console.log('HIT /oauth/authorize validation middleware'); | ||||
|  | ||||
| 				// Find client information from the remote. | ||||
| 				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.'); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| 				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.'); | ||||
| 				} | ||||
|  | ||||
| 				return [clientId, redirectUri]; | ||||
| 			})().then(args => done(null, ...args), err => done(err)); | ||||
| 		})); | ||||
|  | ||||
| 		// fastify.use('/oauth', this.#provider.callback()); | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										79
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										79
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -185,6 +185,9 @@ importers: | ||||
|       escape-regexp: | ||||
|         specifier: 0.0.1 | ||||
|         version: 0.0.1 | ||||
|       express-session: | ||||
|         specifier: ^1.17.3 | ||||
|         version: 1.17.3 | ||||
|       fastify: | ||||
|         specifier: 4.18.0 | ||||
|         version: 4.18.0 | ||||
| @@ -266,6 +269,12 @@ importers: | ||||
|       oauth: | ||||
|         specifier: 0.10.0 | ||||
|         version: 0.10.0 | ||||
|       oauth2orize: | ||||
|         specifier: ^1.11.1 | ||||
|         version: 1.11.1 | ||||
|       oauth2orize-pkce: | ||||
|         specifier: ^0.1.2 | ||||
|         version: 0.1.2 | ||||
|       oidc-provider: | ||||
|         specifier: ^8.1.1 | ||||
|         version: 8.1.1 | ||||
| @@ -505,6 +514,9 @@ importers: | ||||
|       '@types/escape-regexp': | ||||
|         specifier: 0.0.1 | ||||
|         version: 0.0.1 | ||||
|       '@types/express-session': | ||||
|         specifier: ^1.17.6 | ||||
|         version: 1.17.6 | ||||
|       '@types/fluent-ffmpeg': | ||||
|         specifier: 2.1.21 | ||||
|         version: 2.1.21 | ||||
| @@ -541,6 +553,9 @@ importers: | ||||
|       '@types/oauth': | ||||
|         specifier: 0.9.1 | ||||
|         version: 0.9.1 | ||||
|       '@types/oauth2orize': | ||||
|         specifier: ^1.8.11 | ||||
|         version: 1.8.11 | ||||
|       '@types/pg': | ||||
|         specifier: 8.10.2 | ||||
|         version: 8.10.2 | ||||
| @@ -7562,6 +7577,12 @@ packages: | ||||
|       '@types/range-parser': 1.2.4 | ||||
|     dev: true | ||||
|  | ||||
|   /@types/express-session@1.17.6: | ||||
|     resolution: {integrity: sha512-L6sB04HVA4HEZo1hDL65JXdZdBJtzZnCiw/P7MnO4w6746tJCNtXlHtzEASyI9ccn9zyOw6IbqQuhVa03VpO4w==} | ||||
|     dependencies: | ||||
|       '@types/express': 4.17.17 | ||||
|     dev: true | ||||
|  | ||||
|   /@types/express@4.17.17: | ||||
|     resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} | ||||
|     dependencies: | ||||
| @@ -7782,6 +7803,13 @@ packages: | ||||
|     resolution: {integrity: sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ==} | ||||
|     dev: true | ||||
|  | ||||
|   /@types/oauth2orize@1.8.11: | ||||
|     resolution: {integrity: sha512-eir5IGegpcnPuhnx1Asdxj3kDWWP/Qr1qkERMlDASwlEJM6pppVBxkW7ZvAX2H8eBHE+FP7lhg/iNlRrtNGewQ==} | ||||
|     dependencies: | ||||
|       '@types/express': 4.17.17 | ||||
|       '@types/node': 20.3.1 | ||||
|     dev: true | ||||
|  | ||||
|   /@types/oauth@0.9.1: | ||||
|     resolution: {integrity: sha512-a1iY62/a3yhZ7qH7cNUsxoI3U/0Fe9+RnuFrpTKr+0WVOzbKlSLojShCKe20aOD1Sppv+i8Zlq0pLDuTJnwS4A==} | ||||
|     dependencies: | ||||
| @@ -10434,12 +10462,10 @@ packages: | ||||
|  | ||||
|   /cookie-signature@1.0.6: | ||||
|     resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} | ||||
|     dev: true | ||||
|  | ||||
|   /cookie@0.4.2: | ||||
|     resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} | ||||
|     engines: {node: '>= 0.6'} | ||||
|     dev: true | ||||
|  | ||||
|   /cookie@0.5.0: | ||||
|     resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} | ||||
| @@ -11847,6 +11873,22 @@ packages: | ||||
|     resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==} | ||||
|     dev: false | ||||
|  | ||||
|   /express-session@1.17.3: | ||||
|     resolution: {integrity: sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==} | ||||
|     engines: {node: '>= 0.8.0'} | ||||
|     dependencies: | ||||
|       cookie: 0.4.2 | ||||
|       cookie-signature: 1.0.6 | ||||
|       debug: 2.6.9 | ||||
|       depd: 2.0.0 | ||||
|       on-headers: 1.0.2 | ||||
|       parseurl: 1.3.3 | ||||
|       safe-buffer: 5.2.1 | ||||
|       uid-safe: 2.1.5 | ||||
|     transitivePeerDependencies: | ||||
|       - supports-color | ||||
|     dev: false | ||||
|  | ||||
|   /express@4.18.2: | ||||
|     resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} | ||||
|     engines: {node: '>= 0.10.0'} | ||||
| @@ -16276,6 +16318,21 @@ packages: | ||||
|     resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} | ||||
|     dev: false | ||||
|  | ||||
|   /oauth2orize-pkce@0.1.2: | ||||
|     resolution: {integrity: sha512-grto2UYhXHi9GLE3IBgBBbV87xci55+bCyjpVuxKyzol6I5Rg0K1MiTuXE+JZk54R86SG2wqXODMiZYHraPpxw==} | ||||
|     dev: false | ||||
|  | ||||
|   /oauth2orize@1.11.1: | ||||
|     resolution: {integrity: sha512-9dSx/Gwm0J2Rvj4RH9+h7iXVnRXZ6biwWRgb2dCeQhCosODS0nYdM9I/G7BUGsjbgn0pHjGcn1zcCRtzj2SlRA==} | ||||
|     engines: {node: '>= 0.4.0'} | ||||
|     dependencies: | ||||
|       debug: 2.6.9 | ||||
|       uid2: 0.0.4 | ||||
|       utils-merge: 1.0.1 | ||||
|     transitivePeerDependencies: | ||||
|       - supports-color | ||||
|     dev: false | ||||
|  | ||||
|   /oauth@0.10.0: | ||||
|     resolution: {integrity: sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q==} | ||||
|     dev: false | ||||
| @@ -16427,7 +16484,6 @@ packages: | ||||
|   /on-headers@1.0.2: | ||||
|     resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} | ||||
|     engines: {node: '>= 0.8'} | ||||
|     dev: true | ||||
|  | ||||
|   /once@1.4.0: | ||||
|     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} | ||||
| @@ -17777,6 +17833,11 @@ packages: | ||||
|     resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} | ||||
|     dev: true | ||||
|  | ||||
|   /random-bytes@1.0.0: | ||||
|     resolution: {integrity: sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==} | ||||
|     engines: {node: '>= 0.8'} | ||||
|     dev: false | ||||
|  | ||||
|   /random-seed@0.3.0: | ||||
|     resolution: {integrity: sha512-y13xtn3kcTlLub3HKWXxJNeC2qK4mB59evwZ5EkeRlolx+Bp2ztF7LbcZmyCnOqlHQrLnfuNbi1sVmm9lPDlDA==} | ||||
|     engines: {node: '>= 0.6.0'} | ||||
| @@ -20173,6 +20234,17 @@ packages: | ||||
|     dev: true | ||||
|     optional: true | ||||
|  | ||||
|   /uid-safe@2.1.5: | ||||
|     resolution: {integrity: sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==} | ||||
|     engines: {node: '>= 0.8'} | ||||
|     dependencies: | ||||
|       random-bytes: 1.0.0 | ||||
|     dev: false | ||||
|  | ||||
|   /uid2@0.0.4: | ||||
|     resolution: {integrity: sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==} | ||||
|     dev: false | ||||
|  | ||||
|   /uid@2.0.2: | ||||
|     resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} | ||||
|     engines: {node: '>=8'} | ||||
| @@ -20453,7 +20525,6 @@ packages: | ||||
|   /utils-merge@1.0.1: | ||||
|     resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} | ||||
|     engines: {node: '>= 0.4.0'} | ||||
|     dev: true | ||||
|  | ||||
|   /uuid@3.4.0: | ||||
|     resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Kagami Sascha Rosylight
					Kagami Sascha Rosylight