Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate ReCaptcha v3 / v2 with SvelteKit Superforms #323

Open
itaibaruch opened this issue Feb 5, 2024 · 3 comments
Open

Integrate ReCaptcha v3 / v2 with SvelteKit Superforms #323

itaibaruch opened this issue Feb 5, 2024 · 3 comments

Comments

@itaibaruch
Copy link

Hi team,

Thank you for a great package.

I have tried to integrate ReCaptcha with your package, but I don't know what is the best way to implement it.
can you provide example on the site or in the repo.

Thanks.

@dookanooka
Copy link

I've been trying to do this as well. The problem i'm having is getting the token on the client and sending it to an action on the server.

I tried using onSubmit with an async..await to get the ReCaptcha token on the client, add it to formData and submit but that didn't work.

I went outside use:enhance with a function call via on:submit|preventDefault={onFormSubmit} but it gets executed too late for the server action to get the token to query Google.

I need the server action to hold off until the client side function call has completed. I thought simple enough, but proving tricky with Superforms in the mix.

@dookanooka
Copy link

Had this working quite shortly after my last post as below, getting into the swing of Superforms, it's a very nice package :)

I say yes to 'browser error' from Google, not a good idea but hey, it's generally just a guide.

I added localhost to the approved domains on the Google side for development.

+page.svelte

	const { form, errors, enhance } = superForm(data.form, {
		onSubmit(cancel) {
			onFormSubmit(cancel)
		},
	})
	/** @param {{ currentTarget: EventTarget & HTMLFormElement}} event */
	const onFormSubmit = async ({ cancel }) => {
		const formStatus = await superValidate($form, zod(lostPasswordSchema))

		if (!formStatus.valid) {
			cancel()
			return fail(400, {
				formStatus,
			})
		} else {
			try {
				state = State.requesting
				await window.grecaptcha.ready(async function () {
					const token = await window.grecaptcha
						.execute(PUBLIC_RECAPTCHA_SITE_KEY, {
							action: 'submit',
						})
						.then(async function (t) {
							$form.recaptchaToken = t
							const response = await fetch('/api/lostPassword', {
								method: 'POST',
								body: JSON.stringify($form),
								headers: {
									'content-type': 'application/json',
								},
							})
							/** @type {import('@sveltejs/kit').ActionResult} */
							const result = deserialize(await response.text())
							if (result.type == 'success') {
								// Do stuff if its good
							} else {
								toastStore.trigger(errEmail)
							}
							return result
						})
				})
			} catch (error) {
				console.log(`ERROR: ${error}`)
				toastStore.trigger(errEmail)
			}
		}
	}

<form method="POST" class="mt-8 space-y-8" use:enhance>

/api/lostpassword/+server.ts

import { actionResult } from 'sveltekit-superforms';
import { SECRET_RECAPTCHA_KEY } from '$env/static/private';

/**
 * This function is used to verify the reCAPTCHA token received from the client.
 * It sends a POST request to Google's reCAPTCHA API and checks the response.
 * 
 * @async
 * @param {Object} event - The event object containing the client's request.
 * @param {Object} event.request - The client's request.
 * @param {Function} event.getClientAddress - Function to get the client's IP address.
 * 
 * @returns {Promise<Object>} Returns an object with the result of the operation. 
 * If the reCAPTCHA token is successfully verified, it returns an object with a success message. 
 * If the verification fails, it returns an object with an error message.
 * 
 * @throws {Error} If the hostname is 'localhost' and the environment is not development, 
 * an error is thrown indicating that the operation is not permitted.
 */
export async function POST(event) {
    const clientIp = event.getClientAddress();
    const data = await event.request.json()
    const isDev = process.env.NODE_ENV === 'development';

    /* Google verify recaptcha */
    const postData = new URLSearchParams();
    postData.append('secret', SECRET_RECAPTCHA_KEY);
    postData.append('response', data.recaptchaToken);
    postData.append('remoteip', clientIp);
    // Make the request
    const response = await fetch('https://www.google.com/recaptcha/api/siteverify', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: postData
    });
    const captchaData = await response.json();
    if (captchaData.hostname === 'localhost' && !isDev) {
        return actionResult('failure', { error: 'Operation not permitted for localhost in production' });
    }
    else if (captchaData.success && captchaData.score > 0.6 && captchaData.action === 'submit') {
        return actionResult('success', { text: `Good score returned from Google` });
    } else if (captchaData["error-codes"][0] == "browser-error") {
        return actionResult('success', { text: `Google returned browser-error` });
    } else {
        return actionResult('failure', { error: `Failed to verify the token` });
    }
}

@itaibaruch
Copy link
Author

itaibaruch commented Feb 22, 2024

I solve it the following way:

$lib/helpers/recaptcha.ts

import { PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY } from '$env/static/public';

export async function validateReCaptchaServer(
	token: string,
	fetch: typeof window.fetch,
	secret: string
) {
	const res = await fetch('https://www.google.com/recaptcha/api/siteverify', {
		method: 'POST',
		headers: {
			'content-type': 'application/x-www-form-urlencoded',
		},
		body: `secret=${secret}&response=${token}`,
	});
	const json = await res.json();

	return json;
}

export async function createReCaptchaClient(
	formToken: string | undefined,
	grecaptcha: ReCaptchaV2.ReCaptcha
) {
	return new Promise((resolve) => {
		if (formToken) {
			resolve(formToken);
		} else {
			return grecaptcha.ready(function () {
				grecaptcha.execute(PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY, { action: 'submit' }).then(function (
					token: string
				) {
					resolve(token);
				});
			});
		}
	});
}

+page.svelte

<script lang="ts">
        import { createReCaptchaClient } from '$lib/helpers/recaptcha';
	const { form, errors, constraints, enhance, message } = superForm(data.contactForm, {
		// Reset the form upon a successful result
		resetForm: true,
		onSubmit: async ({ formData }) => {
			const token = await createReCaptchaClient($form.token, window.grecaptcha);
			formData.append('token', String(token));
		},
	});
</script>

<svelte:head>
	<script
		src="https://www.google.com/recaptcha/api.js?render={PUBLIC_GOOGLE_RECAPTCHA_SITE_KEY}"
		async
		defer
	></script>
</svelte:head>

<form method="POST" action="?/contact" use:enhance>
.....form

+page.server.ts

import { validateReCaptchaServer } from '$src/lib/helpers/recaptcha';

export const actions: Actions = {
	contact: async (event) => {
		const { request, fetch } = event;
		const form = await superValidate(request, contactSchema);

		if (!form.valid) {
			return fail(400, { form });
		}

		// reCAPTCHA
		const gToken = form.data.token;
		if (!gToken) {
			return message(form, 'Invalid reCAPTCHA', { status: 400 });
		}
		const res = await validateReCaptchaServer(gToken, fetch, GOOGLE_RECAPTCHA_SECRET_KEY);
		if (!res.success) {
			return message(form, 'Failed ReCaptcha', { status: 400 });
		}
....

Hope it helps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants