enhance: require captcha for signin (MisskeyIO#742)

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
riku6460 2024-09-30 21:39:59 +09:00 committed by GitHub
parent c5f5602301
commit 32aa76485f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 72 additions and 2 deletions

View File

@ -20,6 +20,9 @@ import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js'; import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js'; import { UserAuthService } from '@/core/UserAuthService.js';
import { LoggerService } from '@/core/LoggerService.js'; import { LoggerService } from '@/core/LoggerService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { MetaService } from '@/core/MetaService.js';
import { RateLimiterService } from './RateLimiterService.js'; import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js'; import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types'; import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
@ -46,6 +49,8 @@ export class SigninApiService {
private signinService: SigninService, private signinService: SigninService,
private userAuthService: UserAuthService, private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService, private webAuthnService: WebAuthnService,
private metaService: MetaService,
private captchaService: CaptchaService,
) { ) {
} }
@ -57,6 +62,10 @@ export class SigninApiService {
password: string; password: string;
token?: string; token?: string;
credential?: AuthenticationResponseJSON; credential?: AuthenticationResponseJSON;
'hcaptcha-response'?: string;
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'm-captcha-response'?: string;
}; };
}>, }>,
reply: FastifyReply, reply: FastifyReply,
@ -157,6 +166,33 @@ export class SigninApiService {
}; };
if (!profile.twoFactorEnabled) { if (!profile.twoFactorEnabled) {
if (process.env.NODE_ENV !== 'test') {
const meta = await this.metaService.fetch();
if (meta.enableHcaptcha && meta.hcaptchaSecretKey) {
await this.captchaService.verifyHcaptcha(meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
if (meta.enableMcaptcha && meta.mcaptchaSecretKey && meta.mcaptchaSitekey && meta.mcaptchaInstanceUrl) {
await this.captchaService.verifyMcaptcha(meta.mcaptchaSecretKey, meta.mcaptchaSitekey, meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
if (meta.enableRecaptcha && meta.recaptchaSecretKey) {
await this.captchaService.verifyRecaptcha(meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
if (meta.enableTurnstile && meta.turnstileSecretKey) {
await this.captchaService.verifyTurnstile(meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
}
if (same) { if (same) {
logger.info('Successfully signed in with password.'); logger.info('Successfully signed in with password.');
return this.signinService.signin(request, reply, user); return this.signinService.signin(request, reply, user);

View File

@ -91,6 +91,7 @@ if (loaded || props.provider === 'mcaptcha') {
function reset() { function reset() {
if (captcha.value.reset) captcha.value.reset(); if (captcha.value.reset) captcha.value.reset();
emit('update:modelValue', null);
} }
async function requestRender() { async function requestRender() {

View File

@ -19,7 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-lock"></i></template> <template #prefix><i class="ti ti-lock"></i></template>
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template> <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
</MkInput> </MkInput>
<MkButton type="submit" large primary rounded :disabled="!user || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton> <MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="!user?.twoFactorEnabled && instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkButton type="submit" large primary rounded :disabled="!user || captchaFailed || signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
</div> </div>
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }"> <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
<div v-if="user && user.securityKeys" class="twofa-group tap-group"> <div v-if="user && user.securityKeys" class="twofa-group tap-group">
@ -49,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, ref } from 'vue'; import { computed, defineAsyncComponent, ref } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill'; import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
@ -62,6 +66,8 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js'; import { login } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
const signing = ref(false); const signing = ref(false);
const userAbortController = ref<AbortController>(); const userAbortController = ref<AbortController>();
@ -74,6 +80,22 @@ const totpLogin = ref(false);
const isBackupCode = ref(false); const isBackupCode = ref(false);
const queryingKey = ref(false); const queryingKey = ref(false);
const credentialRequest = ref<CredentialRequestOptions | null>(null); const credentialRequest = ref<CredentialRequestOptions | null>(null);
const hcaptcha = ref<Captcha | undefined>();
const mcaptcha = ref<Captcha | undefined>();
const recaptcha = ref<Captcha | undefined>();
const turnstile = ref<Captcha | undefined>();
const hCaptchaResponse = ref<string | null>(null);
const mCaptchaResponse = ref<string | null>(null);
const reCaptchaResponse = ref<string | null>(null);
const turnstileResponse = ref<string | null>(null);
const captchaFailed = computed((): boolean => {
return !user.value?.twoFactorEnabled && (
instance.enableHcaptcha && !hCaptchaResponse.value ||
instance.enableMcaptcha && !mCaptchaResponse.value ||
instance.enableRecaptcha && !reCaptchaResponse.value ||
instance.enableTurnstile && !turnstileResponse.value);
});
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'login', v: any): void; (ev: 'login', v: any): void;
@ -170,6 +192,10 @@ function onSubmit(): void {
misskeyApi('signin', { misskeyApi('signin', {
username: username.value, username: username.value,
password: password.value, password: password.value,
'hcaptcha-response': hCaptchaResponse.value,
'm-captcha-response': mCaptchaResponse.value,
'g-recaptcha-response': reCaptchaResponse.value,
'turnstile-response': turnstileResponse.value,
token: user.value?.twoFactorEnabled ? token.value : undefined, token: user.value?.twoFactorEnabled ? token.value : undefined,
}).then(res => { }).then(res => {
emit('login', res); emit('login', res);
@ -179,6 +205,11 @@ function onSubmit(): void {
} }
function loginFailed(err: any): void { function loginFailed(err: any): void {
hcaptcha.value?.reset?.();
mcaptcha.value?.reset?.();
recaptcha.value?.reset?.();
turnstile.value?.reset?.();
switch (err.id) { switch (err.id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': { case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({ os.alert({

View File

@ -90,6 +90,7 @@ const emit = defineEmits<{
const host = toUnicode(config.host); const host = toUnicode(config.host);
const hcaptcha = ref<Captcha | undefined>(); const hcaptcha = ref<Captcha | undefined>();
const mcaptcha = ref<Captcha | undefined>();
const recaptcha = ref<Captcha | undefined>(); const recaptcha = ref<Captcha | undefined>();
const turnstile = ref<Captcha | undefined>(); const turnstile = ref<Captcha | undefined>();
@ -217,6 +218,7 @@ async function onSubmit(): Promise<void> {
} catch { } catch {
submitting.value = false; submitting.value = false;
hcaptcha.value?.reset?.(); hcaptcha.value?.reset?.();
mcaptcha.value?.reset?.();
recaptcha.value?.reset?.(); recaptcha.value?.reset?.();
turnstile.value?.reset?.(); turnstile.value?.reset?.();
} }