From b21b0580058c14532ff3f4033e2a9147643bfca6 Mon Sep 17 00:00:00 2001 From: syuilo Date: Sun, 15 May 2022 12:18:46 +0900 Subject: [PATCH] feat: make captcha required when signin to improve security --- .../backend/src/server/api/private/signin.ts | 25 ++++++++++++++++--- packages/client/src/components/signin.vue | 17 ++++++++++--- packages/client/src/components/signup.vue | 6 ++--- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/backend/src/server/api/private/signin.ts b/packages/backend/src/server/api/private/signin.ts index 7b66657ad8..e8b222a4d5 100644 --- a/packages/backend/src/server/api/private/signin.ts +++ b/packages/backend/src/server/api/private/signin.ts @@ -1,20 +1,37 @@ +import { randomBytes } from 'node:crypto'; import Koa from 'koa'; import bcrypt from 'bcryptjs'; import * as speakeasy from 'speakeasy'; -import signin from '../common/signin.js'; +import { IsNull } from 'typeorm'; import config from '@/config/index.js'; import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js'; import { ILocalUser } from '@/models/entities/user.js'; import { genId } from '@/misc/gen-id.js'; +import { fetchMeta } from '@/misc/fetch-meta.js'; +import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js'; import { verifyLogin, hash } from '../2fa.js'; -import { randomBytes } from 'node:crypto'; -import { IsNull } from 'typeorm'; +import signin from '../common/signin.js'; export default async (ctx: Koa.Context) => { ctx.set('Access-Control-Allow-Origin', config.url); ctx.set('Access-Control-Allow-Credentials', 'true'); const body = ctx.request.body as any; + + const instance = await fetchMeta(true); + + if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { + await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + + if (instance.enableRecaptcha && instance.recaptchaSecretKey) { + await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => { + ctx.throw(400, e); + }); + } + const username = body['username']; const password = body['password']; const token = body['token']; @@ -155,7 +172,7 @@ export default async (ctx: Koa.Context) => { body.credentialId .replace(/-/g, '+') .replace(/_/g, '/'), - 'base64' + 'base64', ).toString('hex'), }); diff --git a/packages/client/src/components/signin.vue b/packages/client/src/components/signin.vue index bdf247a56f..4f88e1829c 100644 --- a/packages/client/src/components/signin.vue +++ b/packages/client/src/components/signin.vue @@ -33,6 +33,8 @@ + + {{ signing ? $ts.loggingIn : $ts.login }} @@ -60,6 +62,7 @@ export default defineComponent({ components: { MkButton, MkInput, + MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), }, props: { @@ -90,6 +93,8 @@ export default defineComponent({ credential: null, challengeData: null, queryingKey: false, + hCaptchaResponse: null, + reCaptchaResponse: null, }; }, @@ -139,11 +144,13 @@ export default defineComponent({ return os.api('signin', { username: this.username, password: this.password, + 'hcaptcha-response': this.hCaptchaResponse, + 'g-recaptcha-response': this.reCaptchaResponse, signature: hexify(credential.response.signature), authenticatorData: hexify(credential.response.authenticatorData), clientDataJSON: hexify(credential.response.clientDataJSON), credentialId: credential.id, - challengeId: this.challengeData.challengeId + challengeId: this.challengeData.challengeId, }); }).then(res => { this.$emit('login', res); @@ -164,7 +171,9 @@ export default defineComponent({ if (window.PublicKeyCredential && this.user.securityKeys) { os.api('signin', { username: this.username, - password: this.password + password: this.password, + 'hcaptcha-response': this.hCaptchaResponse, + 'g-recaptcha-response': this.reCaptchaResponse, }).then(res => { this.totpLogin = true; this.signing = false; @@ -179,7 +188,9 @@ export default defineComponent({ os.api('signin', { username: this.username, password: this.password, - token: this.user && this.user.twoFactorEnabled ? this.token : undefined + 'hcaptcha-response': this.hCaptchaResponse, + 'g-recaptcha-response': this.reCaptchaResponse, + token: this.user && this.user.twoFactorEnabled ? this.token : undefined, }).then(res => { this.$emit('login', res); this.onLogin(res); diff --git a/packages/client/src/components/signup.vue b/packages/client/src/components/signup.vue index 62f370ffa8..aeed0e53fa 100644 --- a/packages/client/src/components/signup.vue +++ b/packages/client/src/components/signup.vue @@ -58,8 +58,8 @@ - - + + {{ $ts.start }} @@ -81,7 +81,7 @@ export default defineComponent({ MkButton, MkInput, MkSwitch, - captcha: defineAsyncComponent(() => import('./captcha.vue')), + MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), }, props: {