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 Koa from 'koa'; | ||||||
| import bcrypt from 'bcryptjs'; | import bcrypt from 'bcryptjs'; | ||||||
| import * as speakeasy from 'speakeasy'; | import * as speakeasy from 'speakeasy'; | ||||||
| import signin from '../common/signin.js'; | import { IsNull } from 'typeorm'; | ||||||
| import config from '@/config/index.js'; | import config from '@/config/index.js'; | ||||||
| import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js'; | import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js'; | ||||||
| import { ILocalUser } from '@/models/entities/user.js'; | import { ILocalUser } from '@/models/entities/user.js'; | ||||||
| import { genId } from '@/misc/gen-id.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 { verifyLogin, hash } from '../2fa.js'; | ||||||
| import { randomBytes } from 'node:crypto'; | import signin from '../common/signin.js'; | ||||||
| import { IsNull } from 'typeorm'; |  | ||||||
| 
 | 
 | ||||||
| export default async (ctx: Koa.Context) => { | export default async (ctx: Koa.Context) => { | ||||||
| 	ctx.set('Access-Control-Allow-Origin', config.url); | 	ctx.set('Access-Control-Allow-Origin', config.url); | ||||||
| 	ctx.set('Access-Control-Allow-Credentials', 'true'); | 	ctx.set('Access-Control-Allow-Credentials', 'true'); | ||||||
| 
 | 
 | ||||||
| 	const body = ctx.request.body as any; | 	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 username = body['username']; | ||||||
| 	const password = body['password']; | 	const password = body['password']; | ||||||
| 	const token = body['token']; | 	const token = body['token']; | ||||||
|  | @ -155,7 +172,7 @@ export default async (ctx: Koa.Context) => { | ||||||
| 				body.credentialId | 				body.credentialId | ||||||
| 					.replace(/-/g, '+') | 					.replace(/-/g, '+') | ||||||
| 					.replace(/_/g, '/'), | 					.replace(/_/g, '/'), | ||||||
| 					'base64' | 				'base64', | ||||||
| 			).toString('hex'), | 			).toString('hex'), | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -33,6 +33,8 @@ | ||||||
| 					<template #label>{{ $ts.token }}</template> | 					<template #label>{{ $ts.token }}</template> | ||||||
| 					<template #prefix><i class="fas fa-gavel"></i></template> | 					<template #prefix><i class="fas fa-gavel"></i></template> | ||||||
| 				</MkInput> | 				</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> | 				<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? $ts.loggingIn : $ts.login }}</MkButton> | ||||||
| 			</div> | 			</div> | ||||||
| 		</div> | 		</div> | ||||||
|  | @ -60,6 +62,7 @@ export default defineComponent({ | ||||||
| 	components: { | 	components: { | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 		MkInput, | 		MkInput, | ||||||
|  | 		MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  | @ -90,6 +93,8 @@ export default defineComponent({ | ||||||
| 			credential: null, | 			credential: null, | ||||||
| 			challengeData: null, | 			challengeData: null, | ||||||
| 			queryingKey: false, | 			queryingKey: false, | ||||||
|  | 			hCaptchaResponse: null, | ||||||
|  | 			reCaptchaResponse: null, | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | @ -139,11 +144,13 @@ export default defineComponent({ | ||||||
| 				return os.api('signin', { | 				return os.api('signin', { | ||||||
| 					username: this.username, | 					username: this.username, | ||||||
| 					password: this.password, | 					password: this.password, | ||||||
|  | 					'hcaptcha-response': this.hCaptchaResponse, | ||||||
|  | 					'g-recaptcha-response': this.reCaptchaResponse, | ||||||
| 					signature: hexify(credential.response.signature), | 					signature: hexify(credential.response.signature), | ||||||
| 					authenticatorData: hexify(credential.response.authenticatorData), | 					authenticatorData: hexify(credential.response.authenticatorData), | ||||||
| 					clientDataJSON: hexify(credential.response.clientDataJSON), | 					clientDataJSON: hexify(credential.response.clientDataJSON), | ||||||
| 					credentialId: credential.id, | 					credentialId: credential.id, | ||||||
| 					challengeId: this.challengeData.challengeId | 					challengeId: this.challengeData.challengeId, | ||||||
| 				}); | 				}); | ||||||
| 			}).then(res => { | 			}).then(res => { | ||||||
| 				this.$emit('login', res); | 				this.$emit('login', res); | ||||||
|  | @ -164,7 +171,9 @@ export default defineComponent({ | ||||||
| 				if (window.PublicKeyCredential && this.user.securityKeys) { | 				if (window.PublicKeyCredential && this.user.securityKeys) { | ||||||
| 					os.api('signin', { | 					os.api('signin', { | ||||||
| 						username: this.username, | 						username: this.username, | ||||||
| 						password: this.password | 						password: this.password, | ||||||
|  | 						'hcaptcha-response': this.hCaptchaResponse, | ||||||
|  | 						'g-recaptcha-response': this.reCaptchaResponse, | ||||||
| 					}).then(res => { | 					}).then(res => { | ||||||
| 						this.totpLogin = true; | 						this.totpLogin = true; | ||||||
| 						this.signing = false; | 						this.signing = false; | ||||||
|  | @ -179,7 +188,9 @@ export default defineComponent({ | ||||||
| 				os.api('signin', { | 				os.api('signin', { | ||||||
| 					username: this.username, | 					username: this.username, | ||||||
| 					password: this.password, | 					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 => { | 				}).then(res => { | ||||||
| 					this.$emit('login', res); | 					this.$emit('login', res); | ||||||
| 					this.onLogin(res); | 					this.onLogin(res); | ||||||
|  |  | ||||||
|  | @ -58,8 +58,8 @@ | ||||||
| 				</template> | 				</template> | ||||||
| 			</I18n> | 			</I18n> | ||||||
| 		</MkSwitch> | 		</MkSwitch> | ||||||
| 		<captcha v-if="meta.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="meta.hcaptchaSiteKey"/> | 		<MkCaptcha 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.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> | 		<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ $ts.start }}</MkButton> | ||||||
| 	</template> | 	</template> | ||||||
| </form> | </form> | ||||||
|  | @ -81,7 +81,7 @@ export default defineComponent({ | ||||||
| 		MkButton, | 		MkButton, | ||||||
| 		MkInput, | 		MkInput, | ||||||
| 		MkSwitch, | 		MkSwitch, | ||||||
| 		captcha: defineAsyncComponent(() => import('./captcha.vue')), | 		MkCaptcha: defineAsyncComponent(() => import('./captcha.vue')), | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	props: { | 	props: { | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue