Skip to content

Commit

Permalink
feat(iam): add pwned password validation (#29)
Browse files Browse the repository at this point in the history
* [ADD] pwned

* [ADD] types sha1

* [REF] hibp integration

* lint: use prettier and better wording

---------

Co-authored-by: Gustavo Valverde <g.valverde02@gmail.com>
  • Loading branch information
JE1999 and gustavovalverde authored May 20, 2023
1 parent 63c6b15 commit 91ef75d
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 47 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"cryptr": "^6.2.0",
"eslint": "^8.40.0",
"eslint-config-next": "13.2.3",
"hibp": "^13.0.0",
"next": "13.2.3",
"prop-types": ">=15.7.0",
"react": "18.2.0",
Expand Down
2 changes: 1 addition & 1 deletion src/components/elements/boxContentCenter/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function BoxContentCenter({ children }: any) {
alignItems: "center",
}}
>
<Box sx={{ width: "100%", maxWidth: "550px", padding: "0 10px" }}>
<Box sx={{ width: "100%", maxWidth: "560px", padding: "0 10px" }}>
{children}
</Box>
</Box>
Expand Down
24 changes: 24 additions & 0 deletions src/pages/api/pwned/[password].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextApiRequest, NextApiResponse } from 'next/types';
import { pwnedPassword } from 'hibp';
import { Crypto } from '@/helpers';

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<number | void>
): Promise<void> {
const { token } = req.cookies;

if (token !== process.env.NEXT_PUBLIC_COOKIE_KEY) {
return res.status(401).send();
}

const { password } = req.query;

if (typeof password !== 'undefined') {
const passwordKey = Array.isArray(password) ? password[0] : password;
const data = await pwnedPassword(Crypto.decrypt(passwordKey));
res.status(200).json(data);
} else {
res.status(400);
}
}
2 changes: 1 addition & 1 deletion src/pages/register/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function Index() {
<CardAuth
title="Registrar Cuenta Única Ciudadana"
landing={LandingChica2}
landingWidth={290}
landingWidth={320}
landingHeight={290}
>
<StepperRegister />
Expand Down
101 changes: 59 additions & 42 deletions src/pages/register/stepper/step3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { useState } from 'react';
import * as yup from 'yup';
import axios from 'axios';

import { AlertError, AlertWarning } from '@/components/elements/alert';
import {
AlertError,
AlertErrorMessage,
AlertWarning,
} from '@/components/elements/alert';
import { GridContainer, GridItem } from '@/components/elements/grid';
import LoadingBackdrop from '@/components/elements/loadingBackdrop';
import PasswordLevel from '@/components/elements/passwordLevel';
Expand Down Expand Up @@ -42,14 +46,16 @@ const schema = yup.object({
});

export default function Step3({ handleNext, infoCedula }: any) {
const [loadingValidatingPassword, setLoadingValidatingPassword] =
useState(false);
const [loading, setLoading] = useState(false);
const [passwordLevel, setPasswordLevel] = useState<any>({});
const [isPwned, setIsPwned] = useState(false);

const {
register,
handleSubmit,
formState: { errors },
getValues,
setValue,
} = useForm<IFormInputs>({
mode: 'onChange',
Expand All @@ -60,36 +66,58 @@ export default function Step3({ handleNext, infoCedula }: any) {
const level = passwordStrength(password);
setPasswordLevel(level);
setValue('password', password);
setIsPwned(false);
};

const onSubmit = (data: IFormInputs) => {
if (isPwned) return;
if (passwordLevel.id !== 3) return;
setLoading(true);
setLoadingValidatingPassword(true);

const password = Crypto.encrypt(data.password);

axios
.post('/api/iam', {
email: data.email,
username: infoCedula.id,
password,
})
.then(() => {
handleNext();
})
.catch((err) => {
if (err?.response?.status === 409) {
AlertWarning('El correo electrónico ya está registrado.');
} else {
AlertError();
}
})
.finally(() => setLoading(false));
if (!isPwned) {
axios
.get(`/api/pwned/${password}`)
.then((res) => {
console.log(res);
const isPwnedIncludes = res.data === 0 ? false : true;
setIsPwned(isPwnedIncludes);
if (!isPwnedIncludes) {
setLoadingValidatingPassword(false);
setLoading(true);
axios
.post('/api/iam', {
email: data.email,
username: infoCedula.id,
password,
})
.then(() => {
handleNext();
})
.catch((err) => {
if (err?.response?.status === 409) {
AlertWarning('El correo electrónico ya está registrado.');
} else {
AlertError();
}
})
.finally(() => setLoading(false));
}
})
.catch(() => {
return AlertWarning('No pudimos validar si la contraseña es segura.');
})
.finally(() => setLoadingValidatingPassword(false));
}
};

return (
<>
{loading && <LoadingBackdrop text="Estamos creando tu usuario..." />}
{loadingValidatingPassword && (
<LoadingBackdrop text="Estamos validando tu contraseña..." />
)}
{loading && <LoadingBackdrop text="Creando usuario..." />}
<br />
<TextBody textCenter bold>
Para finalizar tu registro completa los siguientes campos:
Expand Down Expand Up @@ -166,10 +194,6 @@ export default function Step3({ handleNext, infoCedula }: any) {
<InputApp
placeholder="*********"
type="password"
onPaste={(e) => {
e.preventDefault();
return false;
}}
autoComplete="off"
{...register('password')}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
Expand All @@ -189,31 +213,24 @@ export default function Step3({ handleNext, infoCedula }: any) {
<InputApp
placeholder="*********"
type="password"
onCopy={(e) => {
e.preventDefault();
return false;
}}
autoComplete="off"
{...register('passwordConfirm')}
disabled={passwordLevel.id === 3 ? false : true}
/>
</FormControlApp>
</GridItem>

{isPwned && (
<GridItem md={12} lg={12}>
<AlertErrorMessage
type="warning"
message="Esta contraseña ha estado en filtraciones de datos, por eso no se considera segura. Te recomendamos eligir otra contraseña."
/>
</GridItem>
)}

<GridItem md={12} lg={12}>
<ButtonApp
submit
disabled={
Object.values(getValues()).every(
(value: any) =>
value !== null && value !== undefined && value !== ''
) === false
? true
: false
}
>
ACEPTAR Y CONFIRMAR
</ButtonApp>
<ButtonApp submit>CREAR CUENTA ÚNICA</ButtonApp>
</GridItem>
</GridContainer>
</form>
Expand Down
Loading

0 comments on commit 91ef75d

Please sign in to comment.