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: {