Integrate Google reCAPTCHA v3 into Spring Boot and Vue 3 (Nuxt 3)

  • |
  • 25 May 2023
Post image

Integrating Google reCAPTCHA v3 with spring boot applications can greatly enhance security and protect against malicious activities, such as spam and bots. In this tutorial, we will learn how to integrate Google reCAPTCHA v3 with a Spring Boot backend and a Nuxt 3 frontend. By the end of this tutorial, you will be able to add reCAPTCHA v3 to your spring boot application and validate user interactions on both the server and client sides.

If you only need to see the code, here is the github link


Before diving into the code section, let’s take a look at the “what do we mean by reCAPTCHA?”

What is reCAPTCHA(Completely Automated Public Turing Test to Tell Computers and Humans Apart)?

reCAPTCHA protects your website from fraud and abuse without creating friction.It uses adaptive challenges to keep malicious software from engaging in abusive activities on your website. Meanwhile, legitimate users will be able to login, make purchases, view pages, or create accounts and fake users will be blocked.

In short, reCAPTCHA’s purpose is to block malicious request from your application.

For instance sending 100.000 login request in 1 min (or even 10 min) can be malicious request

Difference(s) between Google reCAPTCHA v2 and v3?

reCaptcha V2

reCaptcha V2 comes in two different flavors:

  • I’m not a robot checkbox requires a user to click a field to confirm whether they are human or bot
i am not robot
  • Invisible reCaptcha does not need any additional fields. Instead, it integrates into already existing buttons on a web page. The integration requires a JavaScript callback when reCaptcha verification is complete.

reCaptcha V3

reCAPTCHA v3 returns a score for each request without user friction. The score is based on interactions with your site and enables you to take an appropriate action for your site

The score ranges from 0.0 to 1.0. If score is 1.0, then means that request send by legitimate user. 0.0 means no human can act like that. And everything in between is a suspicion range.

Setting up Google reCAPTCHA

I am not going into detail of how to setup Google reCAPTCHA in google console. Because Google can change the way you setup reCAPTCHA over the years. In this tutorial i am assuming that you already have Site Key and Secret Key. These keys will be given after you setup recaptcha in google console.

And don’t forget to set domain for localhost development. You should add at least two domains: localhost & 127.0.0.1

reCAPTCHA token generation and flow for login request

Let me summarize the reCAPTCHA flow for the login process:

recaptcha_login_flow

Setting up Spring Boot

I am assuming that you have already created spring boot project with the following dependency: Spring Web, Spring Security & Lombok

GoogleRecaptchaRequest

First, create the following object to send request between layers (from filter to service and more). Don’t forget to set secret key in the constructor

@Getter
@Setter
public class GoogleRecaptchaRequest {
    private String secret;
    private String response;
    private String remoteip;

    public GoogleRecaptchaRequest() {
        this.secret = // secret key for recaptcha
    }
}

GoogleRecaptchaResponse

After sending the request to the google, google will return the response (with includes many fields). We can wrap these fields into a class

To get more information about api response from the Google, checkout https://developers.google.com/recaptcha/docs/verify

@Data
@ToString
public class GoogleRecaptchaResponse {
    private boolean success;
    private String challenge_ts;
    private String hostname;
    @JsonProperty("error-codes")
    private String[] errorCodes;
    private Double score;
    private String action;
}

Configure RestTemplate

Because we are going to send request to google, we need to configure the restTemplate to use anywhere in the spring context:

@Configuration
public class RestTemplateConfiguration {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }
}

Create service to verify recaptcha

Create GoogleRecaptchaService which has only one method verifyRecaptcha(request):

  • This method takes a reCAPTCHA request, sends it to the reCAPTCHA verification endpoint, and processes the response, including handling errors and checking the score against a threshold value.
@Service
@Slf4j
@RequiredArgsConstructor
public class GoogleRecaptchaService {
    // if score is lower than 0.5, means that request is malicious
    // threshold value is up to you and can change according to the your business logic
    private static final double SCORE_THRESHOLD = 0.5;

    private static final String RECAPTCHA_VERIFY_ADDRESS = "https://www.google.com/recaptcha/api/siteverify";


    private final ObjectMapper objectMapper;
    private final RestTemplate restTemplate;


