]> git.vanrenterghem.biz Git - git.ikiwiki.info.git/blob - doc/todo/require_CAPTCHA_to_edit.mdwn
fix formatting in code samples
[git.ikiwiki.info.git] / doc / todo / require_CAPTCHA_to_edit.mdwn
1 I don't necessarily trust all OpenID providers to stop bots.  I note that ikiwiki allows [[banned_users]], and that there are other todos such as [[todo/openid_user_filtering]] that would extend this.  However, it might be nice to have a CAPTCHA system.
3 I imagine a plugin that modifies the login screen to use <http://recaptcha.net/>.  You would then be required to fill in the captcha as well as log in in the normal way.
5 -- [[users/Will]]
7 > I hate CAPTCHAs with a passion. Someone else is welcome to write such a
8 > plugin.
9 >
10 > If spam via openid (which I have never ever seen yet) becomes
11 > a problem, a provider whitelist/blacklist seems like a much nicer
12 > solution than a CAPTCHA. --[[Joey]]
14 >> Apparently there has been openid spam (you can google for it).  But as for
15 >> white/black lists, were you thinking of listing the openids, or the content?
16 >> Something like the moinmoin global <http://master.moinmo.in/BadContent>
17 >> list?
19 >>> OpenID can be thought of as pushing the problem of determining if
20 >>> someone is a human or a spambot back from the openid consumer to the
21 >>> openid provider. So, providers that make it possible for spambots to
22 >>> use their openids, or that are even set up explicitly for use in
23 >>> spamming, would be the ones to block. Or, providers that are known to
24 >>> use very good screening for humans would be the ones to allow.
25 >>> (Openid delegation makes it a bit harder than just looking at the
26 >>> openid url though.) --[[Joey]]
28 >>>> Well, OpenID only addresses authentication issues, not authorisation issues.
29 >>>> Given that it is trivial to set up your own OpenID provider (a full provider, not
30 >>>> just a forward to another provider), I can't see a
31 >>>> blacklist working in the long term (it would be like blacklisting email).
32 >>>> A whitelist might work (it would not be quite as bad as whitelisting email).  In any case,
33 >>>> there is now a captcha plugin for those that want it.  It is accessible
34 >>>> (there is an audio option) and serves a social purpose along with
35 >>>> keeping bots out (the captcha is used to help digitise hard to read
36 >>>> words in books for [Carnegie Mellon University](http://www.cs.cmu.edu/) and
37 >>>> [The Internet Archive](http://www.archive.org/) ).  Finally, because the actual captcha is outsourced
38 >>>> it means that someone else is taking care of keeping it ahead of
39 >>>> the bot authors.
41 >> As [[spam_fighting]] shows, OpenID spam is now real. Yahoo, at least, would need to be blocked, according to the above, which seems like a bold move. --[[anarcat]]
43 Okie - I have a first pass of this.  There are still some issues.
45 Currently the code verifies the CAPTCHA.  If you get it right then you're fine.
46 If you get the CAPTCHA wrong then the current code tells formbuilder that
47 one of the fields is invalid.  This stops the login from going through.
48 Unfortunately, formbuilder is caching this validity somewhere, and I haven't
49 found a way around that yet.  This means that if you get the CAPTCHA
50 wrong, it will continue to fail.  You need to load the login page again so
51 it doesn't have the error message on the screen, then it'll work again.
53 > fixed this - updated code is attached.
55 A second issue is that the OpenID login system resets the 'required' flags
56 of all the other fields, so using OpenID will cause the CAPTCHA to be
57 ignored.
59 > This is still not fixed.  I would have thought the following patch would
60 > have fixed this second issue, but it doesn't.
62 (code snipped as a working [[patch]] is below)
64 >> What seems to be happing here is that the openid plugin defines a
65 >> validate hook for openid_url that calls validate(). validate() in turn
66 >> redirects the user to the openid server for validation, and exits. If
67 >> the openid plugins' validate hook is called before your recaptcha
68 >> validator, your code never gets a chance to run. I don't know how to
69 >> control the other that FormBuilder validates fields, but the only fix I
70 >> can see is to somehow influence that order. 
71 >>
72 >> Hmm, maybe you need to move your own validation code out of the validate
73 >> hook. Instead, just validate the captcha in the formbuilder_setup hook.
74 >> The problem with this approach is that if validation fails, you can't
75 >> just flag it as invalid and let formbuilder handle that. Instead, you'd
76 >> have to hack something in to redisplay the captcha by hand. --[[Joey]]
78 >>> Fixed this.  I just modified the OpenID plugin to check if the captcha
79 >>> succeeded or failed.  Seeing as the OpenID plugin is the one that is
80 >>> abusing the normal validate method, I figured it was best to keep
81 >>> the fix in the same place.  I also added a config switch so you can set if
82 >>> the captcha is needed for OpenID logins. OpenID defaults to ignoring
83 >>> the captcha.
84 >>> Patch is inline below.
85 >>> I think this whole thing is working now.
87 >>>> Ok, glad it's working. Not thrilled that it needs to modify the
88 >>>> openid plugin, especially as I'm not sure if i I will integrate the
89 >>>> captcha plugin into mainline. Also because it's not very clean to have
90 >>>> the oprnid plugin aware of another plugin like that. I'd like to
91 >>>> prusue my idea of not doing the captcha validation in the validate
92 >>>> hook.
94 [[!format diff """
95 --- a/IkiWiki/Plugin/openid.pm
96 +++ b/IkiWiki/Plugin/openid.pm
97 @@ -18,6 +18,7 @@ sub getopt () {
98         error($@) if $@;
99         Getopt::Long::Configure('pass_through');
100         GetOptions("openidsignup=s" => \$config{openidsignup});
101 +       GetOptions("openidneedscaptcha=s" => \$config{openidneedscaptcha});
102  }
103  
104  sub formbuilder_setup (@) {
105 @@ -61,6 +62,7 @@ sub formbuilder_setup (@) {
106                         # Skip all other required fields in this case.
107                         foreach my $field ($form->field) {
108                                 next if $field eq "openid_url";
109 +                               next if $config{openidneedscaptcha} && $field eq "recaptcha";
110                                 $form->field(name => $field, required => 0,
111                                         validate => '/.*/');
112                         }
113 @@ -96,6 +98,18 @@ sub validate ($$$;$) {
114                 }
115         }
116  
117 +       if ($config{openidneedscaptcha} && defined $form->field("recaptcha")) {
118 +               foreach my $field ($form->field) {
119 +                       next unless ($field eq "recaptcha");
120 +                       if (! $field->validate) {
121 +                               # if they didn't get the captcha right,
122 +                               # then just claim we validated ok so the
123 +                               # captcha can cause a fail
124 +                               return 1;
125 +                       }
126 +               }
127 +       }
129         my $check_url = $claimed_identity->check_url(
130                 return_to => IkiWiki::cgiurl(do => "postsignin"),
131                 trust_root => $config{cgiurl},
133 """]]
135 Instructions
136 =====
138 You need to go to <http://recaptcha.net/api/getkey> and get a key set.
139 The keys are added as options.
141 [[!format perl """
142 reCaptchaPubKey => "LONGPUBLICKEYSTRING",
143 reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",
144 """]]
146 You can also use "signInSSL" if you're using ssl for your login screen.
149 The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.
151 ----------
153 [[!format perl """
154 #!/usr/bin/perl
155 # Ikiwiki password authentication.
156 package IkiWiki::Plugin::recaptcha;
158 use warnings;
159 use strict;
160 use IkiWiki 2.00;
162 sub import {
163         hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
166 sub getopt () {
167         eval q{use Getopt::Long};
168         error($@) if $@;
169         Getopt::Long::Configure('pass_through');
170         GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
171         GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
174 sub formbuilder_setup (@) {
175         my %params=@_;
177         my $form=$params{form};
178         my $session=$params{session};
179         my $cgi=$params{cgi};
180         my $pubkey=$config{reCaptchaPubKey};
181         my $privkey=$config{reCaptchaPrivKey};
182         debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
183                 unless defined $config{reCaptchaPubKey};
184         debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
185                 unless defined $config{reCaptchaPrivKey};
186         my $tagtextPlain=<<EOTAG;
187                 <script type="text/javascript"
188                         src="http://api.recaptcha.net/challenge?k=$pubkey">
189                 </script>
191                 <noscript>
192                         <iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
193                                 height="300" width="500" frameborder="0"></iframe><br>
194                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
195                         <input type="hidden" name="recaptcha_response_field" 
196                                 value="manual_challenge">
197                 </noscript>
198 EOTAG
200         my $tagtextSSL=<<EOTAGS;
201                 <script type="text/javascript"
202                         src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
203                 </script>
205                 <noscript>
206                         <iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
207                                 height="300" width="500" frameborder="0"></iframe><br>
208                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
209                         <input type="hidden" name="recaptcha_response_field" 
210                                 value="manual_challenge">
211                 </noscript>
212 EOTAGS
214         my $tagtext;
216         if ($config{signInSSL}) {
217                 $tagtext = $tagtextSSL;
218         } else {
219                 $tagtext = $tagtextPlain;
220         }
221         
222         if ($form->title eq "signin") {
223                 # Give up if module is unavailable to avoid
224                 # needing to depend on it.
225                 eval q{use LWP::UserAgent};
226                 if ($@) {
227                         debug("unable to load LWP::UserAgent, not enabling reCaptcha");
228                         return;
229                 }
231                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
232                         unless $pubkey;
233                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
234                         unless $privkey;
235                 die("To use reCAPTCHA you must know the remote IP address")
236                         unless $session->remote_addr();
238                 $form->field(
239                         name => "recaptcha",
240                         label => "",
241                         type => 'static',
242                         comment => $tagtext,
243                         required => 1,
244                         message => "CAPTCHA verification failed",
245                 );
247                 # validate the captcha.
248                 if ($form->submitted && $form->submitted eq "Login" &&
249                                 defined $form->cgi_param("recaptcha_challenge_field") && 
250                                 length $form->cgi_param("recaptcha_challenge_field") &&
251                                 defined $form->cgi_param("recaptcha_response_field") && 
252                                 length $form->cgi_param("recaptcha_response_field")) {
254                         my $challenge = "invalid";
255                         my $response = "invalid";
256                         my $result = { is_valid => 0, error => 'recaptcha-not-tested' };
258                         $form->field(name => "recaptcha",
259                                 message => "CAPTCHA verification failed",
260                                 required => 1,
261                                 validate => sub {
262                                         if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
263                                                         $response ne $form->cgi_param("recaptcha_response_field")) {
264                                                 $challenge = $form->cgi_param("recaptcha_challenge_field");
265                                                 $response = $form->cgi_param("recaptcha_response_field");
266                                                 debug("Validating: ".$challenge." ".$response);
267                                                 $result = check_answer($privkey,
268                                                                 $session->remote_addr(),
269                                                                 $challenge, $response);
270                                         } else {
271                                                 debug("re-Validating");
272                                         }
274                                         if ($result->{is_valid}) {
275                                                 debug("valid");
276                                                 return 1;
277                                         } else {
278                                                 debug("invalid");
279                                                 return 0;
280                                         }
281                                 });
282                 }
283         }
286 # The following function is borrowed from
287 # Captcha::reCAPTCHA by Andy Armstrong and are under the PERL Artistic License
289 sub check_answer {
290     my ( $privkey, $remoteip, $challenge, $response ) = @_;
292     die
293       "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
294       unless $privkey;
296     die "For security reasons, you must pass the remote ip to reCAPTCHA"
297       unless $remoteip;
299         if (! ($challenge && $response)) {
300                 debug("Challenge or response not set!");
301                 return { is_valid => 0, error => 'incorrect-captcha-sol' };
302         }
304         my $ua = LWP::UserAgent->new();
306     my $resp = $ua->post(
307         'http://api-verify.recaptcha.net/verify',
308         {
309             privatekey => $privkey,
310             remoteip   => $remoteip,
311             challenge  => $challenge,
312             response   => $response
313         }
314     );
316     if ( $resp->is_success ) {
317         my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
318         if ( $answer =~ /true/ ) {
319             debug("CAPTCHA valid");
320             return { is_valid => 1 };
321         }
322         else {
323             chomp $message;
324             debug("CAPTCHA failed: ".$message);
325             return { is_valid => 0, error => $message };
326         }
327     }
328     else {
329         debug("Unable to contact reCaptcha verification host!");
330         return { is_valid => 0, error => 'recaptcha-not-reachable' };
331     }
334 1;
335 """]]