328 lines
12 KiB
Markdown
328 lines
12 KiB
Markdown
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;
|