Integrating Google's reCAPTCHA with Dash

Hi all,

I recently had to figure out how to integrate reCAPTCHA V2 with Dash and there were no resources online on how to do it, so I thought I would share it here.

Typically, you would do this by including a div element in your form with class=“g-recaptcha” and a script with source src=“https://www.google.com/recaptcha/api.js

The problem I found is that Dash renders the html elements after the script runs, so the captcha does not render.

This is how I managed to do it:

First, I will assume you have registered your site (reCAPTCHA) and have received a sitekey and secret. I store these in app config:

app.server.config['RECAPTCHA_SITEKEY'] = ...
app.server.config['RECAPTCHA_SECRET'] = ...

We need to add the google reCAPTCHA script when initialising dash.

app = dash.Dash(__name__,external_scripts = ['https://www.google.com/recaptcha/api.js?render=explicit'])

Note the “render=explicit” in query string. This means we are going to render the captcha it explicitly though JavaScript.

In your layout you can create 3 html components:

html.Div(id='login-recaptcha')
html.Div(id='login-recaptcha-response',style={'display':'none'})
html.Button('Log In', id='login-button', n_clicks=0)

Now we add a client side callback:

app.clientside_callback(
        """
        function(n_clicks) {
            if (n_clicks==0){
                grecaptcha.render('login-recaptcha', {'sitekey' : '_sitekey'});
            }
            if (n_clicks > 0){
                var recaptcha_response = document.getElementById('g-recaptcha-response');
                return recaptcha_response.value;
            }
        }
        """.replace('_sitekey',app.server.config['RECAPTCHA_SITEKEY']),
        
        Output('login-recaptcha-response', 'children'), 
        Input('login-button', 'n_clicks')
    )

What this does is as follows:

if n_clicks==0 then we render the reCAPTCHA with grecaptcha (this will happen after the page loads)

The response of the user is stored in a hidden textarea with id ‘g-recaptcha-response’. The problem is that we cannot link this to a callback because it is injected by calling grecaptcha.render after the page loads. Therefore, we have to transfer its contents to another html component that we can link to a callback. That’s what happens in the next section of the code. If n_clicks > 0 we copy the user’s response to our ‘login-recaptcha-response’ div.

Now we are ready to handle the response on the server:

@app.callback(
        Output('request-url','pathname'),
        Input('login-button','n_clicks'),
        Input('login-recaptcha-response','children'),
    )
    def handle_login(nclicks,recaptcha_response):

          if recaptcha_response is None or recaptcha_response.strip() == '':
                        raise PreventUpdate

          request_headers = {"Content-Type": "application/x-www-form-urlencoded; charset=utf-8" }

          request_url = 'https://www.google.com/recaptcha/api/siteverify?secret={0}&response={1}'.format(app.server.config['RECAPTCHA_SECRET'],recaptcha_response)
          response = requests.post(request_url,headers=request_headers)

          response_json = json.loads(response.text)    
       
          if response_json['success'] == False:
             raise PreventUpdate
        
       # do what you want here after the captcha has been verified.

And that’s it! Hope it helps and if someone finds an easier way to do it do let me know.

Regards,
Ofir

6 Likes

This is a great solution! And actually a generally useful technique for including DOM-dependent JS in Dash (which is currently a problem without a first class solution)

1 Like

I was trying to create a minimal viable example of this but had an issue on the javascript side I think. I might be missing something because I have not used clientside_callback before. Very much appreciated in advance if anyone can help!

Got the following error:

(This error originated from the built-in JavaScript code that runs Dash apps. Click to see the full stack trace or open your browser's console.)
TypeError: Cannot read properties of undefined (reading 'apply')

    at _callee2$ (http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:580:72)

    at tryCatch (http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:406:1357)

    at Generator.<anonymous> (http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:406:4187)

    at Generator.next (http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:406:2208)

    at asyncGeneratorStep (http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:412:103)

    at _next (http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:413:194)

    at http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:413:364

    at new Promise (<anonymous>)

    at http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:413:97

    at handleClientside (http://127.0.0.1:8050/_dash-component-suites/dash/dash-renderer/build/dash_renderer.v2_13_0m1694351283.dev.js:533:28)

based on this script:

import dash
from dash import dcc
import os
from dash.dependencies import Input, Output
from dash.exceptions import PreventUpdate
from dash import html, callback, clientside_callback
import json
import requests



app = dash.Dash(
    __name__,
    external_scripts=["https://www.google.com/recaptcha/api.js?render=explicit"],
)

app.server.config['RECAPTCHA_SITEKEY'] = os.environ.get("API_SITE_KEY_RECAPTCHA")
app.server.config['RECAPTCHA_SECRET'] = os.environ.get("API_SECRET_KEY_RECAPTCHA")
# print(app.server.config['RECAPTCHA_SITEKEY'])
# print(app.server.config['RECAPTCHA_SECRET'])

app.layout= html.Div(
        [
            html.Div(id="login-recaptcha"),
            html.Div(id="login-recaptcha-response", style={"display": "none"}),
            html.Button("Log In", id="login-button", n_clicks=0),
            dcc.Location(id="request-url"),
            # html.Script(src='/assets/js_functions.js')
        ]
    )


clientside_callback(
    """
        function(n_clicks) {
            if (n_clicks==0){
                grecaptcha.render('login-recaptcha', {'sitekey' : '_sitekey'});
            }
            if (n_clicks > 0){
                var recaptcha_response = document.getElementById('g-recaptcha-response');
                return recaptcha_response.value;
            }
        }
        """.replace(
        "_sitekey", app.server.config["RECAPTCHA_SITEKEY"]
    ),
    Output("login-recaptcha-response", "children"),
    Input("login-button", "n_clicks"),
)


@callback(
    Output("request-url", "pathname"),
    Input("login-button", "n_clicks"),
    Input("login-recaptcha-response", "children"),
)
def handle_login(nclicks, recaptcha_response):
    print("handle_login")
    print(nclicks)
    print(recaptcha_response)
    if recaptcha_response is None or recaptcha_response.strip() == "":
        raise PreventUpdate

    request_headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
    }

    request_url = "https://www.google.com/recaptcha/api/siteverify?secret={0}&response={1}".format(
        app.server.config["RECAPTCHA_SECRET"], recaptcha_response
    )
    response = requests.post(request_url, headers=request_headers)
    print(response.text)

    response_json = json.loads(response.text)

    if response_json["success"] == False:
        raise PreventUpdate

    # do what you want here after the captcha has been verified.
    # return the url you want to go to
    return "/home"

if __name__ == "__main__":
    # app.run(debug=False) # Stop refreshing page
    app.run(debug=True)  # Refresh page on code changes

(Followed similar pattern shown under " Things You Need To Do" in this article to get the keys)
https://www.gitauharrison.com/articles/google-recaptcha-in-flask

Seemed to work out better with:
async function(n_clicks) {
instead of just:
function(n_clicks) {
Would love to know more why!? :stuck_out_tongue_closed_eyes: