spec(OAuth2): `/oauth/api/userinfo`と`/oauth/token/introspect`を実装 (MisskeyIO#435)
This commit is contained in:
parent
73a4b16e14
commit
c06521ba30
|
@ -115,6 +115,7 @@ export class ServerService implements OnApplicationShutdown {
|
||||||
fastify.register(this.nodeinfoServerService.createServer);
|
fastify.register(this.nodeinfoServerService.createServer);
|
||||||
fastify.register(this.wellKnownServerService.createServer);
|
fastify.register(this.wellKnownServerService.createServer);
|
||||||
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
|
fastify.register(this.oauth2ProviderService.createServer, { prefix: '/oauth' });
|
||||||
|
fastify.register(this.oauth2ProviderService.createApiServer, { prefix: '/oauth/api' });
|
||||||
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
|
fastify.register(this.oauth2ProviderService.createTokenServer, { prefix: '/oauth/token' });
|
||||||
|
|
||||||
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
fastify.get<{ Params: { path: string }; Querystring: { static?: any; badge?: any; }; }>('/emoji/:path(.*)', async (request, reply) => {
|
||||||
|
|
|
@ -26,12 +26,13 @@ import fastifyExpress from '@fastify/express';
|
||||||
import { verifyChallenge } from 'pkce-challenge';
|
import { verifyChallenge } from 'pkce-challenge';
|
||||||
import { mf2 } from 'microformats-parser';
|
import { mf2 } from 'microformats-parser';
|
||||||
import { permissions as kinds } from 'misskey-js';
|
import { permissions as kinds } from 'misskey-js';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { bindThis } from '@/decorators.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 { IdService } from '@/core/IdService.js';
|
||||||
import { CacheService } from '@/core/CacheService.js';
|
import { CacheService } from '@/core/CacheService.js';
|
||||||
import type { MiLocalUser } from '@/models/User.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 { StatusError } from '@/misc/status-error.js';
|
||||||
import type { ServerResponse } from 'node:http';
|
import type { ServerResponse } from 'node:http';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import * as Redis from 'ioredis';
|
|
||||||
|
|
||||||
// TODO: Consider migrating to @node-oauth/oauth2-server once
|
// TODO: Consider migrating to @node-oauth/oauth2-server once
|
||||||
// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
|
// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
|
||||||
|
@ -248,6 +248,8 @@ export class OAuth2ProviderService {
|
||||||
private accessTokensRepository: AccessTokensRepository,
|
private accessTokensRepository: AccessTokensRepository,
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
@Inject(DI.userProfilesRepository)
|
||||||
|
private userProfilesRepository: UserProfilesRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
|
@ -359,6 +361,8 @@ export class OAuth2ProviderService {
|
||||||
issuer: this.config.url,
|
issuer: this.config.url,
|
||||||
authorization_endpoint: new URL('/oauth/authorize', this.config.url),
|
authorization_endpoint: new URL('/oauth/authorize', this.config.url),
|
||||||
token_endpoint: new URL('/oauth/token', 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,
|
scopes_supported: kinds,
|
||||||
response_types_supported: ['code'],
|
response_types_supported: ['code'],
|
||||||
grant_types_supported: ['authorization_code'],
|
grant_types_supported: ['authorization_code'],
|
||||||
|
@ -481,11 +485,69 @@ export class OAuth2ProviderService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public async createApiServer(fastify: FastifyInstance): Promise<void> {
|
||||||
|
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
|
@bindThis
|
||||||
public async createTokenServer(fastify: FastifyInstance): Promise<void> {
|
public async createTokenServer(fastify: FastifyInstance): Promise<void> {
|
||||||
fastify.register(fastifyCors);
|
fastify.register(fastifyCors);
|
||||||
|
|
||||||
fastify.post('', async () => { });
|
fastify.post('', async () => { });
|
||||||
|
|
||||||
|
fastify.post<{ Body: Record<string, unknown> | 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);
|
await fastify.register(fastifyExpress);
|
||||||
// Clients may use JSON or urlencoded
|
// Clients may use JSON or urlencoded
|
||||||
fastify.use('', bodyParser.urlencoded({ extended: false }));
|
fastify.use('', bodyParser.urlencoded({ extended: false }));
|
||||||
|
|
Loading…
Reference in New Issue