From b21b0580058c14532ff3f4033e2a9147643bfca6 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> 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 7b66657ad..e8b222a4d 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 bdf247a56..4f88e1829 100644 --- a/packages/client/src/components/signin.vue +++ b/packages/client/src/components/signin.vue @@ -33,6 +33,8 @@ <template #label>{{ $ts.token }}</template> <template #prefix><i class="fas fa-gavel"></i></template> </MkInput> + <MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/> + <MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/> <MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton> </div> </div> @@ -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 62f370ffa..aeed0e53f 100644 --- a/packages/client/src/components/signup.vue +++ b/packages/client/src/components/signup.vue @@ -58,8 +58,8 @@ </template> </I18n> </MkSwitch> - <captcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/> - <captcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/> + <MkCaptcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/> + <MkCaptcha v-if="meta.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="meta.recaptchaSiteKey"/> <MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton> </template> </form> @@ -81,7 +81,7 @@ export default defineComponent({ MkButton, MkInput, MkSwitch, - captcha: defineAsyncComponent(() => import('./captcha.vue')), + MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), }, props: {