Add recaptcha v3 for register form.

This commit is contained in:
theADAMJR 2023-06-05 10:59:46 +01:00
parent 265992bc35
commit 65f7d41284
14 changed files with 135 additions and 47 deletions

View File

@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Recaptcha to prevent API abuse.
### Fixed
## [Winter 0.5.0-alpha] - 2023/01/17
### Added
- Mentions: Mention channels, and users which selectively highlights associated users/channels.

View File

@ -4,4 +4,5 @@ MONGO_URI="mongodb://localhost/accord"
NODE_ENV="dev"
PORT=3000
WEBSITE_URL="http://localhost:4200"
SESSION_SECRET="Please ⭐ this repository."
SESSION_SECRET="Please ⭐ this repository."
RECAPTCHA_SECRET=""

View File

@ -7,14 +7,14 @@ const windowMs = 10 * 60 * 1000;
// additional layer rate limits
export const extraRateLimit = (maxRequests: number) => (req: Request, res: Response, next: NextFunction) => {
if (process.env.NODE_ENV === 'dev') return next();
return rateLimit({
max: maxRequests,
message: JSON.stringify({ message: 'You are being rate limited' }),
store: new RateLimitStore({
uri: process.env.MONGO_URI,
collectionName: 'extraRateLimits',
expireTimeMs: windowMs,
expireTimeMs: windowMs / 2,
}),
windowMs: windowMs / 2,
})(req, res, next);
@ -25,7 +25,7 @@ export default (req: Request, res: Response, next: NextFunction) => {
if (process.env.NODE_ENV === 'dev') return next();
return rateLimit({
max: 5000,
max: 3000,
message: JSON.stringify({ message: 'You are being rate limited' }),
store: new RateLimitStore({
uri: process.env.MONGO_URI,

View File

@ -8,7 +8,7 @@ import { REST } from '@acrd/types';
export const router = Router();
router.post('/login', extraRateLimit(20), (req, res, next) => {
router.post('/login', extraRateLimit(10), (req, res, next) => {
req['flash'] = (_: string, message: string) => res.status(400).json({ message });
next();
}, passport.authenticate('local', {
@ -30,7 +30,22 @@ router.post('/login', extraRateLimit(20), (req, res, next) => {
res.status(201).json({ token: await deps.users.createToken(user) });
});
router.post('/register', extraRateLimit(1), async (req, res) => {
router.post('/register', extraRateLimit(10), async (req, res) => {
if (process.env.RECAPTCHA_SECRET) {
var response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(Object.entries({
secret: process.env.RECAPTCHA_SECRET,
response: req.query.recaptcha as string,
})).toString(),
});
var json = await response.json();
if (!json.success)
throw new APIError(400, 'Invalid captcha');
}
const user = await deps.users.create({
email: req.body.email,
password: req.body.password,
@ -42,7 +57,7 @@ router.post('/register', extraRateLimit(1), async (req, res) => {
res.status(201).json(await deps.users.createToken(user));
});
router.get('/verify', extraRateLimit(10), async (req, res) => {
router.get('/verify', extraRateLimit(5), async (req, res) => {
const email = deps.verification.getEmailFromCode(req.query.code as string);
const user = await User.findOne({ email }) as any;
if (!email || !user)
@ -66,7 +81,7 @@ router.get('/verify', extraRateLimit(10), async (req, res) => {
res.json({ token: await deps.users.createToken(user) });
});
router.get('/email/forgot-password', extraRateLimit(10), async (req, res) => {
router.get('/email/forgot-password', extraRateLimit(5), async (req, res) => {
const email = req.query.email?.toString();
if (!email)
throw new APIError(400, 'Email not provided');
@ -82,7 +97,7 @@ router.get('/email/forgot-password', extraRateLimit(10), async (req, res) => {
}
});
router.post('/change-password', extraRateLimit(10), async (req, res) => {
router.post('/change-password', extraRateLimit(5), async (req, res) => {
const { email, oldPassword, newPassword }: REST.To.Post['/auth/change-password'] = req.body;
const user = await User.findOne({ email }) as any as SelfUserDocument;

View File

@ -6,4 +6,5 @@ NODE_ENV="dev"
PORT=3001
ROOT_ENDPOINT="http://localhost:3001"
WEBSITE_URL="http://localhost:4200"
SESSION_SECRET="Please ⭐ this repository."
SESSION_SECRET="Please ⭐ this repository."
RECAPTCHA_SECRET=""

View File

@ -9,6 +9,7 @@ declare global {
ROOT_ENDPOINT: string;
WEBSITE_URL: string;
SESSION_SECRET: string;
RECAPTCHA_SECRET: string;
/** Set during runtime. */
SSH_KEY: string;
}

View File

@ -6,4 +6,5 @@ REACT_APP_REPO_URL="https://github.com/theadamjr/acrd.app"
REACT_APP_VERSION_NAME="Cyan"
REACT_APP_VERSION_NUMBER="0.6.0-dev-release"
REACT_APP_ROOT_API_URL="http://localhost:3000"
REACT_APP_OWNER_USER_ID="450428712295456768"
REACT_APP_OWNER_USER_ID="450428712295456768"
REACT_APP_RECAPTCHA_SITE_KEY="6Ldzz2omAAAAAAZ7Ey6ZmDtCS5rMSxVYc3Uf5EpQ"

View File

@ -6,4 +6,5 @@ REACT_APP_REPO_URL="https://github.com/theadamjr/acrd.app"
REACT_APP_VERSION_NAME="Cyan"
REACT_APP_VERSION_NUMBER="0.6.0-dev-release"
REACT_APP_ROOT_API_URL="https://api.acrd.app"
REACT_APP_OWNER_USER_ID="451453819310678016"
REACT_APP_OWNER_USER_ID="451453819310678016"
REACT_APP_RECAPTCHA_SITE_KEY="6Ldzz2omAAAAAAZ7Ey6ZmDtCS5rMSxVYc3Uf5EpQ"

View File

@ -38,6 +38,7 @@
"react-contextmenu": "^2.14.0",
"react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-google-recaptcha-v3": "^1.10.1",
"react-hook-form": "^7.12.1",
"react-modal": "^3.14.3",
"react-number-format": "^4.7.3",
@ -67,6 +68,7 @@
"@types/javascript-time-ago": "^2.0.3",
"@types/mocha": "^9.0.0",
"@types/promise-timeout": "^1.3.0",
"@types/react-google-recaptcha": "^2.1.5",
"@types/react-modal": "^3.12.1",
"@types/react-router-dom": "^5.1.8",
"@types/react-select": "^4.0.17",
@ -4122,6 +4124,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-google-recaptcha": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.5.tgz",
"integrity": "sha512-iWTjmVttlNgp0teyh7eBXqNOQzVq2RWNiFROWjraOptRnb1OcHJehQnji0sjqIRAk9K0z8stjyhU+OLpPb0N6w==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-modal": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz",
@ -17495,6 +17506,18 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
"node_modules/react-google-recaptcha-v3": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz",
"integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==",
"dependencies": {
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"react": "^16.3 || ^17.0 || ^18.0",
"react-dom": "^17.0 || ^18.0"
}
},
"node_modules/react-hook-form": {
"version": "7.19.5",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.19.5.tgz",
@ -25580,6 +25603,15 @@
"@types/react": "*"
}
},
"@types/react-google-recaptcha": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.5.tgz",
"integrity": "sha512-iWTjmVttlNgp0teyh7eBXqNOQzVq2RWNiFROWjraOptRnb1OcHJehQnji0sjqIRAk9K0z8stjyhU+OLpPb0N6w==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-modal": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@types/react-modal/-/react-modal-3.13.1.tgz",
@ -35486,6 +35518,14 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
"react-google-recaptcha-v3": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/react-google-recaptcha-v3/-/react-google-recaptcha-v3-1.10.1.tgz",
"integrity": "sha512-K3AYzSE0SasTn+XvV2tq+6YaxM+zQypk9rbCgG4OVUt7Rh4ze9basIKefoBz9sC0CNslJj9N1uwTTgRMJQbQJQ==",
"requires": {
"hoist-non-react-statics": "^3.3.2"
}
},
"react-hook-form": {
"version": "7.19.5",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.19.5.tgz",

View File

@ -34,6 +34,7 @@
"react-contextmenu": "^2.14.0",
"react-dom": "^17.0.2",
"react-draggable": "^4.4.4",
"react-google-recaptcha-v3": "^1.10.1",
"react-hook-form": "^7.12.1",
"react-modal": "^3.14.3",
"react-number-format": "^4.7.3",
@ -95,6 +96,7 @@
"@types/javascript-time-ago": "^2.0.3",
"@types/mocha": "^9.0.0",
"@types/promise-timeout": "^1.3.0",
"@types/react-google-recaptcha": "^2.1.5",
"@types/react-modal": "^3.12.1",
"@types/react-router-dom": "^5.1.8",
"@types/react-select": "^4.0.17",

View File

@ -5,8 +5,9 @@ import { registerUser } from '../../../store/auth';
import NormalButton from '../../utils/buttons/normal-button';
import PageWrapper from '../page-wrapper';
import Input from '../../inputs/input';
import { useState } from 'react';
import { useCallback, useState } from 'react';
import FullParticles from '../../utils/full-particles';
import { GoogleReCaptcha, GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
const RegisterPage: React.FunctionComponent = () => {
const user = useSelector((s: Store.AppState) => s.auth.user);
@ -14,45 +15,61 @@ const RegisterPage: React.FunctionComponent = () => {
const { register, handleSubmit } = useForm();
const query = new URLSearchParams(useLocation().search);
const [email, setEmail] = useState(query.get('email') ?? '');
const [token, setToken] = useState("");
const [refreshReCaptcha, setRefreshReCaptcha] = useState(true);
const onSubmit = (data) => dispatch(registerUser(data));
const doSomething = () => {
/* do something like submit a form and then refresh recaptcha */
setRefreshReCaptcha(r => !r);
}
const onSubmit = async (data) => {
if (token)
dispatch(registerUser(data, token));
else throw new TypeError("Verify you are not a robot");
}
return (user)
? <Redirect to="/channels/@me" />
: (
<PageWrapper>
<div className="flex items-center justify-center absolute top-[30%] left-[35%]">
<form className="rounded-md shadow bg-bg-primary p-8 w-[480px]"
onSubmit={handleSubmit(onSubmit)}>
<h1 className="text-2xl font-bold mb-8 text-center">Create an account</h1>
<GoogleReCaptchaProvider reCaptchaKey={process.env.REACT_APP_RECAPTCHA_SITE_KEY}>
<PageWrapper>
<div className="flex items-center justify-center absolute top-[30%] left-[35%]">
<form className="rounded-md shadow bg-bg-primary p-8 w-[480px]"
onSubmit={handleSubmit(onSubmit)}>
<h1 className="text-2xl font-bold mb-8 text-center">Create an account</h1>
<Input
label="Email"
name="email"
register={register}
className="mt-3"
defaultValue={email!}
onInput={(e) => setEmail(e.currentTarget.value)} />
<Input
label="Username"
name="username"
register={register}
className="mt-3" />
<Input
label="Password"
name="password"
type="password"
register={register}
className="mt-3" />
<Input
label="Email"
name="email"
register={register}
className="mt-3"
defaultValue={email!}
onInput={(e) => setEmail(e.currentTarget.value)} />
<Input
label="Username"
name="username"
register={register}
className="mt-3" />
<Input
label="Password"
name="password"
type="password"
register={register}
className="mt-3" />
<NormalButton className="bg-primary font w-full h-11 rounded-md mt-8">Register</NormalButton>
<p className="mt-2">
<Link to={`/login${email && `?email=${email}`}`}>Already have an account?</Link>
</p>
</form>
</div>
<FullParticles />
</PageWrapper>
<GoogleReCaptcha onVerify={(token) => setToken(token)} />
<NormalButton
onClick={doSomething}
className="bg-primary font w-full h-11 rounded-md mt-8">Register</NormalButton>
<p className="mt-2">
<Link to={`/login${email && `?email=${email}`}`}>Already have an account?</Link>
</p>
</form>
</div>
<FullParticles />
</PageWrapper>
</GoogleReCaptchaProvider>
);
}

View File

@ -9,6 +9,7 @@ import configureStore from './store/configure-store';
import { SnackbarProvider } from 'notistack';
import { applyTheme } from './store/themes';
import accordTheme from '!!raw-loader!./styles/theme/accord-theme.css';
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
applyTheme(accordTheme);

View File

@ -79,12 +79,12 @@ export const logoutUser = () => (dispatch) => {
localStorage.removeItem('token');
}
export const registerUser = (data: REST.To.Post['/auth/register']) => (dispatch) => {
export const registerUser = (data: REST.To.Post['/auth/register'], recaptcha: string) => (dispatch) => {
dispatch(api.restCallBegan({
onSuccess: [actions.loggedInAttempted.type],
method: 'post',
data,
url: `/auth/register`,
url: `/auth/register?recaptcha=${recaptcha}`,
callback: (payload) => {
localStorage.setItem('token', payload);
dispatch(ready());

View File

@ -10,6 +10,7 @@ declare global {
REACT_APP_VERSION_NUMBER: string;
REACT_APP_ROOT_API_URL: string;
REACT_APP_OWNER_USER_ID: string;
REACT_APP_RECAPTCHA_SITE_KEY: string;
}
}
}