{"id":5329,"date":"2018-04-23T11:12:28","date_gmt":"2018-04-23T16:12:28","guid":{"rendered":"https:\/\/www.poweradmin.com\/blog\/?p=5329"},"modified":"2018-04-23T11:12:28","modified_gmt":"2018-04-23T16:12:28","slug":"updating-django-osqa-to-use-recaptcha-2","status":"publish","type":"post","link":"https:\/\/www.poweradmin.com\/blog\/updating-django-osqa-to-use-recaptcha-2\/","title":{"rendered":"Updating Django OSQA to use reCaptcha 2"},"content":{"rendered":"<p><a href=\"https:\/\/www.poweradmin.com\/blog\/wp-content\/uploads\/2018\/04\/recaptcha-in-django.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-5339 alignleft\" src=\"https:\/\/www.poweradmin.com\/blog\/wp-content\/uploads\/2018\/04\/recaptcha-in-django.jpg\" alt=\"\" width=\"300\" height=\"195\"><\/a><\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">Our\u00a0<a href=\"http:\/\/support\/poweradmin.com\/osqa\/\" rel=\"nofollow\" target=\"_blank\">support site<img class=\"extlink-icon\" src=\"https:\/\/www.poweradmin.com\/blog\/wp-content\/plugins\/external-links-nofollow-open-in-new-tab-favicon\/images\/extlink.png\"><\/a>\u00a0is built on OSQA which in turn is built on Django.\u00a0 It was still using reCaptcha (version 1) which was taken over by Google, and discontinued.\u00a0 So it was time to roll up our sleeves and update to reCaptcha 2.<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">The main idea is the reCaptcha is a field based on a widget.\u00a0 In OSQA for example, when a user submits a question and their reputation is low, the question form adds a spam check field to the form using essentially<\/span><\/p>\n<p>\u00a0<\/p>\n<blockquote>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">self.fields[\u2018recaptcha\u2019] =\u00a0ReCaptchaField<\/span><\/p>\n<\/blockquote>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">ReCaptchaField is defined as<\/span><\/p>\n<p>\u00a0<\/p>\n<blockquote>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">class ReCaptchaField(forms.Field):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 def __init__(self, *args, **kwargs):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 super(ReCaptchaField, self).__init__(widget=ReCaptchaWidget)<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">class ReCaptchaWidget(forms.Widget):<\/span><\/p>\n<\/blockquote>\n<p>\u00a0<\/p>\n<p><strong>NOTE: If you copy this code, notice that WordPress has changed the single and double-quote marks.\u00a0 You\u2019ll need to change them back to the simple characters used in code.<\/strong><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">The ReCaptchaWidget renders HTML of the reCaptcha which is essentially<\/span><\/p>\n<p>\u00a0<\/p>\n<blockquote>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">&lt;script src=\u2019https:\/\/www.google.com\/recaptcha\/api.js\u2019&gt;&lt;\/script&gt;<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> &lt;div class=\u201dg-recaptcha\u201d data-sitekey=\u201d%(PublicKey)s\u201d&gt;&lt;\/div&gt;<\/span><\/p>\n<\/blockquote>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">Note that you need a PublicKey and a PrivateKey that you obtain from Google when signing up for reCaptcha.<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">When the form is posted, the reCaptcha will post an additional field,\u00a0g-recaptcha-response, along with the form post.<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">The ReCaptchaField will take that posted field and send it to a function (\u2018submit\u2019 in this case) which will do a post to Google\u2019s recaptcha\/api\/siteverify page to verify the reCaptcha was solved correctly.\u00a0 If it was solved correctly, json is returned with success : true.\u00a0 That needs to be captured and interpreted.\u00a0 You can see how this is done on\u00a0<a href=\"https:\/\/developers.google.com\/recaptcha\/docs\/verify\" target=\"_blank\" rel=\"nofollow\">Google\u2019s reCaptcha page<img class=\"extlink-icon\" src=\"https:\/\/www.poweradmin.com\/blog\/wp-content\/plugins\/external-links-nofollow-open-in-new-tab-favicon\/images\/extlink.png\"><\/a>.<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">So, without further ado, here is the code:<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">In <strong>osqa\/forum_modules\/recaptcha\/settings.py<\/strong><\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">This is pulling in the public and private keys from Django settings<\/span><\/p>\n<p>\u00a0<\/p>\n<blockquote>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">from forum.settings import EXT_KEYS_SET<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> from forum.settings.base import Setting<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">RECAPTCHA_PUB_KEY = Setting(\u2018RECAPTCHA_PUB_KEY\u2019, \u201d, EXT_KEYS_SET, dict(<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> label = \u201cRecaptch public key\u201d,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> help_text = \u201c\u201d\u201d<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> Get this key at &lt;a href=\u201dhttp:\/\/recaptcha.net\u201d&gt;reCaptcha&lt;\/a&gt; to enable<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> recaptcha anti spam through.<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u201c\u201d\u201d,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> required=False))<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">RECAPTCHA_PRIV_KEY = Setting(\u2018RECAPTCHA_PRIV_KEY\u2019, \u201d, EXT_KEYS_SET, dict(<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> label = \u201cRecaptch private key\u201d,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> help_text = \u201c\u201d\u201d<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> This is the private key you\u2019ll get in the same place as the recaptcha public key.<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u201c\u201d\u201d,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> required=False))<\/span><\/p>\n<\/blockquote>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">In <strong>osqa\/forum_modules\/handlers.py<\/strong> we have some simple glue code:<\/span><\/p>\n<p>\u00a0<\/p>\n<blockquote>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">from formfield import ReCaptchaField<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">def create_anti_spam_field():<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 return (\u2018recaptcha\u2019, ReCaptchaField())<\/span><\/p>\n<\/blockquote>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">Things start getting interesting in <strong>\/osqa\/forum_modules\/formfield.py<\/strong><\/span><\/p>\n<p>\u00a0<\/p>\n<blockquote>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">from django import forms<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> from lib import captcha<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> from django.utils.safestring import mark_safe<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> from django.utils.encoding import force_unicode, smart_unicode<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> from django.utils.translation import ugettext_lazy as _<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> import settings<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> import json<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">class ReCaptchaField(forms.Field):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 def __init__(self, *args, **kwargs):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 super(ReCaptchaField, self).__init__(widget=ReCaptchaWidget)<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">def clean(self, value):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 super(ReCaptchaField, self).clean(value)<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 recaptcha_response_value = smart_unicode(value)<\/span><\/p>\n<p> <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 check_captcha = captcha.submit(recaptcha_response_value, settings.RECAPTCHA_PRIV_KEY, {})<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 if not check_captcha.is_valid:<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 raise forms.util.ValidationError(_(\u2018Invalid captcha \u2018 + json.dumps(check_captcha.error_code)))<\/span><\/p>\n<p> <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 return value<\/span><\/p>\n<p>\u00a0<\/p>\n<p>\n <span style=\"font-family: 'courier new', courier, monospace;\"> class ReCaptchaWidget(forms.Widget):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 def render(self, name, value, attrs=None):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 return mark_safe(force_unicode(captcha.displayhtml(settings.RECAPTCHA_PUB_KEY)))<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 def value_from_datadict(self, data, files, name):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 return data.get(\u2018g-recaptcha-response\u2019, None)<\/span><\/p>\n<\/blockquote>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">And finally to the actual reCaptcha handling in <strong>osqa\/forum_modules\/recaptcha\/<span style=\"color: #0000ff;\">lib<\/span>\/captcha.py<\/strong><\/span><\/p>\n<p>\u00a0<\/p>\n<blockquote>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">import urllib2, urllib<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> import json<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">VERIFY_SERVER=\u201dwww.google.com\u201d<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">class RecaptchaResponse(object):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 def __init__(self, is_valid, error_code=None):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 self.is_valid = is_valid<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 self.error_code = error_code<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">def displayhtml (public_key):<\/span><\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 return \u201c\u201d\u201d<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 &lt;script src=\u2019https:\/\/www.google.com\/recaptcha\/api.js\u2019&gt;&lt;\/script&gt;<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 &lt;div class=\u201dg-recaptcha\u201d data-sitekey=\u201d%(PublicKey)s\u201d&gt;&lt;\/div&gt;<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u201c\u201d\u201d % {<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u2018PublicKey\u2019 : public_key,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0}<\/span><\/p>\n<p>\n <span style=\"font-family: 'courier new', courier, monospace;\"> def submit (recaptcha_response_field,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 private_key,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 remoteip):<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 if not (recaptcha_response_field and<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 len (recaptcha_response_field)):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 return RecaptchaResponse (is_valid = False, error_code = \u2018incorrect-captcha-sol\u2019)<\/span><\/p>\n<p>\u00a0<\/p>\n<p>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 def encode_if_necessary(s):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 if isinstance(s, unicode):<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0 return s.encode(\u2018utf-8\u2019)<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 return s<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 params = urllib.urlencode ({<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u2018secret\u2019: encode_if_necessary(private_key),<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u2018response\u2019 : encode_if_necessary(recaptcha_response_field),<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u2018remoteip\u2019 : encode_if_necessary(remoteip),<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 })<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 request = urllib2.Request (<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 url = \u201chttps:\/\/%s\/recaptcha\/api\/siteverify\u201d % VERIFY_SERVER,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 data = params,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 headers = {<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u201cContent-type\u201d: \u201capplication\/x-www-form-urlencoded\u201d,<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 \u201cUser-agent\u201d: \u201creCAPTCHA Python\u201d<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 }<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 )<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 httpresp = urllib2.urlopen(request)<\/span><\/p>\n<p> <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 json_data_str = httpresp.read()<\/span><\/p>\n<p> <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 httpresp.close()<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 response_dict = json.loads(json_data_str)<\/span><\/p>\n<p> <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 return_code = response_dict[\u2018success\u2019];<\/span><\/p>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: 'courier new', courier, monospace;\">\u00a0 \u00a0 if return_code == True:<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 return RecaptchaResponse(is_valid = True)<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 else:<\/span><br>\n <span style=\"font-family: 'courier new', courier, monospace;\"> \u00a0 \u00a0 \u00a0 \u00a0 return RecaptchaResponse(is_valid = False, error_code = response_dict[\u2018error-codes\u2019])<\/span><\/p>\n<\/blockquote>\n<p>\u00a0<\/p>\n<p><span style=\"font-family: verdana, geneva, sans-serif;\">The above files can be downloaded directly <a href=\"https:\/\/www.poweradmin.com\/blog\/wp-content\/uploads\/2018\/04\/recaptcha.zip\">from here.<\/a><\/span><\/p>\n<p>\u00a0<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Our\u00a0support site\u00a0is built on OSQA which in turn is built on Django.\u00a0 It was still using reCaptcha (version 1) which was taken over by Google, and discontinued.\u00a0 So it was time to roll up our sleeves and update to reCaptcha 2. \u00a0 The main idea is the reCaptcha is a field based on a widget.\u00a0 [&hellip;]<\/p>\n","protected":false},"author":3,"featured_media":5339,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[6,9],"tags":[],"class_list":["post-5329","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tech","category-technical"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/posts\/5329","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/comments?post=5329"}],"version-history":[{"count":5,"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/posts\/5329\/revisions"}],"predecessor-version":[{"id":5344,"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/posts\/5329\/revisions\/5344"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/media\/5339"}],"wp:attachment":[{"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/media?parent=5329"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/categories?post=5329"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.poweradmin.com\/blog\/wp-json\/wp\/v2\/tags?post=5329"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}