How to Use Google’s reCAPTCHA with Golang

Introduction

It’s always a good practice to prevent malicious software from engaging in abusive activities on your web application. The most popular tool to achieve that is Google’s reCAPTCHA. At the current state, it supports v2 and v3. In this article, we will focus on v3, as it requires less interaction with users and enables analytics.

Prerequisites

  • Go environment (instructions)
  • An already registered website on Google’s reCAPTCHA console
  • A new directory, e.x recaptcha-endpoints, inside your GOPATH where all of our code will live

Getting started

After registering your website, a new API key and secret pair will be generated. To make reCAPTCHA work, it requires changes on both frontend and backend services of your web application. We will only demonstrate on how to use it on the backend service, as Google’s documentation is pretty straight forward regarding the challenge placement on the frontend.

Implementation

We will implement a simple server with a single endpoint to handle the login of users, given an email and password, protected with Google reCAPTCHA.

The way to verify a reCAPTCHA token is to make a POST request on [https://www.google.com/recaptcha/api/siteverify](https://www.google.com/recaptcha/api/siteverify) followed by the secret and the response token as URL parameters. Then evaluate the response to find out if the challenge was successful. Source code is about to follow.

package main

import (
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"os"
	"time"
)

const siteVerifyURL = "https://www.google.com/recaptcha/api/siteverify"

type LoginRequest struct {
	Email             string `json:"email"`
	Password          string `json:"password"`
	RecaptchaResponse string `json:"g-recaptcha-response"`
}

type SiteVerifyResponse struct {
	Success     bool      `json:"success"`
	Score       float64   `json:"score"`
	Action      string    `json:"action"`
	ChallengeTS time.Time `json:"challenge_ts"`
	Hostname    string    `json:"hostname"`
	ErrorCodes  []string  `json:"error-codes"`
}

func main() {
	// Get recaptcha secret from env variable.
	secret := os.Getenv("RECAPTCHA_SECRET")
	
	// Define endpoint to handle login.
	http.HandleFunc("/login", Login(secret))

	log.Println("Server starting at port 8080...")

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

func Login(secret string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Decode request body.
		var body LoginRequest
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			http.Error(w, "Bad Request", http.StatusBadRequest)
			return
		}

		// Check and verify the recaptcha response token.
		if err := CheckRecaptcha(secret, body.RecaptchaResponse); err != nil {
			http.Error(w, "Unauthorized", http.StatusUnauthorized)
			return
		}

		// Check login credentials.
    		// ....

		w.WriteHeader(http.StatusOK)
	}
}

func CheckRecaptcha(secret, response string) error {
	req, err := http.NewRequest(http.MethodPost, siteVerifyURL, nil)
	if err != nil {
		return err
	}

	// Add necessary request parameters.
	q := req.URL.Query()
	q.Add("secret", secret)
	q.Add("response", response)
	req.URL.RawQuery = q.Encode()

	// Make request
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Decode response.
	var body SiteVerifyResponse
	if err = json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return err
	}

	// Check recaptcha verification success.
	if !body.Success {
		return errors.New("unsuccessful recaptcha verify request")
	}

	// Check response score.
	if body.Score < 0.5 {
		return errors.New("lower received score than expected")
	}

	// Check response action.
	if body.Action != "login" {
		return errors.New("mismatched recaptcha action")
	}

	return nil
}

Let’s dive into it step by step

  • A const variable to hold Google’s reCAPTCHA request link
  • LoginRequest struct that contains the user email and password to validate credentials, plus an extra field for reCAPTCHA token.
  • SiteVerifyResponse struct to decode the response from Google reCAPTCHA API.
  • The main function to define the login endpoint handler, passing the reCAPTCHA secret as argument.
  • The Login function that operates as handler where at first decodes the request body. Then checks and verifies the reCAPTCHA token calling the CheckRecaptcha function and finally validate the provided credentials.
  • The CheckRecaptcha function that does all the work regarding reCAPTCHA verification. Accepts the secret and response token as arguments, constructs the proper POST request and makes it. Decode the response into the SiteVerifyResponse struct and checks if the verification was successful, compares the received score against a provided minimum (0.5) and also checks the action name (login)

You can set custom action names for any operation on your website, by setting the _data-action_ attribute. In this way you can have access to a detailed break down of data in admin console and can apply different business logic for each action (e.g. 2FA authentication for login with low scores).

