Implementing reCAPTCHA Enterprise in a Flutter App with Spring Boot Backend

Implementing reCAPTCHA Enterprise in a Flutter App with Spring Boot Backend

Introduction

Online security is a growing concern, with bots and automated attacks targeting mobile applications. reCAPTCHA Enterprise provides an AI-powered security solution to protect your Flutter app from fraudulent activities.

In this guide, we will integrate reCAPTCHA Enterprise into a Flutter application (using recaptcha_enterprise_flutter) and verify the reCAPTCHA token on a Spring Boot backend.

1. Create a reCAPTCHA Key

To begin, generate a reCAPTCHA site key specifically for mobile applications. This key enables reCAPTCHA to collect user interaction data and assess risk levels. Ensure you create a score-based key for your mobile app.

Follow this guide to create your site key:
🔗 Create a reCAPTCHA Key for Mobile

2. Integrate reCAPTCHA with Your Flutter Frontend

Install Dependencies

Add the recaptcha_enterprise_flutter package to your pubspec.yaml:

dependencies:
    recaptcha_enterprise_flutter: latest_version

Generate reCAPTCHA Token in Flutter

In your Flutter app, use the recaptcha_enterprise_flutter package to execute reCAPTCHA and generate an encrypted token.

Define Action Names

When generating a token, specify action names in the action parameter. Follow these best practices:

Use unique and meaningful names: Instead of "login", use "user_login_attempt" or "checkout_verification".
Avoid user-specific data: Do not include personal identifiers in the action name.

Reference guide:
🔗 Action Names for Mobile

Example: Requesting a reCAPTCHA Token in Flutter

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:recaptcha_enterprise_flutter/recaptcha.dart';
import 'package:recaptcha_enterprise_flutter/recaptcha_action.dart';
import 'package:recaptcha_enterprise_flutter/recaptcha_client.dart';

class GoogleCloudRecaptchaService {
  GoogleCloudRecaptchaService._privateConstructor();

  static final GoogleCloudRecaptchaService _instance =
      GoogleCloudRecaptchaService._privateConstructor();

  static GoogleCloudRecaptchaService get instance => _instance;

  RecaptchaClient? _client;
  final String _clientState = "uninitialized";
  String? _token;

  Future<String?> fetchAndExecute() async {
    var errorMessage = "failure";
    var result = false;

    try {
      // Platform-specific site key
      final siteKey = Platform.isAndroid
          ? "Your Android Site Key"
          : "Your IOS Site Key

      _client = await Recaptcha.fetchClient(siteKey);
      result = true;
    } on PlatformException catch (err) {
      debugPrint('Caught platform exception on init: $err');
      errorMessage = 'Code: ${err.code} Message ${err.message}';
    } catch (err) {
      debugPrint('Caught exception on init: $err');
      errorMessage = err.toString();
    }

    if (result) {
      try {
        _token = await _client?.execute(RecaptchaAction.LOGIN()); // This is your Action 
      } on PlatformException catch (err) {
        debugPrint('Caught platform exception on execute: $err');
        throw Exception('Code: ${err.code} Message ${err.message}');
      } catch (err) {
        debugPrint('Caught exception on execute: $err');
        throw Exception(err.toString());
      }
    } else {
      throw Exception(errorMessage);
    }

    return _token;
  }

  String? get token => _token;
}

Once you obtain the token, send it to your backend for verification.

3. Verify reCAPTCHA Token in Your Backend

On the backend, the received token must be validated using reCAPTCHA Enterprise's assessment API.

Follow this guide to create assessments:
🔗 Create reCAPTCHA Assessments

Steps for Backend Verification:

1️⃣ Receive the token from Flutter
2️⃣ Send the token to Google’s reCAPTCHA API
3️⃣ Analyze the risk score and take action accordingly

Backend Setup

In the backend, the received token must be validated using reCAPTCHA Enterprise’s assessment API. The backend will verify the token, analyze the risk score, and take appropriate action based on the score.

