Skip to content

Commit 1074d62

Browse files
authored
enhance: require captcha for signin (#14655)
* wip * Update MkSignin.vue * Update MkSignin.vue * wip * Update CHANGELOG.md
1 parent 6dde457 commit 1074d62

File tree

4 files changed

+74
-4
lines changed

4 files changed

+74
-4
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
## Unreleased
22

33
### General
4-
-
4+
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
55

66
### Client
77
- Enhance: フォロワーへのメッセージ欄のデザイン改良

packages/backend/src/server/api/SigninApiService.ts

+37
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import * as OTPAuth from 'otpauth';
99
import { IsNull } from 'typeorm';
1010
import { DI } from '@/di-symbols.js';
1111
import type {
12+
MiMeta,
1213
SigninsRepository,
1314
UserProfilesRepository,
1415
UsersRepository,
@@ -20,6 +21,8 @@ import { IdService } from '@/core/IdService.js';
2021
import { bindThis } from '@/decorators.js';
2122
import { WebAuthnService } from '@/core/WebAuthnService.js';
2223
import { UserAuthService } from '@/core/UserAuthService.js';
24+
import { CaptchaService } from '@/core/CaptchaService.js';
25+
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
2326
import { RateLimiterService } from './RateLimiterService.js';
2427
import { SigninService } from './SigninService.js';
2528
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
@@ -31,6 +34,9 @@ export class SigninApiService {
3134
@Inject(DI.config)
3235
private config: Config,
3336

37+
@Inject(DI.meta)
38+
private meta: MiMeta,
39+
3440
@Inject(DI.usersRepository)
3541
private usersRepository: UsersRepository,
3642

@@ -45,6 +51,7 @@ export class SigninApiService {
4551
private signinService: SigninService,
4652
private userAuthService: UserAuthService,
4753
private webAuthnService: WebAuthnService,
54+
private captchaService: CaptchaService,
4855
) {
4956
}
5057

@@ -56,6 +63,10 @@ export class SigninApiService {
5663
password: string;
5764
token?: string;
5865
credential?: AuthenticationResponseJSON;
66+
'hcaptcha-response'?: string;
67+
'g-recaptcha-response'?: string;
68+
'turnstile-response'?: string;
69+
'm-captcha-response'?: string;
5970
};
6071
}>,
6172
reply: FastifyReply,
@@ -139,6 +150,32 @@ export class SigninApiService {
139150
};
140151

141152
if (!profile.twoFactorEnabled) {
153+
if (process.env.NODE_ENV !== 'test') {
154+
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
155+
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
156+
throw new FastifyReplyError(400, err);
157+
});
158+
}
159+
160+
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
161+
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
162+
throw new FastifyReplyError(400, err);
163+
});
164+
}
165+
166+
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
167+
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
168+
throw new FastifyReplyError(400, err);
169+
});
170+
}
171+
172+
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
173+
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
174+
throw new FastifyReplyError(400, err);
175+
});
176+
}
177+
}
178+
142179
if (same) {
143180
return this.signinService.signin(request, reply, user);
144181
} else {

packages/frontend/src/components/MkSignin.vue

+33-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only
3232
<template #prefix><i class="ti ti-lock"></i></template>
3333
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
3434
</MkInput>
35-
<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
35+
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
36+
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
37+
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
38+
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
39+
<MkButton type="submit" large primary rounded :disabled="captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
3640
</div>
3741
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
3842
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
@@ -68,7 +72,7 @@ SPDX-License-Identifier: AGPL-3.0-only
6872
</template>
6973

7074
<script lang="ts" setup>
71-
import { defineAsyncComponent, ref } from 'vue';
75+
import { computed, defineAsyncComponent, ref } from 'vue';
7276
import { toUnicode } from 'punycode/';
7377
import * as Misskey from 'misskey-js';
7478
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
@@ -85,6 +89,8 @@ import * as os from '@/os.js';
8589
import { misskeyApi } from '@/scripts/misskey-api.js';
8690
import { login } from '@/account.js';
8791
import { i18n } from '@/i18n.js';
92+
import { instance } from '@/instance.js';
93+
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
8894

8995
const signing = ref(false);
9096
const user = ref<Misskey.entities.UserDetailed | null>(null);
@@ -98,6 +104,22 @@ const isBackupCode = ref(false);
98104
const queryingKey = ref(false);
99105
let credentialRequest: CredentialRequestOptions | null = null;
100106
const passkey_context = ref('');
107+
const hcaptcha = ref<Captcha | undefined>();
108+
const mcaptcha = ref<Captcha | undefined>();
109+
const recaptcha = ref<Captcha | undefined>();
110+
const turnstile = ref<Captcha | undefined>();
111+
const hCaptchaResponse = ref<string | null>(null);
112+
const mCaptchaResponse = ref<string | null>(null);
113+
const reCaptchaResponse = ref<string | null>(null);
114+
const turnstileResponse = ref<string | null>(null);
115+
116+
const captchaFailed = computed((): boolean => {
117+
return (
118+
instance.enableHcaptcha && !hCaptchaResponse.value ||
119+
instance.enableMcaptcha && !mCaptchaResponse.value ||
120+
instance.enableRecaptcha && !reCaptchaResponse.value ||
121+
instance.enableTurnstile && !turnstileResponse.value);
122+
});
101123

102124
const emit = defineEmits<{
103125
(ev: 'login', v: any): void;
@@ -227,6 +249,10 @@ function onSubmit(): void {
227249
misskeyApi('signin', {
228250
username: username.value,
229251
password: password.value,
252+
'hcaptcha-response': hCaptchaResponse.value,
253+
'm-captcha-response': mCaptchaResponse.value,
254+
'g-recaptcha-response': reCaptchaResponse.value,
255+
'turnstile-response': turnstileResponse.value,
230256
token: user.value?.twoFactorEnabled ? token.value : undefined,
231257
}).then(res => {
232258
emit('login', res);
@@ -236,6 +262,11 @@ function onSubmit(): void {
236262
}
237263

238264
function loginFailed(err: any): void {
265+
hcaptcha.value?.reset?.();
266+
mcaptcha.value?.reset?.();
267+
recaptcha.value?.reset?.();
268+
turnstile.value?.reset?.();
269+
239270
switch (err.id) {
240271
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
241272
os.alert({

packages/frontend/src/components/MkSignupDialog.form.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,10 @@ SPDX-License-Identifier: AGPL-3.0-only
8181
import { ref, computed } from 'vue';
8282
import { toUnicode } from 'punycode/';
8383
import * as Misskey from 'misskey-js';
84+
import * as config from '@@/js/config.js';
8485
import MkButton from './MkButton.vue';
8586
import MkInput from './MkInput.vue';
8687
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
87-
import * as config from '@@/js/config.js';
8888
import * as os from '@/os.js';
8989
import { misskeyApi } from '@/scripts/misskey-api.js';
9090
import { login } from '@/account.js';
@@ -105,6 +105,7 @@ const emit = defineEmits<{
105105
const host = toUnicode(config.host);
106106

107107
const hcaptcha = ref<Captcha | undefined>();
108+
const mcaptcha = ref<Captcha | undefined>();
108109
const recaptcha = ref<Captcha | undefined>();
109110
const turnstile = ref<Captcha | undefined>();
110111

@@ -281,6 +282,7 @@ async function onSubmit(): Promise<void> {
281282
} catch {
282283
submitting.value = false;
283284
hcaptcha.value?.reset?.();
285+
mcaptcha.value?.reset?.();
284286
recaptcha.value?.reset?.();
285287
turnstile.value?.reset?.();
286288

0 commit comments

Comments
 (0)