summaryrefslogtreecommitdiff
path: root/packages/backend/src/server/api/endpoints
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2023-02-20 16:40:24 +0900
committerGitHub <noreply@github.com>2023-02-20 16:40:24 +0900
commit980bf1306e2d097782958f024a86391fc28278a0 (patch)
treee1190b5fa0b8b18a425dee0dcdbf580ce2235c5f /packages/backend/src/server/api/endpoints
parentrefactor: 型エラー修正 / Fix type errors backend (#9983) (diff)
downloadsharkey-980bf1306e2d097782958f024a86391fc28278a0.tar.gz
sharkey-980bf1306e2d097782958f024a86391fc28278a0.tar.bz2
sharkey-980bf1306e2d097782958f024a86391fc28278a0.zip
:art: 2FA設定のデザイン向上 / セキュリティキーの名前を変更できるように (#9985)
* wip * fix * wip * wip * :v: * rename key * :art: * update CHANGELOG.md * パスワードレスログインの判断はサーバーで * 日本語 * 日本語 * 日本語 * 日本語 * :v: * fix * refactor * トークン→確認コード * fix password-less / qr click * use otpauth * 日本語 * autocomplete * パスワードレス設定は外に出す * :art: * :art: --------- Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Diffstat (limited to 'packages/backend/src/server/api/endpoints')
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/done.ts28
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/key-done.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/password-less.ts47
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/register.ts17
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/unregister.ts12
-rw-r--r--packages/backend/src/server/api/endpoints/i/2fa/update-key.ts78
7 files changed, 185 insertions, 17 deletions
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/done.ts b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
index ec9ac1ef90..6c31075e05 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/done.ts
@@ -1,7 +1,10 @@
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
export const meta = {
@@ -22,8 +25,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const token = ps.token.replace(/\s/g, '');
@@ -34,13 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('二段階認証の設定が開始されていません');
}
- const verified = (speakeasy as any).totp.verify({
- secret: profile.twoFactorTempSecret,
- encoding: 'base32',
- token: token,
+ const delta = OTPAuth.TOTP.validate({
+ secret: OTPAuth.Secret.fromBase32(profile.twoFactorTempSecret),
+ digits: 6,
+ token,
+ window: 1,
});
- if (!verified) {
+ if (delta === null) {
throw new Error('not verified');
}
@@ -48,6 +58,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
twoFactorSecret: profile.twoFactorTempSecret,
twoFactorEnabled: true,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
index 6e0849f2b2..ad33398da6 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/key-done.ts
@@ -25,7 +25,7 @@ export const paramDef = {
attestationObject: { type: 'string' },
password: { type: 'string' },
challengeId: { type: 'string' },
- name: { type: 'string' },
+ name: { type: 'string', minLength: 1, maxLength: 30 },
},
required: ['clientDataJSON', 'attestationObject', 'password', 'challengeId', 'name'],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
index 0655a86350..0ee9f556a8 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/password-less.ts
@@ -1,12 +1,23 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UserProfilesRepository } from '@/models/index.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../../error.js';
export const meta = {
requireCredential: true,
secure: true,
+
+ errors: {
+ noKey: {
+ message: 'No security key.',
+ code: 'NO_SECURITY_KEY',
+ id: 'f9c54d7f-d4c2-4d3c-9a8g-a70daac86512',
+ },
+ },
} as const;
export const paramDef = {
@@ -23,11 +34,45 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
+ if (ps.value === true) {
+ // セキュリティキーがなければパスワードレスを有効にはできない
+ const keyCount = await this.userSecurityKeysRepository.count({
+ where: {
+ userId: me.id,
+ },
+ select: {
+ id: true,
+ name: true,
+ lastUsed: true,
+ },
+ });
+
+ if (keyCount === 0) {
+ await this.userProfilesRepository.update(me.id, {
+ usePasswordLessLogin: false,
+ });
+
+ throw new ApiError(meta.errors.noKey);
+ }
+ }
+
await this.userProfilesRepository.update(me.id, {
usePasswordLessLogin: ps.value,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/register.ts b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
index a539c5c221..eb4d7f9c14 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/register.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/register.ts
@@ -1,5 +1,5 @@
import bcrypt from 'bcryptjs';
-import * as speakeasy from 'speakeasy';
+import * as OTPAuth from 'otpauth';
import * as QRCode from 'qrcode';
import { Inject, Injectable } from '@nestjs/common';
import type { UserProfilesRepository } from '@/models/index.js';
@@ -42,25 +42,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
// Generate user's secret key
- const secret = speakeasy.generateSecret({
- length: 32,
- });
+ const secret = new OTPAuth.Secret();
await this.userProfilesRepository.update(me.id, {
twoFactorTempSecret: secret.base32,
});
// Get the data URL of the authenticator URL
- const url = speakeasy.otpauthURL({
- secret: secret.base32,
- encoding: 'base32',
+ const totp = new OTPAuth.TOTP({
+ secret,
+ digits: 6,
label: me.username,
issuer: this.config.host,
});
- const dataUrl = await QRCode.toDataURL(url);
+ const url = totp.toString();
+ const qr = await QRCode.toDataURL(url);
return {
- qr: dataUrl,
+ qr,
url,
secret: secret.base32,
label: me.username,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
index 0f2b0fd7ee..4b726aed80 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/remove-key.ts
@@ -50,6 +50,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
id: ps.credentialId,
});
+ // 使われているキーがなくなったらパスワードレスログインをやめる
+ const keyCount = await this.userSecurityKeysRepository.count({
+ where: {
+ userId: me.id,
+ },
+ select: {
+ id: true,
+ name: true,
+ lastUsed: true,
+ },
+ });
+
+ if (keyCount === 0) {
+ await this.userProfilesRepository.update(me.id, {
+ usePasswordLessLogin: false,
+ });
+ }
+
// Publish meUpdated event
this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
detail: true,
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
index 4c5b151f78..e0e7ba6658 100644
--- a/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
+++ b/packages/backend/src/server/api/endpoints/i/2fa/unregister.ts
@@ -1,7 +1,9 @@
import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
export const meta = {
@@ -24,6 +26,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
@@ -38,7 +43,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.userProfilesRepository.update(me.id, {
twoFactorSecret: null,
twoFactorEnabled: false,
+ usePasswordLessLogin: false,
});
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
new file mode 100644
index 0000000000..d98f60fa5f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/2fa/update-key.ts
@@ -0,0 +1,78 @@
+import bcrypt from 'bcryptjs';
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/index.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '../../../error.js';
+
+export const meta = {
+ requireCredential: true,
+
+ secure: true,
+
+ errors: {
+ noSuchKey: {
+ message: 'No such key.',
+ code: 'NO_SUCH_KEY',
+ id: 'f9c5467f-d492-4d3c-9a8g-a70dacc86512',
+ },
+
+ accessDenied: {
+ message: 'You do not have edit privilege of the channel.',
+ code: 'ACCESS_DENIED',
+ id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string', minLength: 1, maxLength: 30 },
+ credentialId: { type: 'string' },
+ },
+ required: ['name', 'credentialId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
+ private userEntityService: UserEntityService,
+ private globalEventService: GlobalEventService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const key = await this.userSecurityKeysRepository.findOneBy({
+ id: ps.credentialId,
+ });
+
+ if (key == null) {
+ throw new ApiError(meta.errors.noSuchKey);
+ }
+
+ if (key.userId !== me.id) {
+ throw new ApiError(meta.errors.accessDenied);
+ }
+
+ await this.userSecurityKeysRepository.update(key.id, {
+ name: ps.name,
+ });
+
+ // Publish meUpdated event
+ this.globalEventService.publishMainStream(me.id, 'meUpdated', await this.userEntityService.pack(me.id, me, {
+ detail: true,
+ includeSecrets: true,
+ }));
+
+ return {};
+ });
+ }
+}