reCAPTCHA in a login form not using Auth

161 views
Skip to first unread message

David Manns

unread,
Nov 11, 2024, 9:22:44 AM11/11/24
to py4web
I use a login process where a login form that collects an email address then sends an email containing a link with an embedded OTP to the user.

I was hoping to use py4web.utils.recaptcha to do this
from py4web.utils.recaptcha import ReCaptcha

recaptcha = ReCaptcha(RECAPTCHA_KEY, RECAPTCHA_SECRET)


and the form is defined thus:
fields = [recaptcha.field, Field('email', 'string', requires=IS_EMAIL())]
form = Form(fields, formstyle=FormStyleBulma)


I get a form looking like:

G Recaptcha Response [.      ]

Email.  [.    ]

with text input boxes.

Is it possible to use recaptcha.py in this context, and if so how? I have set up recaptcha challenge keys which are in my settings_private.

Thanks!

David

icodk

unread,
Nov 11, 2024, 11:50:10 AM11/11/24
to py4web
Hi David
To my best knowlage the py4web reCaptch is strongly dependent on the auth. However I could not make it work with auth either.
To make reCaptcha work, I abandoned the py4web implementation and "manually " made it with javascript and python.
You can see it in action on the following site: timelume.com
On the main page click on the join us button to see implementation without auth.
If you click on the login button you will see implementation with auth. 
While on the login form you can click on the Lost Password link  to see it again implemented  with auth.
If you are interested I can share all of it

David Manns

unread,
Nov 11, 2024, 2:56:12 PM11/11/24
to py4web
The "join us" implementation is exactly what I'm looking for - my own py4web form with the recaptcha tickbox at the bottom.

Please share how you implemented this - thank you!

Best,

David

icodk

