Skip to content

Commit

Permalink
Improved server-side validation mechanism for reCAPTCHA token
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianLeChat committed Dec 22, 2024
1 parent 5bb405d commit 25bc393
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 69 deletions.
25 changes: 9 additions & 16 deletions app/[locale]/components/recaptcha.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Composant des services de vérification Google reCAPTCHA.
// Composant des services de vérification via Google reCAPTCHA.
//

"use client";
Expand All @@ -22,24 +22,17 @@ export default function Recaptcha()

// Déclaration des constantes.
const recaptchaUrl = new URL( "https://www.google.com/recaptcha/api.js" );
recaptchaUrl.searchParams.append(
"render",
process.env.NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY ?? ""
);
recaptchaUrl.searchParams.append( "render", process.env.NEXT_PUBLIC_RECAPTCHA_PUBLIC_KEY ?? "" );
recaptchaUrl.searchParams.append( "onload", "setupRecaptcha" );

// Activation des services Google reCAPTCHA au consentement des cookies.
const onConsent = useCallback(
( event: CustomEventInit<{ cookie: CookieValue }> ) =>
{
setRecaptcha(
event.detail?.cookie.categories.some(
( category: string ) => category === "security"
) ?? false
);
},
[]
);
const onConsent = useCallback( ( event: CustomEventInit<{ cookie: CookieValue }> ) =>
{
const categories = event.detail?.cookie.categories;
const isSecurity = categories?.some( ( category: string ) => category === "security" );

setRecaptcha( isSecurity ?? false );
}, [] );

// Vérification de la validité de l'utilisateur via Google reCAPTCHA.
const setupRecaptcha = useCallback( () =>
Expand Down
64 changes: 11 additions & 53 deletions middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { NextRequest, NextResponse } from "next/server";

import "./utilities/env";
import { getLanguages } from "./utilities/i18n";
import type { RecaptchaResponse } from "./interfaces/Recaptcha";
import { checkRecaptcha } from "./utilities/recaptcha";

// Typage des fichiers provenant de la base de données.
type FileWithVersions = Prisma.FileGetPayload<{
Expand Down Expand Up @@ -187,71 +187,29 @@ export default async function middleware( request: NextRequest )

// On vérifie également si le service reCAPTCHA est activé ou non
// et s'il s'agit d'une requête de type POST.
if (
process.env.NEXT_PUBLIC_RECAPTCHA_ENABLED === "true"
&& request.method === "POST"
)
{
// On traite les données du formulaire pour récupérer le jeton
// d'authentification reCAPTCHA transmis par l'utilisateur.
let token;

try
{
token = ( await request.formData() ).get( "1_recaptcha" );
}
catch
{
// Une erreur s'est produite lors de la récupération des données
// du formulaire.
return new NextResponse( null, { status: 400 } );
}

if ( !token )
{
// Le jeton d'authentification reCAPTCHA est manquant ou invalide.
return new NextResponse( null, { status: 400 } );
}
const isPostRequest = request.method === "POST";
const isRecaptchaEnabled = process.env.NEXT_PUBLIC_RECAPTCHA_ENABLED === "true";
const isValidRecaptchaRequest = isRecaptchaEnabled && isPostRequest;

// On effectue une requête à l'API de Google reCAPTCHA afin de vérifier
// la validité du jeton d'authentification auprès de leurs services.
const data = await fetch(
`https://www.google.com/recaptcha/api/siteverify?secret=${ process.env.RECAPTCHA_SECRET_KEY }&response=${ token }`,
{ method: "POST" }
);
if ( isValidRecaptchaRequest )
{
const response = await checkRecaptcha( request );

if ( data.ok )
if ( response )
{
// Si la requête a été traitée avec succès, on vérifie alors le
// résultat obtenu de l'API de Google reCAPTCHA sous format JSON.
const json = ( await data.json() ) as RecaptchaResponse;

if ( !json.success || json.score < 0.7 )
{
// En cas de score insuffisant ou si la réponse est invalide,
// on bloque la requête courante.
return new NextResponse( null, { status: 400 } );
}

// Dans le cas contraire et dans le cas où la requête a cherchée
// à accéder à l'API de Google reCAPTCHA, on retourne une réponse
// vide avec un code de statut 200.
if ( request.nextUrl.pathname === "/api/recaptcha" )
{
return new NextResponse( null, { status: 200 } );
}
return response;
}
}

// On créé enfin le mécanisme de gestion des langues et traductions.
// Source : https://next-intl-docs.vercel.app/docs/getting-started/app-router-server-components
const handleI18nRouting = createIntlMiddleware( {
const i18nRouting = createIntlMiddleware( {
locales: getLanguages(),
localePrefix: "never",
defaultLocale: "en"
} );

return handleI18nRouting( request );
return i18nRouting( request );
}

export const config = {
Expand Down
68 changes: 68 additions & 0 deletions utilities/recaptcha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//
// Mécanisme de vérification de la validité du jeton reCAPTCHA.
//
import type { RecaptchaResponse } from "@/interfaces/Recaptcha";
import { NextResponse, type NextRequest } from "next/server";

export async function checkRecaptcha( request: NextRequest ): Promise<NextResponse | undefined>
{
// On traite d'abord le corps de la requête sous format JSON
// pour obtenir le jeton reCAPTCHA transmis par l'utilisateur.
let token;

try
{
const json = await request.json();
token = ( json as { token: string } ).token;
}
catch
{
// Une erreur s'est produite lors de la transformation du corps de
// la requête sous format JSON.
return new NextResponse( null, { status: 400 } );
}

if ( !token )
{
// Le jeton reCAPTCHA est manquant ou invalide.
return new NextResponse( null, { status: 400 } );
}

// On effectue ensuite une requête à l'API de Google reCAPTCHA
// afin de vérifier la validité du jeton auprès de leurs services.
const data = await fetch(
`https://www.google.com/recaptcha/api/siteverify?secret=${ process.env.RECAPTCHA_SECRET_KEY }&response=${ token }`,
{ method: "POST" }
);

if ( data.ok )
{
// Si la requête a été traitée avec succès, on vérifie alors le
// résultat obtenu de l'API de Google reCAPTCHA sous format JSON.
const json = ( await data.json() ) as RecaptchaResponse;
const isInvalidResponse = !json.success || json.score < 0.7;

if ( isInvalidResponse )
{
// En cas de score insuffisant ou si la réponse est invalide,
// on bloque la requête courante.
return new NextResponse( null, { status: 400 } );
}

// Dans le cas contraire et dans le cas où la requête a cherchée
// à accéder à l'API de Google reCAPTCHA, on retourne une réponse
// vide avec un code de statut 200.
if ( request.nextUrl.pathname === "/api/recaptcha" )
{
return new NextResponse( null, { status: 200 } );
}
}
else
{
// On retourne enfin une erreur si un problème est survenu lors de la
// vérification de la validité du jeton auprès des services de Google.
return new NextResponse( null, { status: 500 } );
}

return undefined;
}

0 comments on commit 25bc393

Please sign in to comment.