    public GoogleRecaptchaResponse verifyRecaptcha(GoogleRecaptchaRequest request) {
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

            MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
            map.add("secret", request.getSecret());
            map.add("response", request.getResponse());
            map.add("remoteip", request.getRemoteip());
            HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
            ResponseEntity<GoogleRecaptchaResponse> response = restTemplate.exchange(RECAPTCHA_VERIFY_ADDRESS,
                    HttpMethod.POST,
                    entity,
                    GoogleRecaptchaResponse.class);
            GoogleRecaptchaResponse googleResponse = response.getBody();
            if (Objects.isNull(googleResponse)) {
                throw new RuntimeException("Google response is null");
            }
            if (Objects.nonNull(googleResponse.getScore()) && googleResponse.getScore() < SCORE_THRESHOLD) {
                log.warn("User score is lower than threshold. GoogleResponse :: {}", googleResponse);
                googleResponse.setSuccess(false);
            }
            return googleResponse;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Create and register GoogleRecaptchaFilter

  • This filter will extend the OncePerRequestFilter class, which is a base class for filters that are only supposed to be applied once per request.
  • I will use the HandlerExceptionResolver, to resolve exceptions that occur during the filter execution.
  • Inside the doFilterInternal method, first retrieves the reCAPTCHA response token from the request’s header using the key google-recaptcha-token. If the token is blank (empty or null), it logs a warning message indicating that the Google response is null for the current request URL.
  • Call the verifyRecaptcha method of the googleRecaptchaService instance, passing the googleRecaptchaRequest object, to verify the reCAPTCHA request.
    • If response from google isn’t success, that’s means incoming request is malicious, then throws an error with proper message
    • else, call the doFilter method of the filterChain object to continue the filter chain and proceed to the next filters in the chain.
  • If an exception occurs during the execution of the filter, catch the exception, and then calls the resolveException method of the resolver instance to handle the exception and generate an appropriate response.
/**
 * GoogleRecaptchaFilter class is responsible for filtering requests,
 * extracting the reCAPTCHA response token from the request header,
 * verifying the reCAPTCHA response using the googleRecaptchaService,
 * and either allowing the request to proceed
 * or generating an error response based on the verification result.
 */
@RequiredArgsConstructor
@Slf4j
public class GoogleRecaptchaFilter extends OncePerRequestFilter {
    private final GoogleRecaptchaService googleRecaptchaService;
    private final HandlerExceptionResolver handlerExceptionResolver;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String googleResponse = request.getHeader("google-recaptcha-token");
            if (StringUtils.isBlank(googleResponse)) {
                log.warn("Google response is null for the request :: {}", request.getRequestURL());
            }
            GoogleRecaptchaRequest googleRecaptchaRequest = new GoogleRecaptchaRequest();
            googleRecaptchaRequest.setResponse(googleResponse);
            googleRecaptchaRequest.setRemoteip(request.getRemoteAddr());

            GoogleRecaptchaResponse googleRecaptchaResponse = googleRecaptchaService.verifyRecaptcha(googleRecaptchaRequest);
            if (!googleRecaptchaResponse.isSuccess()) {
                log.error("Google response is not success :: {}", googleRecaptchaResponse);
                throw new RuntimeException("We have detected unusual activities from your browser. Please login again after a few minutes later");
            }
            filterChain.doFilter(request, response);
        } catch (Exception ex) {
            handlerExceptionResolver.resolveException(request, response, null, ex);
        }
    }

    /**
     * shouldNotFilter checks if the request URL contains the string "login"
     * and returns **false** if it does.
     * This means that the filter will be run for requests related to the login process.
     */
    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        return !request.getRequestURL().toString().contains("login");
    }
}

To register the filter as a bean, create an configuration class and define GoogleRecaptchaFilter as a bean:

@Configuration
@RequiredArgsConstructor
public class AppConfiguration {
    private final GoogleRecaptchaService googleRecaptchaService;
    private final HandlerExceptionResolver handlerExceptionResolver;

    @Bean
    public GoogleRecaptchaFilter googleRecaptchaFilter() {
        return new GoogleRecaptchaFilter(googleRecaptchaService, handlerExceptionResolver);
    }
}

Create dummy LoginController

For the demo purpose, LoginController will be too simple, it will just return dummy string indicates that resource has definitely been called.

@RestController
public class LoginController {

    @PostMapping(value = "/api/login")
    public String doLogin() {
        return "Login method has been called";
    }
}

Test the Spring Boot application

If you send the following curl request (request doesn’t include recatpcha token):

$ curl -X POST http://localhost:8080/api/login  -H "Content-Type: application/json"

# response
We have detected unusual activities from your browser. Please login again after a few minutes later

In the log, you can see the response from the google:

ERROR  c.m.r.filter.GoogleRecaptchaFilter       : Google response is not success :: GoogleRecaptchaResponse(success=false, challenge_ts=null, hostname=null, errorCodes=[invalid-input-response], score=null, action=null)

If you send the following curl request (request includes dummy token value), then you will get the same response:

$ curl -X POST http://localhost:8080/api/login  -H "Content-Type: application/json" -H "google-recaptcha-token: 1234"

# response
We have detected unusual activities from your browser. Please login again after a few minutes later
ERROR 243011  c.m.r.filter.GoogleRecaptchaFilter       : Google response is not success :: GoogleRecaptchaResponse(success=false, challenge_ts=null, hostname=null, errorCodes=[invalid-input-response], score=null, action=null)

Setting up Nuxt 3

Add SiteKey into .env file

