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.
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]]
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.
--- a/IkiWiki/Plugin/openid.pm
+++ b/IkiWiki/Plugin/openid.pm
@@ -61,6 +61,7 @@ sub formbuilder_setup (@) { #{{{
# Skip all other required fields in this case.
foreach my $field ($form->field) {
next if $field eq "openid_url";
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]]
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;