Codementor Events

Protecting your Spring Boot Rest Endpoints with Google reCAPTCHA and AOP

Published Nov 25, 2018

You can find the code on github:
https://github.com/cristirosu/spring-boot-recaptcha-aop

What is reCAPTCHA?

reCAPTCHA is a free service that protects your site from spam and abuse. It uses advanced risk analysis techniques to tell humans and bots apart.

Let’s look at the following scenario:

You have an online store where users can create an account by completing their basic information (name, email and phone). After this step, they will receive an SMS to validate their phone number and then proceed to the main page.

The Problem

The creation process sends an SMS to verify the user. If your API lacks spam protection, anyone could just abuse it and since you (probably) use a paid SMS provider, it could get you to pay high bills to your provider or just throttle your SMS system so nobody else could sign-up.

The solution

Just use reCAPTCHA! This service will ask the user to prove that he’s not a robot by completing some simple challenges for a human (but hard for robots) such as visual (identifying certain aspects in pictures) or audio (speech text recognition).

Obtaining your API keys

In order to use this service, you will need a google account, then just go to: https://www.google.com/recaptcha/admin#list , choose reCAPTCHA v2, check the “I’m not a robot” option, input your site’s domain(s), then you will be able to see your 2 keys: client key and server key (you must keep this one private).

Server Side with Spring Boot

We will use the following maven dependencies:

<dependencies>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
   </dependency>
   <dependency>
      <groupId>org.webjars</groupId>
      <artifactId>jquery</artifactId>
      <version>3.3.1</version>
   </dependency>
</dependencies>

Let’s create the following REST endpoint (POST): http://localhost:8080/hello which will receive a request with the user’s name and will kindly greet him.

@RestController
@RequestMapping("/hello")
public class HelloController {
@PostMapping
    public String hello(@RequestBody HelloDTO helloDTO){
  return new HelloResponseDTO("Hello, " + helloDTO.getName() + "!");
    }
}

Let’s test it!

curl -d '{"name":"Chris"}' -H "Content-Type: application/json" -X POST http://localhost:8080/hello

We get the following response: {‘message’:’Hello, Chris!’}

Adding reCAPTCHA protection
In our “application.properties” file we store the server private key:

google.recaptcha.secret=SERVER_KEY

Let’s create the CaptchaValidator:

@Service
public class CaptchaValidator {

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

    @Value("${google.recaptcha.secret}")
    private String recaptchaSecret;

    public boolean validateCaptcha(String captchaResponse){
        RestTemplate restTemplate = new RestTemplate();

        MultiValueMap<String, String> requestMap = new LinkedMultiValueMap<>();
        requestMap.add("secret", recaptchaSecret);
        requestMap.add("response", captchaResponse);

        CaptchaResponse apiResponse = restTemplate.postForObject(GOOGLE_RECAPTCHA_ENDPOINT, requestMap, CaptchaResponse.class);
        if(apiResponse == null){
            return false;
        }

        return Boolean.TRUE.equals(apiResponse.getSuccess());
    }

}

This is the Api Response (getters and setters ommited for clarity):

public class CaptchaResponse {

    private Boolean success;
    private Date timestamp;
    private String hostname;
    @JsonProperty("error-codes")
    private List<String> errorCodes;

}

In the above code example we could see that we inject our private server key into the CaptchaValidator then we send a POST request to google’s REST Api with our secret and client captcha response.

We receive a response from Google containing a field (among others) called success which we can use to test if the client captcha response is valid. If it’s not valid, we can look at the errorCodes to see why.

Modifying the original endpoint to add the CaptchaValidator logic
Adding the captchaResponse to our request body:

public class HelloDTO {

    private String name;
    private String captchaResponse;

}
@PostMapping
public String hello(@RequestBody HelloDTO helloDTO){
    Boolean isValidCaptcha = captchaService.validateCaptcha(helloDTO.getCaptchaResponse()); 
    if(!isValidCaptcha){
        throw new ForbiddenException("Captcha is not valid");
    }
    return new HelloResponseDTO("Hello, " +helloDTO.getName() +"!");
}

We can’t test it with CURL anymore since it requires captcha validation now, so we need to code the client in order have it working again.

Client Side with Jquery