Middleware

The above solution does the work, although it doesn’t scale while your web server is growing. It requires code duplication, generic code inside handlers and an extra field inside each struct, which used to decode requests body.

To avoid all that, we can implement a middleware to handle all the reCAPTCHA verification and apply to any endpoint is required. The previous code now becomes.

package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"time"
)

const siteVerifyURL = "https://www.google.com/recaptcha/api/siteverify"

type LoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type SiteVerifyResponse struct {
	Success     bool      `json:"success"`
	Score       float64   `json:"score"`
	Action      string    `json:"action"`
	ChallengeTS time.Time `json:"challenge_ts"`
	Hostname    string    `json:"hostname"`
	ErrorCodes  []string  `json:"error-codes"`
}

type SiteVerifyRequest struct {
	RecaptchaResponse string `json:"g-recaptcha-response"`
}

func main() {
	// Get recaptcha secret from env variable.
	secret := os.Getenv("RECAPTCHA_SECRET")

	// Define endpoint to handle login.
	http.Handle("/login", RecaptchaMiddleware(secret)(Login()))

	log.Println("Server starting at port 8080...")

	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

func Login() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Decode request body.
		var body LoginRequest
		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
			http.Error(w, "Bad Request", http.StatusBadRequest)
			return
		}

		// Check login credentials.
		// ...

		w.WriteHeader(http.StatusOK)
	}
}

func CheckRecaptcha(secret, response string) error {
	req, err := http.NewRequest(http.MethodPost, siteVerifyURL, nil)
	if err != nil {
		return err
	}

	// Add necessary request parameters.
	q := req.URL.Query()
	q.Add("secret", secret)
	q.Add("response", response)
	req.URL.RawQuery = q.Encode()

	// Make request
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// Decode response.
	var body SiteVerifyResponse
	if err = json.NewDecoder(resp.Body).Decode(&body); err != nil {
		return err
	}

	// Check recaptcha verification success.
	if !body.Success {
		return errors.New("unsuccessful recaptcha verify request")
	}

	// Check response score.
	if body.Score < 0.5 {
		return errors.New("lower received score than expected")
	}

	// Check response action.
	if body.Action != "login" {
		return errors.New("mismatched recaptcha action")
	}

	return nil
}

func RecaptchaMiddleware(secret string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			// Get the reCaptcha response token from default request body field 'g-recaptcha-response'.
			bodyBytes, err := ioutil.ReadAll(r.Body)
			if err != nil {
				http.Error(w, "Unauthorized", http.StatusUnauthorized)
				return
			}

			// Unmarshal body into struct.
			var body SiteVerifyRequest
			if err := json.Unmarshal(bodyBytes, &body); err != nil {
				http.Error(w, "Unauthorized", http.StatusUnauthorized)
				return
			}

			// Restore request body to read more than once.
			r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

			// Check and verify the recaptcha response token.
			if err := CheckRecaptcha(secret, body.RecaptchaResponse); err != nil {
				http.Error(w, "unauthorized", http.StatusUnauthorized)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

We will mention the changes against the first version of our code.

  1. The RecaptchaResponse field has been removed from the LoginRequest struct as is no longer need it.
  2. A new struct SiteVerifyRequest has been introduced and will be used in middleware function to retrieve the reCAPTCHA token from request body.
  3. The Login handler function is now wrapped by RecaptchaMiddleware.
  4. Any source code regarding reCAPTCHA has been removed from handler.
  5. A new function RecaptchaMiddleware that accepts a secret has been created, that operates as middleware. Contains all the code regarding Google’s reCAPTCHA verification. Reads and decodes the request body into SiteVerityRequest struct, then checks and verifies the reCAPTCHA at the same way as before.
  6. There is a tricky part at line 125 that needs attention. In Go, http request body is an io.ReadCloser type that can be read only once. For that reason we have to restore it, to be able to use it again in any following handler.

That’s all the differences regarding the first version of our solution. In this way the code it’s a lot cleaner, handlers only contain the business logic that have been created for and the Google’s reCAPTCHA verification happens to a single place, inside the middleware.

Source code

I have implemented an open source package that handles Google’s reCAPTCHA verification that supports both v2 and v3. An out of the box middleware is also included. You can find it on GitHub.

#google-recaptcha #go #google #recaptcha #golang

How to Use Google’s reCAPTCHA with Golang
32.30 GEEK