Skip to content

Commit 1bec62f

Browse files
syuiloliumingye
authored andcommitted
enhance: require captcha for signin (misskey-dev#14655)
* wip * Update MkSignin.vue * Update MkSignin.vue * wip * Update CHANGELOG.md
1 parent 7ac29a7 commit 1bec62f

File tree

4 files changed

+71
-4
lines changed

4 files changed

+71
-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

+34
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as OTPAuth from 'otpauth';
1010
import { IsNull } from 'typeorm';
1111
import { DI } from '@/di-symbols.js';
1212
import type {
13+
MiMeta,
1314
SigninsRepository,
1415
UserProfilesRepository,
1516
UsersRepository,
@@ -21,6 +22,8 @@ import { IdService } from '@/core/IdService.js';
2122
import { bindThis } from '@/decorators.js';
2223
import { WebAuthnService } from '@/core/WebAuthnService.js';
2324
import { UserAuthService } from '@/core/UserAuthService.js';
25+
import { CaptchaService } from '@/core/CaptchaService.js';
26+
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
2427
import { RateLimiterService } from './RateLimiterService.js';
2528
import { SigninService } from './SigninService.js';
2629
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
@@ -51,6 +54,7 @@ export class SigninApiService {
5154
private signinService: SigninService,
5255
private userAuthService: UserAuthService,
5356
private webAuthnService: WebAuthnService,
57+
private captchaService: CaptchaService,
5458
) {
5559
}
5660

@@ -62,6 +66,10 @@ export class SigninApiService {
6266
password: string;
6367
token?: string;
6468
credential?: AuthenticationResponseJSON;
69+
'hcaptcha-response'?: string;
70+
'g-recaptcha-response'?: string;
71+
'turnstile-response'?: string;
72+
'm-captcha-response'?: string;
6573
};
6674
}>,
6775
reply: FastifyReply,
@@ -162,6 +170,32 @@ export class SigninApiService {
162170
};
163171

164172
if (!profile.twoFactorEnabled) {
173+
if (process.env.NODE_ENV !== 'test') {
174+
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
175+
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
176+
throw new FastifyReplyError(400, err);
177+
});
178+
}
179+
180+
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
181+
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
182+
throw new FastifyReplyError(400, err);
183+
});
184+
}
185+
186+
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
187+
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
188+
throw new FastifyReplyError(400, err);
189+
});
190+
}
191+
192+
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
193+
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
194+
throw new FastifyReplyError(400, err);
195+
});
196+
}
197+
}
198+
165199
if (same) {
166200
if (profile.password!.startsWith('$2')) {
167201
const newHash = await argon2.hash(password);

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';
@@ -86,6 +90,8 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
8690
import { login } from '@/account.js';
8791
import { i18n } from '@/i18n.js';
8892
import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js';
93+
import { instance } from '@/instance.js';
94+
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
8995

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

103125
const emit = defineEmits<{
104126
(ev: 'login', v: any): void;
@@ -233,6 +255,10 @@ function onSubmit(): void {
233255
misskeyApi('signin', {
234256
username: username.value,
235257
password: password.value,
258+
'hcaptcha-response': hCaptchaResponse.value,
259+
'm-captcha-response': mCaptchaResponse.value,
260+
'g-recaptcha-response': reCaptchaResponse.value,
261+
'turnstile-response': turnstileResponse.value,
236262
token: user.value?.twoFactorEnabled ? token.value : undefined,
237263
}).then(res => {
238264
emit('login', res);
@@ -242,6 +268,11 @@ function onSubmit(): void {
242268
}
243269

244270
function loginFailed(err: any): void {
271+
hcaptcha.value?.reset?.();
272+
mcaptcha.value?.reset?.();
273+
recaptcha.value?.reset?.();
274+
turnstile.value?.reset?.();
275+
245276
switch (err.id) {
246277
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
247278
os.alert({

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -85,10 +85,10 @@ SPDX-License-Identifier: AGPL-3.0-only
8585
import { ref, computed } from 'vue';
8686
import { toUnicode } from 'punycode/';
8787
import * as Misskey from 'misskey-js';
88+
import * as config from '@@/js/config.js';
8889
import MkButton from './MkButton.vue';
8990
import MkInput from './MkInput.vue';
9091
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
91-
import * as config from '@@/js/config.js';
9292
import * as os from '@/os.js';
9393
import { misskeyApi } from '@/scripts/misskey-api.js';
9494
import { login } from '@/account.js';
@@ -110,6 +110,7 @@ const emit = defineEmits<{
110110
const host = toUnicode(config.host);
111111

112112
const hcaptcha = ref<Captcha | undefined>();
113+
const mcaptcha = ref<Captcha | undefined>();
113114
const recaptcha = ref<Captcha | undefined>();
114115
const turnstile = ref<Captcha | undefined>();
115116

@@ -295,6 +296,7 @@ async function onSubmit(): Promise<void> {
295296
} catch {
296297
submitting.value = false;
297298
hcaptcha.value?.reset?.();
299+
mcaptcha.value?.reset?.();
298300
recaptcha.value?.reset?.();
299301
turnstile.value?.reset?.();
300302

0 commit comments

Comments
 (0)