enhance: require captcha for signin (#14655)
* wip * Update MkSignin.vue * Update MkSignin.vue * wip * Update CHANGELOG.md
This commit is contained in:
parent
6dde457452
commit
1074d625ed
4 changed files with 74 additions and 4 deletions
|
@ -1,7 +1,7 @@
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### General
|
### General
|
||||||
-
|
- Enhance: セキュリティ向上のため、サインイン時もCAPTCHAを求めるようになりました
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- Enhance: フォロワーへのメッセージ欄のデザイン改良
|
- Enhance: フォロワーへのメッセージ欄のデザイン改良
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as OTPAuth from 'otpauth';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type {
|
import type {
|
||||||
|
MiMeta,
|
||||||
SigninsRepository,
|
SigninsRepository,
|
||||||
UserProfilesRepository,
|
UserProfilesRepository,
|
||||||
UsersRepository,
|
UsersRepository,
|
||||||
|
@ -20,6 +21,8 @@ import { IdService } from '@/core/IdService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
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 { CaptchaService } from '@/core/CaptchaService.js';
|
||||||
|
import { FastifyReplyError } from '@/misc/fastify-reply-error.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';
|
||||||
|
@ -31,6 +34,9 @@ export class SigninApiService {
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -45,6 +51,7 @@ export class SigninApiService {
|
||||||
private signinService: SigninService,
|
private signinService: SigninService,
|
||||||
private userAuthService: UserAuthService,
|
private userAuthService: UserAuthService,
|
||||||
private webAuthnService: WebAuthnService,
|
private webAuthnService: WebAuthnService,
|
||||||
|
private captchaService: CaptchaService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,6 +63,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,
|
||||||
|
@ -139,6 +150,32 @@ export class SigninApiService {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!profile.twoFactorEnabled) {
|
if (!profile.twoFactorEnabled) {
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
|
||||||
|
await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
|
||||||
|
throw new FastifyReplyError(400, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
|
||||||
|
await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
|
||||||
|
throw new FastifyReplyError(400, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
|
||||||
|
await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
|
||||||
|
throw new FastifyReplyError(400, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
|
||||||
|
await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
|
||||||
|
throw new FastifyReplyError(400, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (same) {
|
if (same) {
|
||||||
return this.signinService.signin(request, reply, user);
|
return this.signinService.signin(request, reply, user);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,7 +32,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="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||||
|
<MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
|
||||||
|
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||||
|
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||||
|
<MkButton type="submit" large primary rounded :disabled="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">
|
||||||
|
@ -68,7 +72,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';
|
||||||
|
@ -85,6 +89,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 user = ref<Misskey.entities.UserDetailed | null>(null);
|
const user = ref<Misskey.entities.UserDetailed | null>(null);
|
||||||
|
@ -98,6 +104,22 @@ const isBackupCode = ref(false);
|
||||||
const queryingKey = ref(false);
|
const queryingKey = ref(false);
|
||||||
let credentialRequest: CredentialRequestOptions | null = null;
|
let credentialRequest: CredentialRequestOptions | null = null;
|
||||||
const passkey_context = ref('');
|
const passkey_context = ref('');
|
||||||
|
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 (
|
||||||
|
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;
|
||||||
|
@ -227,6 +249,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);
|
||||||
|
@ -236,6 +262,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({
|
||||||
|
|
|
@ -81,10 +81,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { toUnicode } from 'punycode/';
|
import { toUnicode } from 'punycode/';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
|
import * as config from '@@/js/config.js';
|
||||||
import MkButton from './MkButton.vue';
|
import MkButton from './MkButton.vue';
|
||||||
import MkInput from './MkInput.vue';
|
import MkInput from './MkInput.vue';
|
||||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||||
import * as config from '@@/js/config.js';
|
|
||||||
import * as os from '@/os.js';
|
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';
|
||||||
|
@ -105,6 +105,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>();
|
||||||
|
|
||||||
|
@ -281,6 +282,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?.();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue