Android SafetyNet ReCaptcha Integration and Verification

SafetyNet reCaptcha is used to protect your app, forms, website from spams and malicious traffic. reCAPTCHA provides a mechanism to safeguard from spam like bots or from misuse of your resources like traffic, bandwidth.

This post explains and provides working example of using reCaptcha in Android application. We will also see how to verify and validate reCaptcha response in our backend system.

Obtain reCAPTCHA key pair

The first step before we start integrating reCaptcha is to register the android app and generate key to authenticate reCaptcha API call. Obtain SafetyNet reCaptcha api key by signing up.

Create new reCaptcha site

  • Set Label to name your key
  • Set Package Name of Android App where reCaptcha validation will be used.
  • Accept Terms of Services

Android SafetyNet reCaptcha key settings

Once you submit the form you get Site key and secret key. Site key is used to send request to reCaptcha and Secret Key is used to validate the user response token.

Android Project

Create new Android project with Empty Activity. Add SafetyNet dependency in build.gradle file and sync gradle changes.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    implementation 'com.google.android.gms:play-services-safetynet:16.0.0'

    implementation 'com.google.firebase:firebase-core:16.0.7'
    implementation 'com.google.firebase:firebase-functions:16.1.3'
}

I have added Firebase functions in the gradle dependencies to calL Firebase Cloud backend function for validating user response token.

Android Resource

Update style.xml to support AppBarLayout and Toolbar.

<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="AppTheme.NoActionBar" parent="AppTheme">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
</resources>

Update AndroidManifest.xml to add style to the Activity.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.codeexa.com.recaptchaexample">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:ignore="GoogleAppIndexingWarning">
        <activity android:name=".MainActivity"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Update activity layout to create Register form and validate user. activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <android.support.design.widget.AppBarLayout
        android:layout_height="wrap_content"
        android:layout_width="match_parent"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" >


    <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light" >

    </android.support.v7.widget.Toolbar>

    </android.support.design.widget.AppBarLayout>
<android.support.constraint.ConstraintLayout
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/recaptchaButton"
        style="@style/Widget.AppCompat.Button.Colored"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="Submit!"
        android:textAppearance="@style/TextAppearance.AppCompat.Button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textInputLayout2" />

    <TextView
        android:id="@+id/recaptchResponse"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="8dp"
        android:text=""
        android:textAppearance="@style/TextAppearance.AppCompat.Body1"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/recaptchaButton" />

    <android.support.design.widget.TextInputLayout
        android:id="@+id/textInputLayout"
        android:layout_width="395dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/evEmail"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Email"
            android:inputType="textEmailAddress" />
    </android.support.design.widget.TextInputLayout>

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        android:text="Register"
        android:textAppearance="@style/TextAppearance.AppCompat.Headline"
        app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/progressBar" />

    <android.support.design.widget.TextInputLayout
        android:id="@+id/textInputLayout2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="32dp"
        android:layout_marginEnd="8dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textInputLayout">

        <android.support.design.widget.TextInputEditText
            android:id="@+id/evPassword"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Password"
            android:inputType="textPassword" />
    </android.support.design.widget.TextInputLayout>

    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:max="100"
        android:visibility="invisible"
        android:backgroundTint="@android:color/white"
        android:indeterminateTint="@color/colorAccent"
        android:layout_marginTop="4dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
</android.support.design.widget.CoordinatorLayout>

Android Code Changes

  • Now open the activity file and update RECAPTCHA_KEY with your reCAPTCHA site key.
  • verifyReCaptchaAndRegister() calls SafetyNet api and displays Captcha dialog. On submission of the dialog a token is generated which needs to be sent to your backend server for validation.
  • verifyAndRegisterUser() calls Firebase Cloud Function and posts form data along with user token for validation.
  • The token is validated at the server. In this example I have used Firebase Cloud Functions to validate reCaptcha user token.
package com.codeexa.com.recaptchaexample;

import android.support.annotation.NonNull;
import android.support.design.widget.TextInputEditText;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;

