spec(OAuth2): クライアント情報のDiscoveryの対応していないクライアントでも認証できるように (MisskeyIO#443)

This commit is contained in:
まっちゃとーにゅ
2024-02-12 11:35:19 +09:00
committed by GitHub
parent dea2e3183f
commit bb4583f0be
22 changed files with 969 additions and 10 deletions

View File

@@ -51,6 +51,10 @@ import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federat
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
import * as ep___admin_indieAuth_create from './endpoints/admin/indie-auth/create.js';
import * as ep___admin_indieAuth_delete from './endpoints/admin/indie-auth/delete.js';
import * as ep___admin_indieAuth_list from './endpoints/admin/indie-auth/list.js';
import * as ep___admin_indieAuth_update from './endpoints/admin/indie-auth/update.js';
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
@@ -428,6 +432,10 @@ const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federati
const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
const $admin_federation_removeAllFollowing: Provider = { provide: 'ep:admin/federation/remove-all-following', useClass: ep___admin_federation_removeAllFollowing.default };
const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federation/update-instance', useClass: ep___admin_federation_updateInstance.default };
const $admin_indieAuth_create: Provider = { provide: 'ep:admin/indie-auth/create', useClass: ep___admin_indieAuth_create.default };
const $admin_indieAuth_delete: Provider = { provide: 'ep:admin/indie-auth/delete', useClass: ep___admin_indieAuth_delete.default };
const $admin_indieAuth_list: Provider = { provide: 'ep:admin/indie-auth/list', useClass: ep___admin_indieAuth_list.default };
const $admin_indieAuth_update: Provider = { provide: 'ep:admin/indie-auth/update', useClass: ep___admin_indieAuth_update.default };
const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
@@ -809,6 +817,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_federation_refreshRemoteInstanceMetadata,
$admin_federation_removeAllFollowing,
$admin_federation_updateInstance,
$admin_indieAuth_create,
$admin_indieAuth_delete,
$admin_indieAuth_list,
$admin_indieAuth_update,
$admin_getIndexStats,
$admin_getTableStats,
$admin_getUserIps,
@@ -1184,6 +1196,10 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_federation_refreshRemoteInstanceMetadata,
$admin_federation_removeAllFollowing,
$admin_federation_updateInstance,
$admin_indieAuth_create,
$admin_indieAuth_delete,
$admin_indieAuth_list,
$admin_indieAuth_update,
$admin_getIndexStats,
$admin_getTableStats,
$admin_getUserIps,

View File

@@ -51,6 +51,10 @@ import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federat
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
import * as ep___admin_indieAuth_create from './endpoints/admin/indie-auth/create.js';
import * as ep___admin_indieAuth_delete from './endpoints/admin/indie-auth/delete.js';
import * as ep___admin_indieAuth_list from './endpoints/admin/indie-auth/list.js';
import * as ep___admin_indieAuth_update from './endpoints/admin/indie-auth/update.js';
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
@@ -426,6 +430,10 @@ const eps = [
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
['admin/federation/update-instance', ep___admin_federation_updateInstance],
['admin/indie-auth/create', ep___admin_indieAuth_create],
['admin/indie-auth/delete', ep___admin_indieAuth_delete],
['admin/indie-auth/list', ep___admin_indieAuth_list],
['admin/indie-auth/update', ep___admin_indieAuth_update],
['admin/get-index-stats', ep___admin_getIndexStats],
['admin/get-table-stats', ep___admin_getTableStats],
['admin/get-user-ips', ep___admin_getUserIps],

View File

@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { IndieAuthClientsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:indie-auth',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
name: {
type: 'string',
optional: false, nullable: true,
},
redirectUris: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', minLength: 1 },
name: { type: 'string', nullable: true },
redirectUris: {
type: 'array', minItems: 1,
items: { type: 'string' },
},
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.indieAuthClientsRepository)
private indieAuthClientsRepository: IndieAuthClientsRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const indieAuthClient = await this.indieAuthClientsRepository.insert({
id: ps.id,
createdAt: new Date(),
name: ps.name,
redirectUris: ps.redirectUris,
}).then(r => this.indieAuthClientsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
this.moderationLogService.log(me, 'createIndieAuthClient', {
clientId: indieAuthClient.id,
client: indieAuthClient,
});
return {
id: indieAuthClient.id,
createdAt: indieAuthClient.createdAt.toISOString(),
name: indieAuthClient.name,
redirectUris: indieAuthClient.redirectUris,
};
});
}
}

