Skip to content

Commit

Permalink
✨ Add recaptcha challenge on hasura auth
Browse files Browse the repository at this point in the history
  • Loading branch information
lechinoix committed Nov 3, 2023
1 parent 59e588a commit ba34745
Show file tree
Hide file tree
Showing 12 changed files with 84 additions and 3 deletions.
8 changes: 8 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ export const ERRORS = asErrors({
status: StatusCodes.INTERNAL_SERVER_ERROR,
message: 'Invalid OAuth configuration',
},
'missing-or-invalid-captcha': {
status: StatusCodes.UNAUTHORIZED,
message: 'Missing or Invalid recaptcha challenge',
},
'missing-captcha-site-key': {
status: StatusCodes.INTERNAL_SERVER_ERROR,
message: 'Missing catpcha site key',
},
});

export const sendError = (
Expand Down
25 changes: 25 additions & 0 deletions src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RequestHandler } from 'express';
import { getPermissionVariables } from '@/utils';
import { sendError } from '@/errors';
import { checkRecaptchaChallenge } from '@/utils/recaptcha';

export const authMiddleware: RequestHandler = async (req, _, next) => {
try {
Expand All @@ -25,3 +26,27 @@ export const authenticationGate: RequestHandler = (req, res, next) => {
next();
}
};

export const verifyCaptcha: RequestHandler = async (req, res, next) => {
const { body } = req;
const { recaptchaChallenge } = body;

try {
const isValidChallenge = await checkRecaptchaChallenge(recaptchaChallenge);

if (!isValidChallenge) {
return sendError(res, 'missing-or-invalid-captcha');
} else {
next();
}
} catch (e: any) {
sendError(
res,
e.message === 'missing-captcha-site-key'
? 'missing-captcha-site-key'
: 'internal-error'
);
}
};

export const alwaysAllow: RequestHandler = (_, __, next) => next();
1 change: 1 addition & 0 deletions src/routes/signin/email-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Joi, email, password } from '@/validation';
export const signInEmailPasswordSchema = Joi.object({
email: email.required(),
password: password.required(),
recaptchaChallenge: Joi.string(),
}).meta({ className: 'SignInEmailPasswordSchema' });

export const signInEmailPasswordHandler: RequestHandler<
Expand Down
6 changes: 5 additions & 1 deletion src/routes/signin/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from 'express';

import { asyncWrapper as aw } from '@/utils';
import { ENV, asyncWrapper as aw } from '@/utils';
import { bodyValidator } from '@/validation';

import { signInAnonymousHandler, signInAnonymousSchema } from './anonymous';
Expand All @@ -23,6 +23,7 @@ import {
signInWebauthnHandler,
signInWebauthnSchema,
} from './webauthn';
import { alwaysAllow, verifyCaptcha } from '@/middleware/auth';

const router = Router();

Expand All @@ -39,6 +40,7 @@ const router = Router();
router.post(
'/signin/email-password',
bodyValidator(signInEmailPasswordSchema),
ENV.AUTH_SIGNIN_RECAPTCHA_CHALLENGE ? verifyCaptcha : alwaysAllow,
aw(signInEmailPasswordHandler)
);