1. Setting Up reCAPTCHA API Keys in Spring Boot

First, make sure to store sensitive data such as API keys and project IDs securely using Spring Boot's application.properties or application.yml files.

Why Store Keys in Properties Files?
Storing keys directly in code is risky as it exposes sensitive data if the source code is shared. Using properties files ensures better security practices. It also allows easy configuration changes without modifying the code.

Example: Store API Keys in application.properties

# Google reCAPTCHA Enterprise Configuration
recaptcha.api-endpoint=https://recaptchaenterprise.googleapis.com/v1/projects/${projectId}/assessments?key=${apiKey}
recaptcha.project-id=your-google-cloud-project-id
recaptcha.site-key=your-recaptcha-site-key
recaptcha.api-key=your-recaptcha-api-key
recaptcha.expected-action=user_login_attempt

2. Implementing the reCAPTCHA Token Verification

Once the token is sent from the Flutter app, we use Spring Boot WebClient to call Google's reCAPTCHA API and validate the token.

Here’s the Spring Boot Service that will handle the API request:

Example: Verifying reCAPTCHA Token in a Spring Boot Backend

package com.app.test.service.impl;

import com.app.test.config.GoogleCloudProperties;
import com.app.test.service.RecaptchaService;
import com.app.test.service.dto.RecaptchaRequestDTO;
import com.app.test.service.dto.RecaptchaResponseDTO;
import com.app.test.web.rest.errors.BadRequestAlertException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Service("webClientRecaptchaService")
public class RecaptchaServiceImpl implements RecaptchaService {
    private static final Logger log = LoggerFactory.getLogger(RecaptchaServiceImpl.class);
    private static final float HIGH_RISK_THRESHOLD = 0.3f;
    private static final float MEDIUM_RISK_THRESHOLD = 0.7f;

    private final WebClient webClient;
    private final GoogleCloudProperties googleCloudProperties;

    public RecaptchaServiceImpl(WebClient webClient, GoogleCloudProperties googleCloudProperties) {
        this.webClient = webClient;
        this.googleCloudProperties = googleCloudProperties;
    }

    @Override
    public RecaptchaResponseDTO createAssessment(RecaptchaRequestDTO recaptchaRequest) throws IOException {
        if (recaptchaRequest == null || recaptchaRequest.getToken() == null) {
            throw new BadRequestAlertException("Invalid reCAPTCHA request", "recaptcha", "invalid-request");
        }

        String apiEndpoint = googleCloudProperties.getApiEndpoint();
        if (apiEndpoint == null) {
            throw new BadRequestAlertException("reCAPTCHA API endpoint not configured", "recaptcha", "missing-endpoint");
        }

        apiEndpoint = apiEndpoint.replace("${projectId}", googleCloudProperties.getRecaptcha().getProjectId())
                                 .replace("${apiKey}", googleCloudProperties.getRecaptcha().getApiKey());

        recaptchaRequest.setRecaptchaSiteKey(googleCloudProperties.getRecaptcha().getSiteKey());
        recaptchaRequest.setRecaptchaAction(googleCloudProperties.getRecaptcha().getExpectedAction());

        Map<String, Object> requestBody = preparePayload(recaptchaRequest);

        return sendRecaptchaRequest(apiEndpoint, requestBody);
    }

    private RecaptchaResponseDTO sendRecaptchaRequest(String apiEndpoint, Map<String, Object> requestBody) {
        RecaptchaResponseDTO response = webClient.method(HttpMethod.POST)
            .uri(apiEndpoint)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(requestBody)
            .retrieve()
            .bodyToMono(RecaptchaResponseDTO.class)
            .block();

        if (response == null) {
            throw new BadRequestAlertException("Null response from reCAPTCHA service", "recaptcha", "null-response");
        }
        return response;
    }

