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