feat: make captcha required when signin to improve security
This commit is contained in:
		
							parent
							
								
									6de40cf789
								
							
						
					
					
						commit
						b21b058005
					
				
					 3 changed files with 38 additions and 10 deletions
				
			
		| 
						 | 
				
			
			@ -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'),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue