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.
recaptcha-endpoints
, inside your GOPATH
where all of our code will liveAfter 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.
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
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.main
function to define the login endpoint handler, passing the reCAPTCHA secret as argument.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.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).
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.
RecaptchaResponse
field has been removed from the LoginRequest
struct as is no longer need it.SiteVerifyRequest
has been introduced and will be used in middleware function to retrieve the reCAPTCHA token from request body.Login
handler function is now wrapped by RecaptchaMiddleware
.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.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.
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