also tag 'patch/core', considering that over half of the changes are there
[ikiwiki] / 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.
2
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.
4
5 -- [[users/Will]]
6
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]]
13
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?
18
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]]
27
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.
40
41 Okie - I have a first pass of this.  There are still some issues.
42
43 Currently the code verifies the CAPTCHA.  If you get it right then you're fine.
44 If you get the CAPTCHA wrong then the current code tells formbuilder that
45 one of the fields is invalid.  This stops the login from going through.
46 Unfortunately, formbuilder is caching this validity somewhere, and I haven't
47 found a way around that yet.  This means that if you get the CAPTCHA
48 wrong, it will continue to fail.  You need to load the login page again so
49 it doesn't have the error message on the screen, then it'll work again.
50
51 > fixed this - updated code is attached.
52
53 A second issue is that the OpenID login system resets the 'required' flags
54 of all the other fields, so using OpenID will cause the CAPTCHA to be
55 ignored.
56
57 > This is still not fixed.  I would have thought the following patch would
58 > have fixed this second issue, but it doesn't.
59
60 (code snipped as a working [[patch]] is below)
61
62 >> What seems to be happing here is that the openid plugin defines a
63 >> validate hook for openid_url that calls validate(). validate() in turn
64 >> redirects the user to the openid server for validation, and exits. If
65 >> the openid plugins' validate hook is called before your recaptcha
66 >> validator, your code never gets a chance to run. I don't know how to
67 >> control the other that FormBuilder validates fields, but the only fix I
68 >> can see is to somehow influence that order. 
69 >>
70 >> Hmm, maybe you need to move your own validation code out of the validate
71 >> hook. Instead, just validate the captcha in the formbuilder_setup hook.
72 >> The problem with this approach is that if validation fails, you can't
73 >> just flag it as invalid and let formbuilder handle that. Instead, you'd
74 >> have to hack something in to redisplay the captcha by hand. --[[Joey]]
75
76 >>> Fixed this.  I just modified the OpenID plugin to check if the captcha
77 >>> succeeded or failed.  Seeing as the OpenID plugin is the one that is
78 >>> abusing the normal validate method, I figured it was best to keep
79 >>> the fix in the same place.  I also added a config switch so you can set if
80 >>> the captcha is needed for OpenID logins. OpenID defaults to ignoring
81 >>> the captcha.
82 >>> Patch is inline below.
83 >>> I think this whole thing is working now.
84
85 >>>> Ok, glad it's working. Not thrilled that it needs to modify the
86 >>>> openid plugin, especially as I'm not sure if i I will integrate the
87 >>>> captcha plugin into mainline. Also because it's not very clean to have
88 >>>> the oprnid plugin aware of another plugin like that. I'd like to
89 >>>> prusue my idea of not doing the captcha validation in the validate
90 >>>> hook.
91
92 --- a/IkiWiki/Plugin/openid.pm
93 +++ b/IkiWiki/Plugin/openid.pm
94 @@ -18,6 +18,7 @@ sub getopt () {
95         error($@) if $@;
96         Getopt::Long::Configure('pass_through');
97         GetOptions("openidsignup=s" => \$config{openidsignup});
98 +       GetOptions("openidneedscaptcha=s" => \$config{openidneedscaptcha});
99  }
100  
101  sub formbuilder_setup (@) {
102 @@ -61,6 +62,7 @@ sub formbuilder_setup (@) {
103                         # Skip all other required fields in this case.
104                         foreach my $field ($form->field) {
105                                 next if $field eq "openid_url";
106 +                               next if $config{openidneedscaptcha} && $field eq "recaptcha";
107                                 $form->field(name => $field, required => 0,
108                                         validate => '/.*/');
109                         }
110 @@ -96,6 +98,18 @@ sub validate ($$$;$) {
111                 }
112         }
113  
114 +       if ($config{openidneedscaptcha} && defined $form->field("recaptcha")) {
115 +               foreach my $field ($form->field) {
116 +                       next unless ($field eq "recaptcha");
117 +                       if (! $field->validate) {
118 +                               # if they didn't get the captcha right,
119 +                               # then just claim we validated ok so the
120 +                               # captcha can cause a fail
121 +                               return 1;
122 +                       }
123 +               }
124 +       }
125 +
126         my $check_url = $claimed_identity->check_url(
127                 return_to => IkiWiki::cgiurl(do => "postsignin"),
128                 trust_root => $config{cgiurl},
129
130
131 Instructions
132 =====
133
134 You need to go to <http://recaptcha.net/api/getkey> and get a key set.
135 The keys are added as options.
136
137         reCaptchaPubKey => "LONGPUBLICKEYSTRING",
138         reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",
139
140 You can also use "signInSSL" if you're using ssl for your login screen.
141
142
143 The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.
144
145 ----------
146
147 #!/usr/bin/perl
148 # Ikiwiki password authentication.
149 package IkiWiki::Plugin::recaptcha;
150
151 use warnings;
152 use strict;
153 use IkiWiki 2.00;
154
155 sub import {
156         hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
157 }
158
159 sub getopt () {
160         eval q{use Getopt::Long};
161         error($@) if $@;
162         Getopt::Long::Configure('pass_through');
163         GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
164         GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
165 }
166
167 sub formbuilder_setup (@) {
168         my %params=@_;
169
170         my $form=$params{form};
171         my $session=$params{session};
172         my $cgi=$params{cgi};
173         my $pubkey=$config{reCaptchaPubKey};
174         my $privkey=$config{reCaptchaPrivKey};
175         debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
176                 unless defined $config{reCaptchaPubKey};
177         debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
178                 unless defined $config{reCaptchaPrivKey};
179         my $tagtextPlain=<<EOTAG;
180                 <script type="text/javascript"
181                         src="http://api.recaptcha.net/challenge?k=$pubkey">
182                 </script>
183
184                 <noscript>
185                         <iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
186                                 height="300" width="500" frameborder="0"></iframe><br>
187                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
188                         <input type="hidden" name="recaptcha_response_field" 
189                                 value="manual_challenge">
190                 </noscript>
191 EOTAG
192
193         my $tagtextSSL=<<EOTAGS;
194                 <script type="text/javascript"
195                         src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
196                 </script>
197
198                 <noscript>
199                         <iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
200                                 height="300" width="500" frameborder="0"></iframe><br>
201                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
202                         <input type="hidden" name="recaptcha_response_field" 
203                                 value="manual_challenge">
204                 </noscript>
205 EOTAGS
206
207         my $tagtext;
208
209         if ($config{signInSSL}) {
210                 $tagtext = $tagtextSSL;
211         } else {
212                 $tagtext = $tagtextPlain;
213         }
214         
215         if ($form->title eq "signin") {
216                 # Give up if module is unavailable to avoid
217                 # needing to depend on it.
218                 eval q{use LWP::UserAgent};
219                 if ($@) {
220                         debug("unable to load LWP::UserAgent, not enabling reCaptcha");
221                         return;
222                 }
223
224                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
225                         unless $pubkey;
226                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
227                         unless $privkey;
228                 die("To use reCAPTCHA you must know the remote IP address")
229                         unless $session->remote_addr();
230
231                 $form->field(
232                         name => "recaptcha",
233                         label => "",
234                         type => 'static',
235                         comment => $tagtext,
236                         required => 1,
237                         message => "CAPTCHA verification failed",
238                 );
239
240                 # validate the captcha.
241                 if ($form->submitted && $form->submitted eq "Login" &&
242                                 defined $form->cgi_param("recaptcha_challenge_field") && 
243                                 length $form->cgi_param("recaptcha_challenge_field") &&
244                                 defined $form->cgi_param("recaptcha_response_field") && 
245                                 length $form->cgi_param("recaptcha_response_field")) {
246
247                         my $challenge = "invalid";
248                         my $response = "invalid";
249                         my $result = { is_valid => 0, error => 'recaptcha-not-tested' };
250
251                         $form->field(name => "recaptcha",
252                                 message => "CAPTCHA verification failed",
253                                 required => 1,
254                                 validate => sub {
255                                         if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
256                                                         $response ne $form->cgi_param("recaptcha_response_field")) {
257                                                 $challenge = $form->cgi_param("recaptcha_challenge_field");
258                                                 $response = $form->cgi_param("recaptcha_response_field");
259                                                 debug("Validating: ".$challenge." ".$response);
260                                                 $result = check_answer($privkey,
261                                                                 $session->remote_addr(),
262                                                                 $challenge, $response);
263                                         } else {
264                                                 debug("re-Validating");
265                                         }
266
267                                         if ($result->{is_valid}) {
268                                                 debug("valid");
269                                                 return 1;
270                                         } else {
271                                                 debug("invalid");
272                                                 return 0;
273                                         }
274                                 });
275                 }
276         }
277 }
278
279 # The following function is borrowed from
280 # Captcha::reCAPTCHA by Andy Armstrong and are under the PERL Artistic License
281
282 sub check_answer {
283     my ( $privkey, $remoteip, $challenge, $response ) = @_;
284
285     die
286       "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
287       unless $privkey;
288
289     die "For security reasons, you must pass the remote ip to reCAPTCHA"
290       unless $remoteip;
291
292         if (! ($challenge && $response)) {
293                 debug("Challenge or response not set!");
294                 return { is_valid => 0, error => 'incorrect-captcha-sol' };
295         }
296
297         my $ua = LWP::UserAgent->new();
298
299     my $resp = $ua->post(
300         'http://api-verify.recaptcha.net/verify',
301         {
302             privatekey => $privkey,
303             remoteip   => $remoteip,
304             challenge  => $challenge,
305             response   => $response
306         }
307     );
308
309     if ( $resp->is_success ) {
310         my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
311         if ( $answer =~ /true/ ) {
312             debug("CAPTCHA valid");
313             return { is_valid => 1 };
314         }
315         else {
316             chomp $message;
317             debug("CAPTCHA failed: ".$message);
318             return { is_valid => 0, error => $message };
319         }
320     }
321     else {
322         debug("Unable to contact reCaptcha verification host!");
323         return { is_valid => 0, error => 'recaptcha-not-reachable' };
324     }
325 }
326
327 1;