Integracja hCaptcha z next.js

Wstęp
HCaptcha to firma tworząca mechanizmy broniące przed oszustwami, wykrywające boty, mechanizmy wykrywania przejęcia kont użytkowników i zwiększające ochronę prywatności. W poniższym artykule zajmiemy się ochroną formularza kontaktowego na stronie przed spamem wysyłanym przez boty, które wypełniają formularze automatycznie.
Podczas Twojej codziennej podróży po Internecie na pewno spotkałeś/aś się już z checkboxem "Jestem człowiekiem", który po jego zaznaczeniu daje specjalne zadanie do wykonania np. zaznaczenie czegoś na obrazku, odczytanie znaków itd. Jest to jeden z elementów mechanizmu hCaptcha, broniący serwis przed botami. Inne popularne rozwiązanie to reCaptcha od Google.
Dzięki jego zastosowaniu na formularzu, możemy ochronić nasz serwis przed botami szukającymi luk w zabezpieczeniach np. bot testuje wszystkie możliwe warianty wypełnienia formularza, tak aby otrzymać pozytywną decyzję kredytową. Silniki decyzyjne banków są oczywiście chronione przed takimi botami.
Możemy dzięki temu uniknąć, też niebezpiecznych kosztów. Wyobraźmy sobie, że wypełnienie formularza powoduje wysłanie SMSa o pewnym koszcie. Taki bot może wysyłać kilka takich zapytań na sekundę, a co gorsza może nas atakować kikanaście takich botów. To może doprowadzić do kosztów, które nie będą dla nas przyjemne ;)
Jak to działa?
Podczas wizyty na naszej strony internetowej użytkownik pobiera kod hCaptchy do swojej przeglądarki. HCaptcha wysyła na swój serwer zapytanie o zadanie do rozwiązanie razem z kodem (passcode). Po pozytywnym rozwiązaniu zadania kod (passcode) zostaje przesłany razem z danymi formularza na nasz serwer. Tam passcode zostaje wysłany na serwer hCaptcha do weryfikacji i otrzymujemy informację na nasz serwer czy użytkownik jest botem, czy też nie.
Implementacja w kodzie
Wykorzystamy prostą bibliotekę @hcaptcha/react-hcaptcha zaczniemy od instalacji:
npm install @hcaptcha/react-hcaptcha
UWAGA: hCaptchy nie da się testować na localhost, dlatego użyjemy sugerowanych w dokumentacji kluczy które nazwałem:
FAKE_SITE_KEY, FAKE_SECRET_KEY i H_CAPTCHA_FAKE_RESPONSE.
Klucze wygenerujesz lub zostaną przydzielone do Twojego konta hCaptcha.
FAKE_SITE_KEY - twój klucz identyfikujący stronę.
FAKE_SECRET_KEY - twój klucz do api hCaptcha.
H_CAPTCHA_FAKE_RESPONSE - odpowiedź na nasz passcode z serwera hCaptcha.
Zestaw takich wartości:
Site Key 10000000-ffff-ffff-ffff-000000000001
Secret Key 0x0000000000000000000000000000000000000000
Response Token 10000000-aaaa-bbbb-cccc-000000000001
zawsze zwróci nam obiekt
{success: true}
czyli uznajemy, że weryfikacja przeszła pomyślnie. Do testów na środowisku lokalnym użyjemy tych wartości, a na środowiskach testowych i produkcyjnym, gdzie mamy już swoją domenę, użyjemy rzeczywistch kluczy.
Stwórzmy komponent, który będziemy mogli użyć w wielu różnych miejscach w przyszłości do zabezpieczania, różnych elementów aplikacji.
import HCaptcha from '@hcaptcha/react-hcaptcha';
export interface CaptchaProps {
onToken: (token: string) => void;
}
const FAKE_SITE_KEY = '10000000-ffff-ffff-ffff-000000000001';
// wyjaśnione wyżej
export const Captcha = ({ onToken }:CaptchaProps) => (
<HCaptcha
{/* w zależności od środowiska pobieramy wartość sitekey z envów
lub ze zmiennej powyżej*/}
sitekey={
process.env.NODE_ENV === 'development'
? FAKE_SITE_KEY
: (process.env.NEXT_PUBLIC_SITE_KEY as string)
}
onExpire={() => onToken('')}
{/* funkcja onToken służy nam jako callback
z zewnątrz, po weryfikacji możemy ją
wykorzystać do ustawienia tokenu
w odpowiednim miejscu */}
onVerify={onToken}
{/* funkcja onError ustawi token na pusty
string w przypadku błędnej weryfikacji */}
onError={() => {
onToken('');
}}
/>
);
Stwórzmy komponent formularza, który zbierze dane wiadomości, będzie zabezpieczony hCaptchą i wyśle dane formularza na serwer. Formularz został maksymalnie uproszczony na potrzeby demonstracji. W realnym formularzu, warto też zadbać o obsługę błędów, obsłużyć stany ładowania i walidację.
import { Captcha } from '../Captcha/Captcha';
import { useForm } from 'react-hook-form';
const H_CAPTCHA_FAKE_RESPONSE = '10000000-aaaa-bbbb-cccc-000000000001';
export const ContactForm = () => {
const {
handleSubmit,
register,
setValue,
} = useForm();
const onSubmit: SubmitHandler<ContactFormData> = async (data) => {
// adres naszego nextowego endpointu
fetch('/api/emails', {
body: JSON.stringify({
message: data.message,
// w zależności od środowiska wysyłamy
// realny token, albo sztuczny
token: process.env.NODE_ENV === 'development' ? H_CAPTCHA_FAKE_RESPONSE : data.token,
}),
method: 'POST',
})
};
return (<form onSubmit={handleSubmit(onSubmit)}>
<textarea
id='message'
placeholder='Treść wiadomości'
{...register('message')}
/>
<Captcha
onToken={(token) => {
setValue('token', token);
}}
/>
<button>Wyslij</button>
</form>)
};
Po wypełnieniu formularza i kliknięciu na przycisk wyślij, wykonuje się funkcja onSubmit wysyłająca dane na endpoint /api/emails.
Na początek stwórzmy funkcję checkCaptcha, której będziemy mogli używać do zabezpieczania większej liczby endpointów w przyszłości.
Zgodnie z wymaganiami hCaptcha token i secret są wysyłane na endpoint serwera hCaptcha jako urlSearchParams.
const FAKE_SECRET_KEY = '0x0000000000000000000000000000000000000000';
export const checkCaptcha = async (token: string) => {
const response = await fetch('https://api.hcaptcha.com/siteverify', {
body: new URLSearchParams({
response: token,
secret:
process.env.NODE_ENV === 'development' ? FAKE_SECRET_KEY : (process.env.H_SECRET as string),
}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
method: 'POST',
});
const hCaptchaResponseJson = await response.json();
const { success } = hCaptchaResponseJson;
return success; // zawsze true albo false
};
Przejdźmy do endpointu. Tutaj też został on maksymalnie uproszczony na potrzeby prezentacji. W realnym przykładzie trzeba jeszcze dodać walidację. Tworzymy plik rote.ts w odpowiednim folderze np. /src/app/api/email/route.ts
import { NextRequest, NextResponse } from 'next/server';
export const POST = async (req: NextRequest) => {
const reqBody = await req.json();
const { message, token } = data;
// wykorzystanie wcześniej stworzonej funkcji
const isCaptchaOk = checkCaptcha(token);
if (!isCaptchaOk) {
return NextResponse.json({ message: 'Oops, you are a bot' }, { status: API_CODES.BAD_REQUEST });
}
// dalsza logika w endpoinicie
// w przykładzie mówimy o wysyłaniu maila,
// funkcja jego wysyłania nie jest ważna w kontekście tego artykułu,
// więc została zamknięta do funkcji sendEmail a jej implementację pozostawiam Tobie ;)
try{
const response=await sendEmail()
}catch(err){
return NextResponse.json(
{ message: 'Oops..' + JSON.stringify(error) },
{ status: 500 },
);
}
};
Podsumowanie
W tym artykule przyjrzeliśmy się jak lepiej zabezpieczyć nasz endpoint przed atakiem ze strony botów wykorzystując mechanizm oferowany przez firmę hCaptcha. Sposób implementacji dla reCaptchy od Google jest bardzo podobny do oferowanego przez firmę hCaptcha.