    private Map<String, Object> preparePayload(RecaptchaRequestDTO recaptchaRequest) {
        Map<String, Object> eventMap = new HashMap<>(Map.of(
            "siteKey", recaptchaRequest.getRecaptchaSiteKey(),
            "token", recaptchaRequest.getToken(),
            "expectedAction", recaptchaRequest.getRecaptchaAction()
        ));

        if (recaptchaRequest.getUserIpAddress() != null) {
            eventMap.put("userIpAddress", recaptchaRequest.getUserIpAddress());
        }
        if (recaptchaRequest.getUserAgent() != null) {
            eventMap.put("userAgent", recaptchaRequest.getUserAgent());
        }
        if (recaptchaRequest.getJa3() != null) {
            eventMap.put("ja3", recaptchaRequest.getJa3());
        }

        return Map.of("event", eventMap);
    }
}

4. Interpret reCAPTCHA Scores

reCAPTCHA scores range from 0.0 to 1.0, with higher scores indicating lower risk.

  • 1.0 → Almost certainly a human (low risk ✅)

  • 0.0 → Likely a bot (high risk ❌)

  • Intermediate Scores (0.1, 0.3, 0.7, 0.9) → Require review

By default, only the scores 0.1, 0.3, 0.7, and 0.9 are available unless you enable billing.

Example Handling Strategy:

Score RangeSuggested Action
≥ 0.7Allow login ✅
0.3 - 0.7Require additional verification (OTP, email confirmation) ⚠️
≤ 0.3Reject request or enforce CAPTCHA ❌

Example JSON Response from reCAPTCHA API

{
 "event":{
    "expectedAction":"EXPECTED_ACTION",
    "hashedAccountId":"ACCOUNT_ID",
    "siteKey":"KEY_ID",
    "token":"TOKEN",
    "userAgent":"(USER-PROVIDED STRING)",
    "userIpAddress":"USER_PROVIDED_IP_ADDRESS"
 },
 "name":"ASSESSMENT_ID",
 "riskAnalysis":{
   "reasons":[],
   "score":"SCORE"
 },
 "tokenProperties":{
   "action":"USER_INTERACTION",
   "createTime":"TIMESTAMP",
   "hostname":"HOSTNAME",
   "invalidReason":"(ENUM)",
   "valid":(BOOLEAN)
 }
}

5. Implementing Conditional Logic Based on Score

Once the backend receives the risk score, implement logic to handle different risk levels.

Example: Handling reCAPTCHA Scores in a Spring Boot Backend

private void validateResponse(RecaptchaResponseDTO response, RecaptchaRequestDTO recaptchaRequest) {
    if (response == null || response.getTokenProperties() == null || response.getRiskAnalysis() == null) {
        throw new BadRequestAlertException("Invalid reCAPTCHA response", "recaptcha", "invalid-response");
    }

    if (!response.getTokenProperties().isValid()) {
        throw new BadRequestAlertException("Invalid reCAPTCHA token", "recaptcha", "invalid-token: " + response.getTokenProperties().getInvalidReason());
    }

    if (!response.getTokenProperties().getAction().equals(recaptchaRequest.getRecaptchaAction())) {
        throw new BadRequestAlertException("reCAPTCHA action mismatch", "recaptcha", "action-mismatch");
    }

    float score = response.getRiskAnalysis().getScore();
    log.debug("reCAPTCHA score: {}", score);

    if (score < HIGH_RISK_THRESHOLD) {
        throw new BadRequestAlertException("High risk detected", "recaptcha", "high-risk");
    } else if (score < MEDIUM_RISK_THRESHOLD) {
        throw new BadRequestAlertException("Medium risk detected", "recaptcha", "medium-risk");
    } else {
        log.info("Low risk interaction detected. Score: {}", score);
    }
}

Conclusion

By integrating reCAPTCHA Enterprise into your Flutter app and backend, you can prevent fraudulent interactions while ensuring a smooth user experience. Adjust your logic based on the risk assessment scores to optimize security.

Would you like additional details on handling reCAPTCHA failures or logging assessment data? Let me know!

Did you find this article valuable?

Support Abhishek's Blog by becoming a sponsor. Any amount is appreciated!