From adabff708d1cb798f7f2df0854994d0673794685 Mon Sep 17 00:00:00 2001
From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 3 Oct 2024 20:34:05 +0900
Subject: [PATCH] =?UTF-8?q?=E6=AC=A1=E3=81=AE=E5=87=A6=E7=90=86=E3=82=92si?=
=?UTF-8?q?gnin=20api=E3=81=8B=E3=82=89=E8=AA=AD=E3=81=BF=E5=8F=96?=
=?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/core/entities/UserEntityService.ts | 13 +-
.../backend/src/models/json-schema/user.ts | 27 +-
.../src/server/api/SigninApiService.ts | 77 ++++-
.../src/components/MkSignin.passkey.vue | 2 -
.../src/components/MkSignin.password.vue | 14 +-
packages/frontend/src/components/MkSignin.vue | 292 ++++++++++--------
.../src/components/MkSigninDialog.vue | 1 -
packages/misskey-js/etc/misskey-js.api.md | 2 +-
packages/misskey-js/src/autogen/types.ts | 9 +-
packages/misskey-js/src/entities.ts | 2 +-
10 files changed, 255 insertions(+), 184 deletions(-)
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index 69e2d6fc89..c9939adf11 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -545,11 +545,6 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
- twoFactorEnabled: profile!.twoFactorEnabled,
- usePasswordLessLogin: profile!.usePasswordLessLogin,
- securityKeys: profile!.twoFactorEnabled
- ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
- : false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
name: role.name,
@@ -564,6 +559,14 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
+ ...(isDetailed && (isMe || iAmModerator) ? {
+ twoFactorEnabled: profile!.twoFactorEnabled,
+ usePasswordLessLogin: profile!.usePasswordLessLogin,
+ securityKeys: profile!.twoFactorEnabled
+ ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
+ : false,
+ } : {}),
+
...(isDetailed && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 16c8a5a097..c87100b59f 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -346,21 +346,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
- twoFactorEnabled: {
- type: 'boolean',
- nullable: false, optional: false,
- default: false,
- },
- usePasswordLessLogin: {
- type: 'boolean',
- nullable: false, optional: false,
- default: false,
- },
- securityKeys: {
- type: 'boolean',
- nullable: false, optional: false,
- default: false,
- },
roles: {
type: 'array',
nullable: false, optional: false,
@@ -382,6 +367,18 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: false, optional: true,
},
+ twoFactorEnabled: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
+ usePasswordLessLogin: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
+ securityKeys: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
//#region relations
isFollowing: {
type: 'boolean',
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 2ccc75da00..83abe3c5e7 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -12,6 +12,7 @@ import type {
MiMeta,
SigninsRepository,
UserProfilesRepository,
+ UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
@@ -25,9 +26,18 @@ import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
-import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
+import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
+type SigninErrorResponse = {
+ id: string;
+ next?: 'captcha' | 'password' | 'totp';
+} | {
+ id: string;
+ next: 'passkey';
+ authRequest: PublicKeyCredentialRequestOptionsJSON;
+};
+
@Injectable()
export class SigninApiService {
constructor(
@@ -43,6 +53,9 @@ export class SigninApiService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
@@ -60,7 +73,7 @@ export class SigninApiService {
request: FastifyRequest<{
Body: {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string;
@@ -79,7 +92,7 @@ export class SigninApiService {
const password = body['password'];
const token = body['token'];
- function error(status: number, error: { id: string }) {
+ function error(status: number, error: SigninErrorResponse) {
reply.code(status);
return { error };
}
@@ -103,11 +116,6 @@ export class SigninApiService {
return;
}
- if (typeof password !== 'string') {
- reply.code(400);
- return;
- }
-
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
@@ -132,11 +140,36 @@ export class SigninApiService {
}
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+ const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
+
+ if (password == null) {
+ reply.code(403);
+ if (profile.twoFactorEnabled) {
+ return {
+ error: {
+ id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
+ next: 'password',
+ },
+ } satisfies { error: SigninErrorResponse };
+ } else {
+ return {
+ error: {
+ id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
+ next: 'captcha',
+ },
+ } satisfies { error: SigninErrorResponse };
+ }
+ }
+
+ if (typeof password !== 'string') {
+ reply.code(400);
+ return;
+ }
// Compare password
const same = await bcrypt.compare(password, profile.password!);
- const fail = async (status?: number, failure?: { id: string }) => {
+ const fail = async (status?: number, failure?: SigninErrorResponse) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
@@ -217,7 +250,7 @@ export class SigninApiService {
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
});
}
- } else {
+ } else if (securityKeysAvailable) {
if (!same && !profile.usePasswordLessLogin) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
@@ -226,8 +259,28 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
- reply.code(200);
- return authRequest;
+ reply.code(403);
+ return {
+ error: {
+ id: '06e661b9-8146-4ae3-bde5-47138c0ae0c4',
+ next: 'passkey',
+ authRequest,
+ },
+ } satisfies { error: SigninErrorResponse };
+ } else {
+ if (!same || !profile.twoFactorEnabled) {
+ return await fail(403, {
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+ });
+ } else {
+ reply.code(403);
+ return {
+ error: {
+ id: '144ff4f8-bd6c-41bc-82c3-b672eb09efbf',
+ next: 'totp',
+ },
+ } satisfies { error: SigninErrorResponse };
+ }
}
// never get here
}
diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue
index e9a97812e1..297db95f89 100644
--- a/packages/frontend/src/components/MkSignin.passkey.vue
+++ b/packages/frontend/src/components/MkSignin.passkey.vue
@@ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only