<html>
    <head>
        <title>Spring Boot reCAPTCHA with AOP</title>
        <script type="text/javascript"
                src="webjars/jquery/3.3.1/jquery.min.js"></script>

        <script src='https://www.google.com/recaptcha/api.js'></script>
    </head>
    <body>

        <script>
            $(document).ready(function () {
                $("#button").click(function () {
                    var captchaResponse = grecaptcha.getResponse();
                    var name = $("#name").val();
                    var helloRequest = {
                        'name': name,
                        'captchaResponse': captchaResponse
                    };

                    $.ajax({
                        type: "POST",
                        contentType: 'application/json',
                        dataType: "json",
                        data: JSON.stringify(helloRequest),
                        url: "http://localhost:8080/hello",
                        success: function (data) {
                            alert(data.message);
                        }
                    });
                });
            });
        </script>

        <div>
            <input type="text" id="name"/>
            <button type="submit" id="button">Hello</button>
            <div class="g-recaptcha" data-sitekey="CLIENT_KEY"></div>
        </div>
    </body>
</html>

I chose jQuery because I can provide a 1 file example but there are libraries for AngularJs, Angular 2+, React and even if you can’t find one for your frontend library, you can just use plain javascript anyway.

The above code results in this simple page which calls our backend hello endpoint with the name and captchaResponse as the request body:
1.png

If we complete the name, tick the “I’m not a robot” checkbox and press on the button we’ll get an alert greeting us!
2.png

We just secured our REST endpoint with reCAPTCHA, that’s awesome!

One problem though…

If we’d want to add reCAPTCHA validation to any other of our REST endpoints, we would have to follow the next steps:

Include a field in our request body to retrieve the captchaResponse
Add logic in our controller or service to validate the captcha
Seems pretty simple, but it’s quite repetitive. And what about GET requests? We can’t have a body on those…

AOP to the rescue!

AOP (Aspect Oriented Programming) lets us run some code before (or after) one of our methods is called.

We’re going to make the following changes in our code:

Move the client captchaResponse from the request body to an http header (to be more generic and also be able to include it for requests which can’t have a body)
Implement an AOP annotation to eliminate the boiler plate code and achieve a much cleaner code.

1. Moving the capthca to the header

We modify the client to send the captchaResponse in the header instead of body:

<script>
    $(document).ready(function () {
        $("#button").click(function () {
            var captchaResponse = grecaptcha.getResponse();
            var name = $("#name").val();
            var helloRequest = {
                'name': name
            };

            $.ajax({
                type: "POST",
                contentType: 'application/json',
                dataType: "json",
                headers: {
                    "captcha-response": captchaResponse
                },
                data: JSON.stringify(helloRequest),
                url: "http://localhost:8080/hello",
                success: function (data) {
                    alert(data.message);
                }
            });
        });
    });
</script>

2. Create an AOP annotation to handle the capthca more gracefully

a. Create the annotation

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresCaptcha {

}

b. Create the aspect

@Aspect
@Component
public class CaptchaAspect {

    @Autowired
    private CaptchaValidator captchaValidator;

    private static final String CAPTCHA_HEADER_NAME = "captcha-response";

    @Around("@annotation(RequiresCaptcha)")
    public Object validateCaptcha(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        String captchaResponse = request.getHeader(CAPTCHA_HEADER_NAME);
        boolean isValidCaptcha = captchaValidator.validateCaptcha(captchaResponse);
        if(!isValidCaptcha){
            throw new ForbiddenException("Invalid captcha");
        }
        return joinPoint.proceed();
    }

}

Based on the above code, we can see that the captcha validation logic is now in our aspect.

Some explanations:

@Around("@annotation(RequiresCaptcha)")

This tells Spring to run our code before a method annotated with “@RequiresCaptcha” is called.

HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String captchaResponse = request.getHeader(CAPTCHA_HEADER_NAME);
boolean isValidCaptcha = captchaValidator.validateCaptcha(captchaResponse);
if(!isValidCaptcha){
    throw new ForbiddenException("Invalid captcha");
}

This code retrieves the captcha header from the current HttpServletRequest and sends it to the validator for checking.
If everything is ok the following line will execute:

return joinPoint.proceed();

This will call the original method which will return the greeting to the client. If the captcha isn’t valid, an exception will be raised.

Back to our controller

@PostMapping
@RequiresCaptcha
public HelloResponseDTO hello(@RequestBody HelloDTO helloDTO){
  return new HelloResponseDTO("Hello, " + helloDTO.getName() + "!");
}

As we can see, we removed the boiler plate code for checking for validating the captcha and instead we added the annotation “@RequiresCaptcha”.

Now we can use this annotation whenever we want to add captcha validation to one of our REST endpoints which is great!

You can see the full code:
https://github.com/cristirosu/spring-boot-recaptcha-aop

Discover and read more posts from Cristian Rosu
get started