View File

@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { IndieAuthClientsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:indie-auth',
errors: {
noSuchIndieAuthClient: {
message: 'No such client',
code: 'NO_SUCH_CLIENT',
id: '02c4e690-af0c-4dc9-9f2f-c436c3b2782d',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string' },
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.indieAuthClientsRepository)
private indieAuthClientsRepository: IndieAuthClientsRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const client = await this.indieAuthClientsRepository.findOneBy({ id: ps.id });
if (client == null) throw new ApiError(meta.errors.noSuchIndieAuthClient);
await this.indieAuthClientsRepository.delete(client.id);
this.moderationLogService.log(me, 'deleteIndieAuthClient', {
clientId: client.id,
client: client,
});
});
}
}

View File

@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { IndieAuthClientsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'read:admin:indie-auth',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
createdAt: {
type: 'string',
optional: false, nullable: false,
format: 'date-time',
},
name: {
type: 'string',
optional: false, nullable: true,
},
redirectUris: {
type: 'array',
optional: false, nullable: false,
items: { type: 'string' },
},
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.indieAuthClientsRepository)
private indieAuthClientsRepository: IndieAuthClientsRepository,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.indieAuthClientsRepository.createQueryBuilder('client');
const clients = await query.offset(ps.offset).limit(ps.limit).getMany();
return clients.map(client => ({
id: client.id,
createdAt: client.createdAt.toISOString(),
name: client.name,
redirectUris: client.redirectUris,
}));
});
}
}

View File

@@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { IndieAuthClientsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:indie-auth',
errors: {
noSuchIndieAuthClient: {
message: 'No such client',
code: 'NO_SUCH_CLIENT',
id: 'd4f9440a-45aa-495c-af66-b4d1e339d4fc',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', minLength: 1 },
name: { type: 'string', nullable: true },
redirectUris: {
type: 'array', minItems: 1,
items: { type: 'string' },
},
},
required: ['id'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.indieAuthClientsRepository)
private indieAuthClientsRepository: IndieAuthClientsRepository,
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const client = await this.indieAuthClientsRepository.findOneBy({ id: ps.id });
if (client == null) throw new ApiError(meta.errors.noSuchIndieAuthClient);
await this.indieAuthClientsRepository.update(client.id, {
name: ps.name,
redirectUris: ps.redirectUris,
});
const updatedClient = await this.indieAuthClientsRepository.findOneByOrFail({ id: client.id });
this.moderationLogService.log(me, 'updateIndieAuthClient', {
clientId: client.id,
before: client,
after: updatedClient,
});
});
}
}

View File

@@ -32,7 +32,12 @@ 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, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type {
AccessTokensRepository,
IndieAuthClientsRepository,
UserProfilesRepository,
UsersRepository
} from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import type { MiLocalUser } from '@/models/User.js';
@@ -100,8 +105,8 @@ function validateClientId(raw: string): URL {
interface ClientInformation {
id: string;
redirectUris: string[];
name: string;
redirectUris: string[];
}
// https://indieauth.spec.indieweb.org/#client-information-discovery
@@ -246,6 +251,8 @@ export class OAuth2ProviderService {
private redisClient: Redis.Redis,
@Inject(DI.accessTokensRepository)
private accessTokensRepository: AccessTokensRepository,
@Inject(DI.indieAuthClientsRepository)
private indieAuthClientsRepository: IndieAuthClientsRepository,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
@@ -423,8 +430,10 @@ export class OAuth2ProviderService {
}
}
// Find client information from the database.
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
// Find client information from the remote.
const clientInfo = await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
const clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
// Require the redirect URI to be included in an explicit list, per
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3