After you initialize your nuxt 3 project, create file called .env and update it:

NUXT_PUBLIC_GOOGLE_RECAPTCHA_KEY= // recaptcha site key

And also update the nuxt.config.ts file to read property inside the .env

export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      googleRecaptchaKey: process.env.NUXT_PUBLIC_GOOGLE_RECAPTCHA_KEY,
    },
  },
});

After these setup, we can read the siteKey using useRuntimeConfig()

Install package vue-recaptcha-v3

It is a package for a simple and easy to use reCAPTCHA (v3 only) for Vue .

For more information https://www.npmjs.com/package/vue-recaptcha-v3

Install the package:

npm i vue-recaptcha-v3

Create recaptcha.ts to use vue-recaptcha-v3

In order to use vue-recaptcha-v3 inside the Nuxt 3 application, we need to register it as plugin.

Nuxt automatically reads the files in your plugins directory and loads them at the creation of the Vue application.

  • Create directory called plugins
  • Inside the directory, create file called recaptcha.ts:
import { VueReCaptcha } from "vue-recaptcha-v3";
import { IReCaptchaOptions } from "vue-recaptcha-v3/dist/IReCaptchaOptions";
// The plugin enables the usage of Google reCAPTCHA in a Nuxt.js application
// by registering the VueReCaptcha plugin with the necessary configuration options.
export default defineNuxtPlugin((nuxtApp) => {
  // The useRuntimeConfig function is called to retrieve the runtime
  // configuration of the Nuxt.js application.
  const config = useRuntimeConfig();

  const options: IReCaptchaOptions = {
    siteKey: config.public.googleRecaptchaKey,
    loaderOptions: {
      useRecaptchaNet: true,
    },
  };
  nuxtApp.vueApp.use(VueReCaptcha, options);
});

Create useGoogleRecaptcha.ts file

In order to use recaptcha operation across all pages, create the composables/useGoogleRecaptcha.ts file

import { useReCaptcha } from "vue-recaptcha-v3";

export class RecaptchaAction {
  public static readonly login = new RecaptchaAction("login");

  private constructor(public readonly name: string) {}
}

/**
 * The exported executeRecaptcha function allows
 * you to execute reCAPTCHA actions
 * and retrieve the reCAPTCHA token along with the header options
 * to be used in subsequent requests.
 */
export default () => {
  let recaptchaInstance = useReCaptcha();

  const executeRecaptcha = async (action: RecaptchaAction) => {
    /**
     * Wait for the recaptchaInstance to be loaded
     * by calling the recaptchaLoaded method.
     * This ensures that the reCAPTCHA library is fully loaded
     * and ready to execute reCAPTCHA actions.
     */
    await recaptchaInstance?.recaptchaLoaded();
    const token = await recaptchaInstance?.executeRecaptcha(action.name);
    const headerOptions = {
      headers: {
        "google-recaptcha-token": token,
      },
    };
    return { token, headerOptions };
  };

  return { executeRecaptcha };
};

Create dummy login page

Create pages/index.vue file, we will add the form login in here.

<template>
  <div class="form-center">
    <form @submit.prevent="handleSubmit">
      <label>Email:</label>
      <input type="email" name="email" placeholder="Email" />
      <input type="submit" />
    </form>
  </div>
</template>

<script setup lang="ts">
import useGoogleRecaptcha, {
  RecaptchaAction,
} from "~/composables/useGoogleRecaptcha";
const { executeRecaptcha } = useGoogleRecaptcha();

const handleSubmit = async () => {
  const { token } = await executeRecaptcha(RecaptchaAction.login);
  const fetchData = await $fetch<string>("/api/login", {
    baseURL: "http://localhost:8080",
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "google-recaptcha-token": token ?? "",
    },
  });
  console.log("response from api :: ", fetchData);
};
</script>

<style scoped>
.form-center {
  width: 400px;
  margin: 0 auto;
}
</style>

In this page:

  • Provide a form for users to enter an email address,
  • And when the form is submitted, it executes a reCAPTCHA action, retrieves the reCAPTCHA token,
  • And makes a POST request to an API endpoint with the token included in the request header.

Here is the sample response from the backend after form is submitted:

GoogleRecaptchaResponse(success=true, challenge_ts=2023-05-18T11:46:28Z, hostname=localhost, errorCodes=null, score=0.9, action=login)

Conclusion

Integrating Google reCAPTCHA v3 with Spring Boot and Nuxt 3 offers several benefits for developers aiming to enhance the security and protection of their web applications:

  1. Improved Security: Google reCAPTCHA v3 provides an effective way to prevent automated bots and malicious activities from accessing your application.
  2. User Experience: With reCAPTCHA v3, there is no need for users to solve complex puzzles or click on checkboxes.
  3. Easy Integration: Spring Boot and Nuxt 3 provide libraries and plugins that simplify the integration process with reCAPTCHA v3.

You May Also Like