unread,
Nov 11, 2024, 4:50:59 PM11/11/24
to py4web
There are a few parts to the puzel
1. The form must contains  two extra fields as follows:
1.1 A field that will   display the   reCaptcha icon and checkbox
This filed can be added to the template where you want to display the captcha (I add it just after the [[=form]] . This field will take care of displaying 
the captcha UI automatically. Nothing here for you to do (I think you change the 'I am not a Robot' language. see google,docs)
                              <div class="g-recaptcha" data-sitekey="YOUR-CAPTCHAT-SITE-KEY"></div>

1.2 When the user check the checkbox, If successfule, Captcha server(google) sends a long confirmation string and put it
 in the second field, which you should add to the form in a way that it will be returned to the controller when the form is submitted
I did it by adding this line to the controller just befor the controller returns the dict containing the forrm object: 
form.structure.insert(0, INPUT(_name='captcha_data',_id='captcha_data', _hidden=True, _value='a'))
(may be there are other ways to do it)
This make sure that the  field is returned to the controller and you can validate the information.
2. Validating the information
2.1  I do it by adding a validation=validate_user_form when creating the form by calling form = FORM(.... validation=validate_user_form ) in the controller.
You will need to  write the validate_user_form() function in order to validate the captcha.
2.2 Validation the captcha data
def validate_user_form(form):
       if verify_captcha(form.vars['captcha_data']):
          return
       form.errors['comment'] ="You are probably a Robot"

2.3 .. And the verify_captcha() looks like:

def verify_captcha(captchaData=None):
     if captchaData is None:
        return False
     data = {"secret": "YOUR-SECRET-CAPTHA-KEY", "response": captchaData}
     res = requests.post("https://www.google.com/recaptcha/api/siteverify", data=data)
     try:
       if res.json()["success"]:
         return True
       except Exception as exc:
         pass
     return False

Thats it !
Let me know if you need mor information

Massimo

unread,
Nov 11, 2024, 10:04:50 PM11/11/24
to py4web
For an arbitrary form it should be

from py4web.core.recaptcha import Recaptcha
mycaptcha = Recaptcha(api_key, api_secret)

@action("index")
@action.uses("index.html", auth, mycaptcha.fixture) # apply the fixture
def index():
fields = [Field("something"), mycaptcha.field] # include the field
form = Form(fields)
return locals()

# AND in index.html at the bottom add [[=recaptcha]] to include the required <script>

Is this nor working?

David Manns

unread,
Nov 12, 2024, 10:47:10 AM11/12/24
to py4web
Massimo's instructions don't seem to work My form displays as in the screen shot: I don't get the recaptcha button displayed.

I don't use auth, though its still initialized in common.py.

If I enter email address, I get the error message "Invalid ReCaptcha respnse", as I would expect since I can't responed
screenshot-127.0.0.1_8000-2024.11.12-10_19_37.png

David Manns

unread,
Nov 12, 2024, 2:51:35 PM11/12/24
to py4web
PS
I used
from py4web.utils.recaptcha import ReCaptcha

assuming this was the intended import.

I can't figure out the [[=recapture]] - seems to me as if this should not be valid?

David Manns

unread,
Nov 14, 2024, 3:21:02 PM11/14/24
to py4web

I have also tried icodk''s solution without success. my controller looks, with some extraneous stuff stripped, like this:

@action('login', method=['POST', 'GET'])
@action.uses("recaptcha_form.html", db, session, flash, Inject(RECAPTCHA_KEY=RECAPTCHA_KEY))
def login():
def verify_captcha(captchaData=None):
if captchaData is None:
return False
data = {"secret": RECAPTCHA_SECRET, "response": captchaData}
try:
if res.json()["success"]:
return True
except Exception as exc:
pass
return False

def validate_user_form(form):
if verify_captcha(form.vars['captcha_data']):
return
form.errors['comment'] ="You are probably a Robot"

fields = [Field('email', 'string', requires=IS_EMAIL())]
form = Form(fields, validation=validate_user_form)
   form.structure.insert(0, INPUT(_name='captcha_data',_id='captcha_data', _hidden=True, _value='a'))

if form.accepted:
redirect(URL('send_email_confirmation', vars=dict(email=form.vars['email'], url=request.query.url,
timestamp=datetime.datetime.now(TIME_ZONE).replace(tzinfo=None))))

return locals()


and recaptcha.html looks like:

[[extend 'layout.html']]
[[=header]]
[[=form]]
<div class="g-recaptcha" data-sitekey="[[=RECAPTCHA_KEY]]"></div>

There are two immediate problems. First, the recapture tickbox is not displayed. I see the email field and the form's Submit button, that's it. If I inspect the source in browser, the <div> is there, containing the correct site_key. (RECAPTCHA_KEY is defined in settings_private.py).

If I click submit, the code fails in verify_captcha() - requests is not defined? I first thought to use request, not requests, but that is not correct either.

David Manns

unread,
Nov 14, 2024, 3:51:20 PM11/14/24
to py4web
PS, I figured out to import the requests module.

But, no recaptcha widget is displayed.

If I submit a valid email, verify_captcha returns False, as I would expect.

icodk

unread,
Nov 14, 2024, 3:53:24 PM11/14/24
to py4web
Hi David
You say the tickbox is not displayed. Is any part of the recaptcha is displaied ?
Do you see something like:
recaptch_error.png
If yes you might siwtched between the two keys

David Manns

unread,
Nov 14, 2024, 4:13:52 PM11/14/24
to py4web
Hi,

No, nothing is displayed. Here is a picture of the form after filling in an email and submitting (I replaced 'comment' with 'email' so that the error displays on the email field.
screenshot-127.0.0.1_8000-2024.11.14-16_12_59.png

David Manns

unread,
Nov 14, 2024, 4:22:04 PM11/14/24
to py4web
I suspect the issue is the same with both implementations, using py4web/utils/recaptcha.py or not.

I wondered if it might have to do with extending layout.html, but same behavior without layout.html.

icodk

unread,
Nov 14, 2024, 4:28:59 PM11/14/24
to py4web
Do you include the reCaptch  script ?
<script src="https://www.google.com/recaptcha/api.js" async defer></script>

David Manns

unread,
Nov 14, 2024, 4:50:19 PM11/14/24
to py4web
Bingo! See sreenshot Thank you!

Now I have to figure out why it says this, as I do have localhost in the domain list.
screenshot-127.0.0.1_8000-2024.11.14-16_46_10.png

icodk

unread,
Nov 14, 2024, 4:54:54 PM11/14/24
to py4web
Great
I also had a problem with localhos and sudenly it start working. No idea why. If you just operated the localhost in Captch admin 
try it tomorrow again. this is my best gues.

David Manns

unread,
Nov 15, 2024, 4:59:40 PM11/15/24
to py4web
Some progress. I now see the recaptcha widget. I can tick the widget (and sometimes it displays picture test), but when the form is submitted the validation doesn't work.

In turns out that the captcha_data field still contains 'a' when the form is submitted, i.e. the confirmation string isn't delivered.

the html template is:
[[extend 'layout.html']]
[[=header]]
[[=form]]
<div class="g-recaptcha" data-sitekey="[[=RECAPTCHA_KEY]]"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>



in my controller:


def verify_captcha(captchaData=None):
if captchaData is None:
return False
data = {"secret": RECAPTCHA_SECRET, "response": captchaData}
res = requests.post("https://www.google.com/recaptcha/api/siteverify", data=data)
try:
if res.json()["success"]:
return True
except Exception as exc:
pass
return False

def validate_user_form(form):
if verify_captcha(form.vars['captcha_data']):
return
form.errors['email'] ="Please verify you are not a robot"

fields = [Field('email', 'string', requires=IS_EMAIL())]
form = Form(fields, validation=validate_user_form)
form.structure.insert(0, INPUT(_name='captcha_data',_id='captcha_data', _hidden=True, _value='a'))



I'm stuck!

David Manns

unread,
Nov 15, 2024, 5:04:37 PM11/15/24
to py4web
PS I see this in the google cloud console:

"Incomplete
Finish setting up your key: Request scores
To fully protect your site or app, finish setting up your key.
Your key is requesting tokens (executes), but isn't requesting scores (assessments)"

icodk

unread,
Nov 15, 2024, 5:22:28 PM11/15/24
to py4web

I have no idea. 
Looks like you are missing something in the configuration
I am away for the weekend but remember that there was some checkboxes
In the configuration of the recaptcha  2 configuration. Might be totally wrong about that
Will look at it on Tue next week

David Manns

unread,
Nov 16, 2024, 10:29:18 AM11/16/24
to py4web
Now that I have a working recaptcha site_key, I went back to try the py4web/utils/recaptcha solution again, as described by Massimo.

The form shows my input field for email address and instead of a recaptch widget, the heading "G Recaptcha Response"

If I enter email address and submit, I see what is shown in the attached screen snippet.

Am I correct in assuming that py4web/utils/recaptcha implements recaptcha v2, or is it intended for v3?

David
screenshot-127.0.0.1_8000-2024.11.16-10_19_41.png

David Manns

unread,
Nov 17, 2024, 5:29:45 PM11/17/24
to py4web
I have the icodk solution working! Thanks.

I needed to replace  'captcha_data' with 'g-recaptcha-response' so that the Field captures the confirmation string correctly.

David

Massimo

unread,
Nov 18, 2024, 1:00:36 AM11/18/24
to py4web
Thanks for figuring out. Would be great if you could post your final code and I will adjust the Captcha helper accordingly or deprecate it and recommend your solution instead

David Manns

unread,
Nov 18, 2024, 9:03:13 AM11/18/24
to py4web
My final login controller (simplified) and html are below. The full version also includes a rate limiter to prevent sending a second email verification message to the same email, or initiated from the same IP, within 5 minutes.

Many thanks to icodk whose solution for recaptcha v2 this is based on.


@action('login', method=['POST', 'GET'])
@action.uses("recaptcha_form.html", db, session, flash, Inject(RECAPTCHA_KEY=RECAPTCHA_KEY))
def login():
def verify_captcha(captchaData=None):
if captchaData is None:
return False
data = {"secret": RECAPTCHA_SECRET, "response": captchaData}
res = requests.post("https://www.google.com/recaptcha/api/siteverify", data=data)
try:
if res.json()["success"]:
return True
except Exception as exc:
pass
return False

def validate_user_form(form):
if verify_captcha(form.vars['g-recaptcha-response']):
return
form.errors['email'] ="Please verify you are not a robot"

fields = [Field('email', 'string', requires=IS_EMAIL())]
form = Form(fields, validation=validate_user_form)
form.structure.insert(0, INPUT(_name='g-recaptcha-response',_id='g-recaptcha-response', _hidden=True, _value='a'))

header = P(XML(f"Please specify your email to login.<br />If you have signed in previously, please use the \
same email as this identifies your record.<br />You can change your email after logging in via 'My account'.<br />If \
you no longer have access to your old email, please contact {A(SUPPORT_EMAIL, _href='mailto:'+SUPPORT_EMAIL)}."))
if form.accepted:
redirect(URL('send_email_confirmation', vars=dict(email=form.vars['email'])))

return locals()



[[extend 'layout.html']]
[[=header]]
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
[[=form]]
<div class="g-recaptcha" data-sitekey="[[=RECAPTCHA_KEY]]"></div>


icodk

unread,
Nov 18, 2024, 5:28:26 PM11/18/24
to py4web
Hi David 
Glad you manged to make it work. 
I realy do no understand that my
form.structure.insert(0, INPUT(_name='captcha_data',_id='captcha_data', _hidden=True, _value='a'))
does not work for you. It is a copy from my program.
but so be it!
Reply all
Reply to author
Forward
0 new messages