Skip to content

Commit

Permalink
Merge pull request #92 from SocialGouv/feat/security-user
Browse files Browse the repository at this point in the history
Feat: security user
  • Loading branch information
ClementNumericite authored Apr 26, 2024
2 parents 0b3657b + ce676b4 commit 8bb46e0
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 39 deletions.
13 changes: 12 additions & 1 deletion webapp-next/components/layouts/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import cookie from 'js-cookie';
import { hasAtLeastOneFilter, ELASTIC_API_KEY_NAME } from '@/utils/tools';
import { FilterAssociateCauses } from '../filters/AssociateCauses';
import { RegionFilter } from '../filters/Regions';
import { auth } from '../login/FormLogin';
import useSWRMutation from 'swr/mutation';

export const ageRanges = [
{ from: 0, to: 0, key: 'Moins de 1 an' },
Expand All @@ -36,6 +38,11 @@ export function Menu() {
throw new Error('Menu must be used within a Cm2dProvider');
}

const { trigger: triggerInvalidateApiKey } = useSWRMutation(
"/api/auth/invalidate-api-key",
auth<{ username: string }>
);

const { filters, setFilters, user } = context;

return (
Expand Down Expand Up @@ -166,7 +173,11 @@ export function Menu() {
icon: '/icons/log-out.svg',
onClick: () => {
cookie.remove(ELASTIC_API_KEY_NAME);
window.location.reload();
triggerInvalidateApiKey({
username: context.user.username as string
}).then(() => {
window.location.reload();
});
},
link: '/'
}
Expand Down
57 changes: 43 additions & 14 deletions webapp-next/components/login/FormLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import useSWRMutation from "swr/mutation";
import { ELASTIC_API_KEY_NAME } from "@/utils/tools";
import { ContentCGU } from "@/pages/legals/cgu";

async function auth<T>(url: string, { arg }: { arg: T }) {
export async function auth<T>(url: string, { arg }: { arg: T }) {
return fetch(url, {
method: "POST",
body: JSON.stringify(arg),
Expand All @@ -57,7 +57,8 @@ export const FormLogin = () => {
const [isLoading, setIsLoading] = useState(false);
const [showCodeForm, setShowCodeForm] = useState(false);

const [remaningRequests, setRemaningRequests] = useState(0);
const [remaningRequestsLogin, setRemaningRequestsLogin] = useState(0);
const [remaningRequestsOTP, setRemaningRequestsOTP] = useState(0);

const [timer, setTimer] = useState(30);
const intervalRef = useRef<NodeJS.Timeout | undefined>();
Expand Down Expand Up @@ -113,7 +114,9 @@ export const FormLogin = () => {
code: code.toString(),
})) as any;
const result = await res.json();
cookie.set(ELASTIC_API_KEY_NAME, result.apiKey.encoded);
cookie.set(ELASTIC_API_KEY_NAME, result.apiKey.encoded, {
expires: 1,
});
onCloseTerms();
router.push("/bo");
}
Expand All @@ -134,14 +137,21 @@ export const FormLogin = () => {
if (result.firstLogin) {
onOpenTerms();
} else {
cookie.set(ELASTIC_API_KEY_NAME, result.apiKey.encoded);
cookie.set(ELASTIC_API_KEY_NAME, result.apiKey.encoded, {
expires: 1,
});
router.push("/bo");
}
setIsLoading(false);
} else {
setFormError(true);
setTimeout(() => {
setRemaningRequestsOTP(
parseInt(res.headers.get("X-RateLimit-Remaining") as string) || 0
);
setFormError(true);
setIsLoading(false);
}, 1000);
}

setIsLoading(false);
}
};

Expand All @@ -154,15 +164,17 @@ export const FormLogin = () => {
if (res.ok) {
const result = await res.json();
if (process.env.NODE_ENV === "development") {
cookie.set(ELASTIC_API_KEY_NAME, result.encoded);
cookie.set(ELASTIC_API_KEY_NAME, result.encoded, {
expires: 1,
});
router.push("/bo");
}
startTimer();
setShowCodeForm(true);
setIsLoading(false);
} else {
setTimeout(() => {
setRemaningRequests(
setRemaningRequestsLogin(
parseInt(res.headers.get("X-RateLimit-Remaining") as string) || 0
);
setFormError(true);
Expand Down Expand Up @@ -217,7 +229,24 @@ export const FormLogin = () => {
<Box mb={8}>
<Alert status="error" mb={4}>
<AlertIcon />
<AlertTitle>Code incorrect</AlertTitle>
<Box>
<AlertTitle>
{remaningRequestsOTP === 0
? "Taux de limite atteint"
: "Code incorrect"}
</AlertTitle>
{remaningRequestsOTP === 0 ? (
<AlertDescription>
Vous avez atteint le nombre maximum de tentatives, veuillez
réessayer dans 1 minute.
</AlertDescription>
) : (
<AlertDescription>
Il vous reste {remaningRequestsOTP} essai
{remaningRequestsOTP > 1 && "s"} !
</AlertDescription>
)}
</Box>
</Alert>
</Box>
)}
Expand Down Expand Up @@ -318,19 +347,19 @@ export const FormLogin = () => {
<AlertIcon />
<Box>
<AlertTitle>
{remaningRequests === 0
{remaningRequestsLogin === 0
? "Taux de limite atteint"
: "Erreurs dans les identifiants !"}
</AlertTitle>
{remaningRequests === 0 ? (
{remaningRequestsLogin === 0 ? (
<AlertDescription>
Vous avez atteint le nombre maximum de tentatives, veuillez
réessayer dans 1 minute.
</AlertDescription>
) : (
<AlertDescription>
Il vous reste {remaningRequests} essai
{remaningRequests > 1 && "s"} !
Il vous reste {remaningRequestsLogin} essai
{remaningRequestsLogin > 1 && "s"} !
</AlertDescription>
)}
</Box>
Expand Down
38 changes: 38 additions & 0 deletions webapp-next/pages/api/auth/invalidate-api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Client } from "@elastic/elasticsearch";
import fs from "fs";
import type { NextApiRequest, NextApiResponse } from "next";
import path from "path";

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === "POST") {
const { username } = req.body;

const adminClient = new Client({
node: process.env.ELASTIC_HOST,
auth: {
username: process.env.ELASTIC_USERNAME as string,
password: process.env.ELASTIC_PASSWORD as string,
},
tls: {
ca: fs.readFileSync(path.resolve(process.cwd(), "./certs/ca/ca.crt")),
rejectUnauthorized: false,
},
});

try {
const invalidatedApiKey = await adminClient.security.invalidateApiKey({
username,
});

res.status(200).json(invalidatedApiKey);
} catch (error: any) {
res.status(500).end();
}
} else {
res.setHeader("Allow", "POST");
res.status(405).end("Method Not Allowed");
}
}
67 changes: 43 additions & 24 deletions webapp-next/pages/api/auth/verify-code.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,71 @@
import { Client } from '@elastic/elasticsearch';
import type { NextApiRequest, NextApiResponse } from 'next';
const tmpCodes = require('../../../utils/codes');
import fs from 'fs';
import path from 'path';
import { Client } from "@elastic/elasticsearch";
import type { NextApiRequest, NextApiResponse } from "next";
import fs from "fs";
import path from "path";
import rateLimit from "@/utils/rate-limit";
const tmpCodes = require("../../../utils/codes");

const limiter = rateLimit({
interval: 60 * 1000, // 60 seconds
uniqueTokenPerInterval: 50, // Max 50 users per second
});

export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
if (req.method === "POST") {
const forwarded = req.headers["x-forwarded-for"];

const userIp =
typeof forwarded === "string"
? forwarded.split(/, /)[0]
: req.socket.remoteAddress;

// Rate limiting to prevent brute force auth
try {
await limiter.check(res, 5, userIp as string); // 5 requests max per minute
} catch (e: any) {
return res.status(e.statusCode).end(e.message);
}

const codeObj = tmpCodes[req.body.username];

if (codeObj && codeObj.code === req.body.code.toString()) {
let firstLogin = false;

const client = new Client({
node: process.env.ELASTIC_HOST,
auth: {
apiKey: codeObj.apiKey.encoded
apiKey: codeObj.apiKey.encoded,
},
tls: {
ca: fs.readFileSync(path.resolve(process.cwd(), './certs/ca/ca.crt')),
rejectUnauthorized: false
}
ca: fs.readFileSync(path.resolve(process.cwd(), "./certs/ca/ca.crt")),
rejectUnauthorized: false,
},
});

try {
await client.get({
index: 'cm2d_users',
id: req.body.username
index: "cm2d_users",
id: req.body.username,
});
} catch (e) {
firstLogin = true;
}

res
.status(200)
.json({
apiKey:
firstLogin && process.env.NODE_ENV !== 'development'
? undefined
: codeObj.apiKey,
firstLogin
});
res.status(200).json({
apiKey:
firstLogin && process.env.NODE_ENV !== "development"
? undefined
: codeObj.apiKey,
firstLogin,
});
} else {
res.status(401).end('Unauthorized');
res.status(401).end("Unauthorized");
}
} else {
res.setHeader('Allow', 'POST');
res.status(405).end('Method Not Allowed');
res.setHeader("Allow", "POST");
res.status(405).end("Method Not Allowed");
}
}

0 comments on commit 8bb46e0

Please sign in to comment.