Expand All @@ -55,6 +57,7 @@ router.post(
router.post(
'/signin/passwordless/email',
bodyValidator(signInPasswordlessEmailSchema),
ENV.AUTH_SIGNIN_RECAPTCHA_CHALLENGE ? verifyCaptcha : alwaysAllow,
aw(signInPasswordlessEmailHandler)
);

Expand All @@ -70,6 +73,7 @@ router.post(
router.post(
'/signin/passwordless/sms',
bodyValidator(signInPasswordlessSmsSchema),
ENV.AUTH_SIGNIN_RECAPTCHA_CHALLENGE ? verifyCaptcha : alwaysAllow,
aw(signInPasswordlessSmsHandler)
);

Expand Down
2 changes: 2 additions & 0 deletions src/routes/signin/passwordless/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import { Joi, email, registrationOptions } from '@/validation';
export type PasswordLessEmailRequestBody = {
email: string;
options: UserRegistrationOptionsWithRedirect;
recaptchaChallenge?: string;
};

export const signInPasswordlessEmailSchema =
Joi.object<PasswordLessEmailRequestBody>({
email: email.required(),
options: registrationOptions,
recaptchaChallenge: Joi.string(),
}).meta({ className: 'SignInPasswordlessEmailSchema' });

export const signInPasswordlessEmailHandler: RequestHandler<
Expand Down
2 changes: 2 additions & 0 deletions src/routes/signin/passwordless/sms/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ import { renderTemplate } from '@/templates';
export type PasswordLessSmsRequestBody = {
phoneNumber: string;
options: UserRegistrationOptions;
recaptchaChallenge?: string;
};

export const signInPasswordlessSmsSchema =
Joi.object<PasswordLessSmsRequestBody>({
phoneNumber,
options: registrationOptions,
recaptchaChallenge: Joi.string(),
}).meta({ className: 'SignInPasswordlessSmsSchema' });

export const signInPasswordlessSmsHandler: RequestHandler<
Expand Down
1 change: 1 addition & 0 deletions src/routes/signup/email-password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const signUpEmailPasswordSchema = Joi.object({
email: email.required(),
password: passwordInsert.required(),
options: registrationOptions,
recaptchaChallenge: Joi.string(),
}).meta({ className: 'SignUpEmailPasswordSchema' });

export const signUpEmailPasswordHandler: RequestHandler<
Expand Down
3 changes: 3 additions & 0 deletions src/routes/signup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
signUpWebauthnHandler,
signUpWebauthnSchema,
} from './webauthn';
import { alwaysAllow, verifyCaptcha } from '@/middleware/auth';
import { ENV } from '@/utils/env';

const router = Router();

Expand All @@ -27,6 +29,7 @@ const router = Router();
router.post(
'/signup/email-password',
bodyValidator(signUpEmailPasswordSchema),
ENV.AUTH_SIGNUP_RECAPTCHA_CHALLENGE ? verifyCaptcha : alwaysAllow,
aw(signUpEmailPasswordHandler)
);

Expand Down
9 changes: 7 additions & 2 deletions src/routes/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { Router } from 'express';

import { asyncWrapper as aw } from '@/utils';
import { ENV, asyncWrapper as aw } from '@/utils';
import { bodyValidator } from '@/validation';
import { authenticationGate } from '@/middleware/auth';
import {
alwaysAllow,
authenticationGate,
verifyCaptcha,
} from '@/middleware/auth';

import { userMFAHandler, userMfaSchema } from './mfa';
import { userHandler } from './user';
Expand Down Expand Up @@ -51,6 +55,7 @@ router.get('/user', authenticationGate, aw(userHandler));
router.post(
'/user/password/reset',
bodyValidator(userPasswordResetSchema),
ENV.AUTH_SIGNIN_RECAPTCHA_CHALLENGE ? verifyCaptcha : alwaysAllow,
aw(userPasswordResetHandler)
);

Expand Down
1 change: 1 addition & 0 deletions src/routes/user/password-reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const userPasswordResetSchema = Joi.object({
options: Joi.object({
redirectTo,
}).default(),
recaptchaChallenge: Joi.string(),
}).meta({ className: 'UserPasswordResetSchema' });

export const userPasswordResetHandler: RequestHandler<
Expand Down
12 changes: 12 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ export const ENV = {
return castIntEnv('AUTH_WEBAUTHN_ATTESTATION_TIMEOUT', 60000);
},

// RECAPTCHA
get AUTH_RECAPTCHA_SITE_KEY() {
return castStringEnv('AUTH_RECAPTCHA_SITE_KEY', '');
},

// SIGN UP
get AUTH_ANONYMOUS_USERS_ENABLED() {
return castBooleanEnv('AUTH_ANONYMOUS_USERS_ENABLED', false);
Expand Down Expand Up @@ -164,6 +169,9 @@ export const ENV = {
// remove duplicates
return [...new Set(locales)];
},
get AUTH_SIGNUP_RECAPTCHA_CHALLENGE() {
return castBooleanEnv('AUTH_SIGNUP_RECAPTCHA_CHALLENGE', false);
},

// SIGN IN
get AUTH_EMAIL_PASSWORDLESS_ENABLED() {
Expand All @@ -175,6 +183,10 @@ export const ENV = {
get AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED() {
return castBooleanEnv('AUTH_EMAIL_SIGNIN_EMAIL_VERIFIED_REQUIRED', true);
},
get AUTH_SIGNIN_RECAPTCHA_CHALLENGE() {
return castBooleanEnv('AUTH_SIGNIN_RECAPTCHA_CHALLENGE', false);
},

// get AUTH_SIGNIN_PHONE_NUMBER_VERIFIED_REQUIRED() {
// return castBooleanEnv('AUTH_SIGNIN_PHONE_NUMBER_VERIFIED_REQUIRED', true);
// },
Expand Down
17 changes: 17 additions & 0 deletions src/utils/recaptcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import axios from 'axios';
import { ENV } from '@/utils/env';

export const RECAPTCHA_BASE_URL =
'https://www.google.com/recaptcha/api/siteverify';

export const checkRecaptchaChallenge = async (recaptchaChallenge: string) => {
if (!ENV.AUTH_RECAPTCHA_SITE_KEY) {
throw new Error('missing-captcha-site-key');
}

const { data } = await axios.post(
`${RECAPTCHA_BASE_URL}?secret=${ENV.AUTH_RECAPTCHA_SITE_KEY}&response=${recaptchaChallenge}`
);

return data.success;
};

0 comments on commit ba34745

Please sign in to comment.