Integrating captcha verification with SvelteKit and hCaptcha
March 30, 2023
Integrating captcha verification with SvelteKit and hCaptcha

When you allow users to post content or trigger an API call, you usually want to make sure that the action is actually submitted by a human being. Based on your needs, it's important to implement several strategies to mitigate risks. It's important to understand that a captcha is probably not the only strategy you want in place, but it is an important one. Let's see how to do that with SvelteKit.


hCaptcha

hCaptcha is an alternative to google's reCaptcha. It's privacy oriented, GPDR compliant and easy to use.

It has a free-tier that comes with some limitation but will be perfect for smaller traffic projects. If you have a commercial project that generates some income you might want to use their pro-tier in order to use their low-friction mode (users are not prompted to solve a puzzle every time).

Head over to hCaptcha in order to create an account. Once it's done you will need to create a new site in hCaptcha's interface.

Prerequisite

You'll need a SvelteKit project, of course. We will also use svelte-hcaptcha to make our task easier

	npm install svelte-hcaptcha --save-dev

We also need to set-up our .env variables. Go to your hCaptcha account. On your dashboard, go to your site and get the Site Key. This is our public facing key. Now, go to the settings and grab your secret key (this is a private key that you don't want exposed ANYWHERE, we will use it to verify the captchas on the backend).

Now, at the root of your project, add to your .env (or create an .env file if there isn't one). Now add your secrets.

	
	PRIVATE_CAPTCHA_KEY= 
	/*enter here your secret key*/  

	PUBLIC_SITEKEY=
	/*enter here your site key*/

Before you do anything, please make sure that your .env is added to your .gitignore file. This will allow us to exclude it from our commits. During your deployments you will need to use the appropriate method to add your secrets to the service you use.

SvelteKit allows us to specify if a variable is public facing or private. variables starting with PRIVATE_ will only be accessible to the backend and won't be shared with the client, while PUBLIC_ will be available by the client.

Creating the form

Alright, now we're ready to roll. In our page or component, we need to create a form. Before submit we want to make sure the captcha has been solved Let's do that

	
	<
	script>
	

	import 
	HCaptcha 
	from 
	'svelte-hcaptcha'

	import {
	PUBLIC_SITEKEY} 
	from 
	'$env/static/public'


	const key = 
	PUBLIC_SITEKEY


	</
	script>



	<
	form 
	method=
	"POST" 
	action=
	"?/sendComment">

/*Here goes your form*/




	</
	form>

Let's take a closer look at what we've done so far.

  • We imported the Hcaptcha component, as well as our SITE_KEY environment variable. We then create a "key" variable to which we assign the value of PUBLIC_SITEKEY. The only reason is to make it shorter to use in our code.
  • We created a form element, added a POST method since we're sending data, and attributed a sendComment action to it

If you're wondering what this sendComment action is don't worry, we haven't implemented it yet.

Now, let's add our captcha component to our code inside our form

	
	<
	HCaptcha  
        
	sitekey=
	{key}  
        
	bind:this=
	{captcha}  
        
	on:success=
	{handleSuccess}  
        
	on:error=
	{handleError}  
/>

At this point ESLint should give you several red squiggles. This is normal, some variables and functions haven't been created yet. Let's take a look at those parameters

  • sitekey, as it's names indicates, is simply our hCaptcha sitekey. we are correctly passing that from our .env file
  • bind:this allows us to bind our captcha to a variable so we can later reset it
  • on:success and on:error allow us to trigger a function when the even is dispatched

If you want to learn more about svelte's events forwarding you can read about it here

Let's add all of that now

	
	let captcha;

	let token;

	let isDisabled = 
	true  
  

	const 
	handleError = (
	) => {  
    captcha.
	reset();  
}  

	const 
	handleSuccess = (
	payload) => {  
    token = payload.
	detail.
	token  
    isDisabled = 
	false;  
}
  • we added a captcha variable
  • we added a token variable. This will allow us to store the token sent by hCaptcha after it's been solved
  • we also added a disabled boolean variable, it will be useful to handle the state of our button down the line
  • we created a handleHerror function. It simply reset out captcha, but we could also log the error, send some feedback to the user etc. Feel free to add what you want there
  • we also created a handleSuccess function that will take the response payload from hCaptcha and store the token. It will allow change disabled to false since our captcha has been solved

Adding the submit button

we need to add a button of type submit to our form. It will trigger the action we set-up in our form tag (that we haven't written yet)

	
	<
	button 
	type=
	"submit" 
	disabled=
	{isDisabled}>Send
	</
	button>

The submit button is disabled by default because it's value is the one of the variable "isDisabled". When the captcha is solved, it's value will change to "true" and the user will be able to submit the form

Getting our token ready to be passed to the server

When our form is submitted, we will beed to pass the token, returned by hCaptcha, to our backend in order to verify it. The easiest solution is to simply add a hidden input in our form

	
	<
	input 
	type=
	"hidden" 
	name=
	"token" 
	value=
	{token}/>

When our captcha is solved it will trigger the handleSuccess method who will then store it. When the form is submitted we will then be able to easily pass our token to our backend.

So far this is how it should look :

	
	<
	script>
	

	import 
	HCaptcha 
	from 
	'svelte-hcaptcha'

	import {
	PUBLIC_SITEKEY} 
	from 
	'$env/static/public'

	const key = 
	PUBLIC_SITEKEY


	let captcha;

	let token;

	let isDisabled = 
	true  
  

	const 
	handleError = (
	) => {  
    captcha.
	reset();  
}  

	const 
	handleSuccess = (
	payload) => {  
    token = payload.
	detail.
	token  
    isDisabled = 
	false;  
}


	</
	script>



	<
	form 
	method=
	"POST" 
	action=
	"?/sendComment">

/*Here goes your form*/


	<
	HCaptcha  
        
	sitekey=
	{key}  
        
	bind:this=
	{captcha}  
        
	on:success=
	{handleSuccess}  
        
	on:error=
	{handleError}  
/>


	<
	button 
	type=
	"submit" 
	disabled=
	{isDisabled}>Send
	</
	button>




	</
	form>

Great. So let's slow down a bit and try to understand what we've done so far.

We created a form. This form has a submit button, who is disabled by default. It also has a captcha. When the captcha is solved, the button is then enabled and the user can send the form.

Wonderful.

Wonderful, but not enough ! It would be trivial to change the state of this button and bypass our captcha. This is why you should ALWAYS, ALWAYS, use backend verification.

Fortunately for us, hCaptcha allow us to take our captcha token and our secret key and send it to them. They can then verify if the captcha was indeed solved and send us a response. If the user hasn't solved the captcha then we ignore the request. Rude but necessary. Let's do that

Creating the backend function

First we need to create the method that will handle the captcha verification. in our route, we need to create our +page.server.ts file. If you don't know what that is take a look at the documentation page

We're now in the +page.server.ts you have created. Let's set-up our imports :

	
	import 
	type {
	Actions} 
	from 
	'@sveltejs/kit';  

	import {
	PRIVATE_CAPTCHA_KEY} 
	from 
	'$env/static/private'  

	import {
	PUBLIC_SITEKEY} 
	from 
	'$env/static/public'    

	import {error} 
	from 
	"@sveltejs/kit";


	const 
	verifyUrl: 
	string = 
	'https://hcaptcha.com/siteverify'

	const 
	key: 
	string = 
	PRIVATE_CAPTCHA_KEY  

	const 
	siteKey: 
	string = 
	PUBLIC_SITEKEY

Since we're now in a TypeScript file, we import our Actions types from SvelteKit. We then import our secret key and our site key and assign them to shorter-named variables like we have before.In addition we import sveltekit's error handling library in order to throw errors

we also create a verifyUrl variables who will store the url that will be required in order to verify our token

Now let's create our action :

	
	export 
	const 
	actions: 
	Actions = {
  
	sendComment: 
	async({
	request: 
	Request}) => {
  
  }
}

That's the basis of our action. Now we want to get our data using formData(). In this example I will only be getting our token, but this is where you would get all of the value of the form inputs

inside my action :

	
	const data = 
	await request.
	formData();

	const token = data.
	get(
	'token');

Alright. Now that our backend has the captcha token we can ask hCaptcha's server to verify it.

	
	if(!token) {  
    
	throw 
	error(
	403, {  
        
	message: 
	'No token'  
    })  
}

if no token was provided we will just throw and nothing else will happen.

	
	const body = 
	new 
	URLSearchParams({  
  
	secret: key,  
  
	response: token 
	as 
	string,  
  
	sitekey: siteKey  
})

We now prepare our body to be sent. We pass our secret key, the response token as well as the sitekey

Everything is ready, let's make the request.

	
	const response = 
	await 
	fetch(verifyUrl, {  
  
	method: 
	'POST',  
  
	credentials: 
	'omit',  
  
	headers: {  
    
	'Content-Type': 
	'application/x-www-form-urlencoded',  
  },  
  body,  
});

And now let's get the success status from the response :

	
	const {success} = 
	await response.
	json();

Now we know if the token has been verified or not

	
	if (success) {
  
	// do your thing...
} 
	else {
  
	throw 
	error(
	403, {
    
	message: 
	'access denied'
  })
}

That should look like this :

	
	import 
	type {
	Actions} 
	from 
	'@sveltejs/kit';  

	import {
	PRIVATE_CAPTCHA_KEY} 
	from 
	'$env/static/private'  

	import {
	PUBLIC_SITEKEY} 
	from 
	'$env/static/public'  

	import {error} 
	from 
	"@sveltejs/kit";  
  

	const 
	verifyUrl: 
	string = 
	'https://hcaptcha.com/siteverify'  

	const 
	key: 
	string = 
	PRIVATE_CAPTCHA_KEY  

	const 
	siteKey: 
	string = 
	PUBLIC_SITEKEY  
  

	export 
	const 
	actions: 
	Actions = {  
    
	sendComment: 
	async ({
	request: 
	Request}) => {  
        
	const data = 
	await request.
	formData();  
  
        
	const token = data.
	get(
	'token');  
        
	if (!token) {  
            
	throw 
	error(
	403, {  
                
	message: 
	'No token'  
            })  
        }  
        
	const body = 
	new 
	URLSearchParams({  
            
	secret: key,  
            
	response: token 
	as 
	string,  
            
	sitekey: siteKey  
        })  
        
	const response = 
	await 
	fetch(verifyUrl, {  
            
	method: 
	'POST',  
            
	credentials: 
	'omit',  
            
	headers: {  
                
	'Content-Type': 
	'application/x-www-form-urlencoded',  
            },            body,  
        });  
        
	const {success} = 
	await response.
	json();  
  
        
	if (success) {  
            
	// do your thing...  
        } 
	else {  
            
	throw 
	error(
	403, {  
                
	message: 
	'access denied'  
            })  
        }  
  
    }}

Neat ! Now there is one more thing we can do on the frontend and that's progressive enhancement. It will allow us to use javascript if allowed in order to get a result without reloading for example. If you;re not familiar with it I would suggest reading the documentation about progressive enhancement

In order to do that we need to modify our form opening tag and add use:enhance to it

	<form method=
	"post" action=
	"?/sendComment" 
	use:enhance={
	(
	{form}) => {  
  
    
	return 
	async ({ result }) => {  
  
        
	if (result.
	type === 
	'success') {  
            
	console.
	info(
	"success")  
            isDisabled = 
	true;  
            captcha.
	reset();  
            form.
	reset();  
  
        } 
	else {  
           
	console.
	error(
	"error");  
           isDisabled = 
	true;  
           captcha.
	reset();  
        }    }  
}}>

This is pretty straightforward. After the action has been triggered, the frontend will get in return a result. We can then let our frontend leverage that. If the result is 'success' we will reset the form and the captcha and disable the button. We could have an alert letting us know the form was submitted. On the other hand if the result is 'error' we reset the captcha and disable the button. This is also where we would let our user know that something went wrong.


This conclude this tutorial. If you have any question leave a comment and I'll try to help.

Add a comment.
250 characters left
mastodon github linkedin