From c06521ba30dd8ae2c60722e97b84a6a017bf7aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:44:20 +0900 Subject: [PATCH] =?UTF-8?q?spec(OAuth2):=20`/oauth/api/userinfo`=E3=81=A8`?= =?UTF-8?q?/oauth/token/introspect`=E3=82=92=E5=AE=9F=E8=A3=85=20(MisskeyI?= =?UTF-8?q?O#435)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/server/ServerService.ts | 1 + .../src/server/oauth/OAuth2ProviderService.ts | 66 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index c060aa97e4..f8e63577b6 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -115,6 +115,7 @@ export class ServerService implements OnApplicationShutdown { fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.wellKnownServerService.createServer); fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' }); + fastify.register(this.oauth2ProviderService.createApiServer, { prefix: '/oauth/api' }); fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' }); fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => { diff --git a/packages/backend/src/server/oauth/OAuth2ProviderService.ts b/packages/backend/src/server/oauth/OAuth2ProviderService.ts index d4f2f009c4..939f00fae7 100644 --- a/packages/backend/src/server/oauth/OAuth2ProviderService.ts +++ b/packages/backend/src/server/oauth/OAuth2ProviderService.ts @@ -26,12 +26,13 @@ import fastifyExpress from '@fastify/express'; import { verifyChallenge } from 'pkce-challenge'; import { mf2 } from 'microformats-parser'; import { permissions as kinds } from 'misskey-js'; +import * as Redis from 'ioredis'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import type { AccessTokensRepository, UsersRepository } from '@/models/_.js'; +import type { AccessTokensRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import type { MiLocalUser } from '@/models/User.js'; @@ -40,7 +41,6 @@ import Logger from '@/logger.js'; import { StatusError } from '@/misc/status-error.js'; import type { ServerResponse } from 'node:http'; import type { FastifyInstance } from 'fastify'; -import * as Redis from 'ioredis'; // TODO: Consider migrating to @node-oauth/oauth2-server once // https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out. @@ -248,6 +248,8 @@ export class OAuth2ProviderService { private accessTokensRepository: AccessTokensRepository, @Inject(DI.usersRepository) private usersRepository: UsersRepository, + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, private idService: IdService, private cacheService: CacheService, @@ -359,6 +361,8 @@ export class OAuth2ProviderService { issuer: this.config.url, authorization_endpoint: new URL('/oauth/authorize', this.config.url), token_endpoint: new URL('/oauth/token', this.config.url), + introspection_endpoint: new URL('/oauth/token/introspect', this.config.url), + userinfo_endpoint: new URL('/oauth/api/userinfo', this.config.url), scopes_supported: kinds, response_types_supported: ['code'], grant_types_supported: ['authorization_code'], @@ -481,11 +485,69 @@ export class OAuth2ProviderService { }); } + @bindThis + public async createApiServer(fastify: FastifyInstance): Promise { + fastify.register(fastifyCors); + + fastify.get('/userinfo', async (request, reply) => { + // https://datatracker.ietf.org/doc/html/rfc6750.html#section-2.1 (case sensitive) + const token = request.headers.authorization?.startsWith('Bearer ') + ? request.headers.authorization.slice(7) + : null; + if (!token) { + reply.code(401); + return; + } + + const accessToken = await this.accessTokensRepository.findOneBy({ token }); + if (!accessToken) { + reply.code(401); + return; + } + + const user = await this.userProfilesRepository.findOneBy({ userId: accessToken.userId }); + + reply.code(200); + return { + sub: accessToken.userId, + name: accessToken.user?.name, + preferred_username: accessToken.user?.username, + profile: accessToken.user ? `${this.config.url}/@${accessToken.user.username}` : undefined, + picture: accessToken.user?.avatarUrl, + email: user?.email, + email_verified: user?.emailVerified, + updated_at: accessToken.lastUsedAt?.getTime() ?? 0 / 1000, + }; + }); + } + @bindThis public async createTokenServer(fastify: FastifyInstance): Promise { fastify.register(fastifyCors); + fastify.post('', async () => { }); + fastify.post<{ Body: Record | undefined }>('/introspect', async (request, reply) => { + const token = request.body?.['token']; + if (!token || typeof token !== 'string') { + reply.code(400); + return; + } + + const accessToken = await this.accessTokensRepository.findOneBy({ token }); + reply.code(200); + + if (!accessToken) return { active: false }; + return { + active: true, + me: accessToken.user ? `${this.config.url}/@${accessToken.user.username}` : undefined, + scope: accessToken.permission.join(' '), + client_id: accessToken.name, + user_id: accessToken.userId, + token_type: 'Bearer', + }; + }); + await fastify.register(fastifyExpress); // Clients may use JSON or urlencoded fastify.use('', bodyParser.urlencoded({ extended: false }));