aboutsummaryrefslogtreecommitdiff
path: root/doc/todo/require_CAPTCHA_to_edit.mdwn
blob: 83ba07eb0f8bcd8e9c110c8d5b213fdb139c3229 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
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.

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.

-- [[users/Will]]

> I hate CAPTCHAs with a passion. Someone else is welcome to write such a
> plugin.
>
> If spam via openid (which I have never ever seen yet) becomes
> a problem, a provider whitelist/blacklist seems like a much nicer
> solution than a CAPTCHA. --[[Joey]]

>> Apparently there has been openid spam (you can google for it).  But as for
>> white/black lists, were you thinking of listing the openids, or the content?
>> Something like the moinmoin global <http://master.moinmo.in/BadContent>
>> list?

>>> OpenID can be thought of as pushing the problem of determining if
>>> someone is a human or a spambot back from the openid consumer to the
>>> openid provider. So, providers that make it possible for spambots to
>>> use their openids, or that are even set up explicitly for use in
>>> spamming, would be the ones to block. Or, providers that are known to
>>> use very good screening for humans would be the ones to allow.
>>> (Openid delegation makes it a bit harder than just looking at the
>>> openid url though.) --[[Joey]]

>>>> Well, OpenID only addresses authentication issues, not authorisation issues.
>>>> Given that it is trivial to set up your own OpenID provider (a full provider, not
>>>> just a forward to another provider), I can't see a
>>>> blacklist working in the long term (it would be like blacklisting email).
>>>> A whitelist might work (it would not be quite as bad as whitelisting email).  In any case,
>>>> there is now a captcha plugin for those that want it.  It is accessible
>>>> (there is an audio option) and serves a social purpose along with
>>>> keeping bots out (the captcha is used to help digitise hard to read
>>>> words in books for [Carnegie Mellon University](http://www.cs.cmu.edu/) and
>>>> [The Internet Archive](http://www.archive.org/) ).  Finally, because the actual captcha is outsourced
>>>> it means that someone else is taking care of keeping it ahead of
>>>> the bot authors.

Okie - I have a first pass of this.  There are still some issues.

Currently the code verifies the CAPTCHA.  If you get it right then you're fine.
If you get the CAPTCHA wrong then the current code tells formbuilder that
one of the fields is invalid.  This stops the login from going through.
Unfortunately, formbuilder is caching this validity somewhere, and I haven't
found a way around that yet.  This means that if you get the CAPTCHA
wrong, it will continue to fail.  You need to load the login page again so
it doesn't have the error message on the screen, then it'll work again.

> fixed this - updated code is attached.

A second issue is that the OpenID login system resets the 'required' flags
of all the other fields, so using OpenID will cause the CAPTCHA to be
ignored.

> This is still not fixed.  I would have thought the following patch would
> have fixed this second issue, but it doesn't.

(code snipped as a working [[patch]] is below)

>> What seems to be happing here is that the openid plugin defines a
>> validate hook for openid_url that calls validate(). validate() in turn
>> redirects the user to the openid server for validation, and exits. If
>> the openid plugins' validate hook is called before your recaptcha
>> validator, your code never gets a chance to run. I don't know how to
>> control the other that FormBuilder validates fields, but the only fix I
>> can see is to somehow influence that order. 
>>
>> Hmm, maybe you need to move your own validation code out of the validate
>> hook. Instead, just validate the captcha in the formbuilder_setup hook.
>> The problem with this approach is that if validation fails, you can't
>> just flag it as invalid and let formbuilder handle that. Instead, you'd
>> have to hack something in to redisplay the captcha by hand. --[[Joey]]

>>> Fixed this.  I just modified the OpenID plugin to check if the captcha
>>> succeeded or failed.  Seeing as the OpenID plugin is the one that is
>>> abusing the normal validate method, I figured it was best to keep
>>> the fix in the same place.  I also added a config switch so you can set if
>>> the captcha is needed for OpenID logins. OpenID defaults to ignoring
>>> the captcha.
>>> Patch is inline below.
>>> I think this whole thing is working now.

>>>> Ok, glad it's working. Not thrilled that it needs to modify the
>>>> openid plugin, especially as I'm not sure if i I will integrate the
>>>> captcha plugin into mainline. Also because it's not very clean to have
>>>> the oprnid plugin aware of another plugin like that. I'd like to
>>>> prusue my idea of not doing the captcha validation in the validate
>>>> hook.

--- a/IkiWiki/Plugin/openid.pm
+++ b/IkiWiki/Plugin/openid.pm
@@ -18,6 +18,7 @@ sub getopt () {
 	error($@) if $@;
 	Getopt::Long::Configure('pass_through');
 	GetOptions("openidsignup=s" => \$config{openidsignup});
+	GetOptions("openidneedscaptcha=s" => \$config{openidneedscaptcha});
 }
 
 sub formbuilder_setup (@) {
@@ -61,6 +62,7 @@ sub formbuilder_setup (@) {
 			# Skip all other required fields in this case.
 			foreach my $field ($form->field) {
 				next if $field eq "openid_url";
+				next if $config{openidneedscaptcha} && $field eq "recaptcha";
 				$form->field(name => $field, required => 0,
 					validate => '/.*/');
 			}
@@ -96,6 +98,18 @@ sub validate ($$$;$) {
 		}
 	}
 
+	if ($config{openidneedscaptcha} && defined $form->field("recaptcha")) {
+		foreach my $field ($form->field) {
+			next unless ($field eq "recaptcha");
+			if (! $field->validate) {
+				# if they didn't get the captcha right,
+				# then just claim we validated ok so the
+				# captcha can cause a fail
+				return 1;
+			}
+		}
+	}
+
 	my $check_url = $claimed_identity->check_url(
 		return_to => IkiWiki::cgiurl(do => "postsignin"),
 		trust_root => $config{cgiurl},


Instructions
=====

You need to go to <http://recaptcha.net/api/getkey> and get a key set.
The keys are added as options.

	reCaptchaPubKey => "LONGPUBLICKEYSTRING",
	reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",

You can also use "signInSSL" if you're using ssl for your login screen.


The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.

----------

#!/usr/bin/perl
# Ikiwiki password authentication.
package IkiWiki::Plugin::recaptcha;

use warnings;
use strict;
use IkiWiki 2.00;

sub import {
	hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
}

sub getopt () {
	eval q{use Getopt::Long};
	error($@) if $@;
	Getopt::Long::Configure('pass_through');
	GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
	GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
}

sub formbuilder_setup (@) {
	my %params=@_;

	my $form=$params{form};
	my $session=$params{session};
	my $cgi=$params{cgi};
	my $pubkey=$config{reCaptchaPubKey};
	my $privkey=$config{reCaptchaPrivKey};
	debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
		unless defined $config{reCaptchaPubKey};
	debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
		unless defined $config{reCaptchaPrivKey};
	my $tagtextPlain=<<EOTAG;
		<script type="text/javascript"
			src="http://api.recaptcha.net/challenge?k=$pubkey">
		</script>

		<noscript>
			<iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
				height="300" width="500" frameborder="0"></iframe><br>
			<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
			<input type="hidden" name="recaptcha_response_field" 
				value="manual_challenge">
		</noscript>
EOTAG

	my $tagtextSSL=<<EOTAGS;
		<script type="text/javascript"
			src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
		</script>

		<noscript>
			<iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
				height="300" width="500" frameborder="0"></iframe><br>
			<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
			<input type="hidden" name="recaptcha_response_field" 
				value="manual_challenge">
		</noscript>
EOTAGS

	my $tagtext;

	if ($config{signInSSL}) {
		$tagtext = $tagtextSSL;
	} else {
		$tagtext = $tagtextPlain;
	}
	
	if ($form->title eq "signin") {
		# Give up if module is unavailable to avoid
		# needing to depend on it.
		eval q{use LWP::UserAgent};
		if ($@) {
			debug("unable to load LWP::UserAgent, not enabling reCaptcha");
			return;
		}

		die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
			unless $pubkey;
		die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
			unless $privkey;
		die("To use reCAPTCHA you must know the remote IP address")
			unless $session->remote_addr();

		$form->field(
			name => "recaptcha",
			label => "",
			type => 'static',
			comment => $tagtext,
			required => 1,
			message => "CAPTCHA verification failed",
		);

		# validate the captcha.
		if ($form->submitted && $form->submitted eq "Login" &&
				defined $form->cgi_param("recaptcha_challenge_field") && 
				length $form->cgi_param("recaptcha_challenge_field") &&
				defined $form->cgi_param("recaptcha_response_field") && 
				length $form->cgi_param("recaptcha_response_field")) {

			my $challenge = "invalid";
			my $response = "invalid";
			my $result = { is_valid => 0, error => 'recaptcha-not-tested' };

			$form->field(name => "recaptcha",
				message => "CAPTCHA verification failed",
				required => 1,
				validate => sub {
					if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
							$response ne $form->cgi_param("recaptcha_response_field")) {
						$challenge = $form->cgi_param("recaptcha_challenge_field");
						$response = $form->cgi_param("recaptcha_response_field");
						debug("Validating: ".$challenge." ".$response);
						$result = check_answer($privkey,
								$session->remote_addr(),
								$challenge, $response);
					} else {
						debug("re-Validating");
					}

					if ($result->{is_valid}) {
						debug("valid");
						return 1;
					} else {
						debug("invalid");
						return 0;
					}
				});
		}
	}
}

# The following function is borrowed from
# Captcha::reCAPTCHA by Andy Armstrong and are under the PERL Artistic License

sub check_answer {
    my ( $privkey, $remoteip, $challenge, $response ) = @_;

    die
      "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
      unless $privkey;

    die "For security reasons, you must pass the remote ip to reCAPTCHA"
      unless $remoteip;

	if (! ($challenge && $response)) {
		debug("Challenge or response not set!");
		return { is_valid => 0, error => 'incorrect-captcha-sol' };
	}

	my $ua = LWP::UserAgent->new();

    my $resp = $ua->post(
        'http://api-verify.recaptcha.net/verify',
        {
            privatekey => $privkey,
            remoteip   => $remoteip,
            challenge  => $challenge,
            response   => $response
        }
    );

    if ( $resp->is_success ) {
        my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
        if ( $answer =~ /true/ ) {
            debug("CAPTCHA valid");
            return { is_valid => 1 };
        }
        else {
            chomp $message;
            debug("CAPTCHA failed: ".$message);
            return { is_valid => 0, error => $message };
        }
    }
    else {
        debug("Unable to contact reCaptcha verification host!");
        return { is_valid => 0, error => 'recaptcha-not-reachable' };
    }
}

1;