import com.google.android.gms.common.api.ApiException;
import com.google.android.gms.common.api.CommonStatusCodes;
import com.google.android.gms.safetynet.SafetyNet;
import com.google.android.gms.safetynet.SafetyNetApi;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnCompleteListener;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.firebase.functions.FirebaseFunctions;
import com.google.firebase.functions.HttpsCallableResult;
import com.google.gson.Gson;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    public static final String RECAPTCHA_KEY = "_______CHANGE_______";
    private TextView mTextView;
    private TextInputEditText mEmailView, mPasswordView;
    private ProgressBar mProgressBar;
    private Button mSubmitBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar= findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        mSubmitBtn = findViewById(R.id.recaptchaButton);
        mSubmitBtn.setOnClickListener(this);

        mEmailView = findViewById(R.id.evEmail);
        mPasswordView = findViewById(R.id.evPassword);
        mProgressBar = findViewById(R.id.progressBar);

        mTextView = findViewById(R.id.recaptchResponse);
    }

    @Override
    public void onClick(View v) {
        if(v.getId() == R.id.recaptchaButton) {
            mSubmitBtn.setEnabled(false);
            mProgressBar.setVisibility(View.VISIBLE);
            verifyReCaptchaAndRegister();
        }
    }

    private void verifyReCaptchaAndRegister() {
        SafetyNet.getClient(this).verifyWithRecaptcha(RECAPTCHA_KEY)
                .addOnSuccessListener( this,
                        new OnSuccessListener<SafetyNetApi.RecaptchaTokenResponse>() {
                            @Override
                            public void onSuccess(SafetyNetApi.RecaptchaTokenResponse response) {
                                // Indicates communication with reCAPTCHA service was
                                // successful.
                                String userResponseToken = response.getTokenResult();
                                if (!userResponseToken.isEmpty()) {
                                    // Validate the user response token using the
                                    // reCAPTCHA siteverify API.
                                    Task<HttpsCallableResult> verifyTask = verifyAndRegisterUser(userResponseToken);
                                    verifyTask.addOnCompleteListener(new OnCompleteListener<HttpsCallableResult>() {
                                        @Override
                                        public void onComplete(@NonNull Task<HttpsCallableResult> task) {
                                            mProgressBar.setVisibility(View.INVISIBLE);
                                            if(task.isSuccessful()) {
                                                RegisterResponse resp = new Gson().fromJson(task.getResult().getData().toString(), RegisterResponse.class);
                                                if(resp.getmStatus() == 200) {
                                                    mTextView.setText(String.format(Locale.getDefault(), "Result: %s", resp.getmData()));
                                                }else{
                                                    mTextView.setText(String.format(Locale.getDefault(), "Failed: %s", resp.getmData()));
                                                }
                                            }
                                            else
                                                mTextView.setText(String.format(Locale.getDefault(),"%s", task.getException().getMessage()));

                                        }
                                    });
                                }else{
                                    mProgressBar.setVisibility(View.INVISIBLE);
                                    mSubmitBtn.setEnabled(true);
                                }
                            }
                        })
                .addOnFailureListener( this, new OnFailureListener() {
                    @Override
                    public void onFailure(@NonNull Exception e) {
                        mProgressBar.setVisibility(View.INVISIBLE);
                        mSubmitBtn.setEnabled(true);
                        if (e instanceof ApiException) {
                            // An error occurred when communicating with the
                            // reCAPTCHA service. Refer to the status code to
                            // handle the error appropriately.
                            ApiException apiException = (ApiException) e;
                            int statusCode = apiException.getStatusCode();
                            Log.d("TAG", "Error: " + CommonStatusCodes
                                    .getStatusCodeString(statusCode));
                        } else {
                            // A different, unknown type of error occurred.
                            Log.d("TAG", "Error: " + e.getMessage());
                        }
                    }
                });
    }


    private Task<HttpsCallableResult> verifyAndRegisterUser(String respToken) {
        FirebaseFunctions mFunctions = FirebaseFunctions.getInstance();
        Map<String, Object> data = new HashMap<>();
        data.put("token", respToken);
        data.put("email", mEmailView.getText().toString());
        data.put("password", mPasswordView.getText().toString());

        return mFunctions
                .getHttpsCallable("registerUser")
                .call(data)
                .continueWith(new Continuation<HttpsCallableResult, HttpsCallableResult>() {
                    @Override
                    public HttpsCallableResult then(@NonNull Task<HttpsCallableResult> task) {
                        // This continuation runs on either success or failure, but if the task
                        // has failed then getResult() will throw an Exception which will be
                        // propagated down.
                            return  task.getResult();

                    }
                });

    }
}

Validation of User Token in Firebase Cloud

In my past tutorial I have written about how to connect and use Firebase Cloud Functions. If you are new to Cloud Functions then the post is a starting point to understand how to use Google Firebase to create your own backend server.

const admin = require('firebase-admin');
const functions = require('firebase-functions');
const superAgent = require('superagent');

const RECAPTCHA_SECRET = "_______CHANGE_______";

admin.initializeApp();

exports.registerUser = functions.https.onCall((data, context) => {
    const recaptchaToken = data.token;
    const userEmail = data.email;
    const userPassword = data.password;

    function createNewUser(recaptchaResponse) {
        // Save user in database
        
        return JSON.stringify({
            status: 200,
            data: "User created successfully"
        });
    }

    function verifyUserToken() {
        return superAgent
            .post('https://www.google.com/recaptcha/api/siteverify')
            .type('form')
            .send({
                secret: RECAPTCHA_SECRET
            }).send({
                response: recaptchaToken
            })
            .then((response) => {
                var status = response.body;
                console.log("Captcha Response:", response.body);
                if (status.success === false)
                    throw new functions.https.HttpsError('invalid-argument', "Failed with reason:" + response.body["error-codes"]);
                else return response.body;
            }).catch(error => {
                console.log("Captcha Error:", error);
                throw new functions.https.HttpsError('invalid-argument', error);
            });
    }

    return Promise.resolve(verifyUserToken())
        .then((result) => createNewUser(result));

});

Replace RECAPTCHA_SECRET with secret key obtained from reCAPTCHA.

Dependencies used in the above code are superagentfirebase-functions and firebase-admin

 

Leave a Reply