diff options
author | Joey Hess <joeyh@joeyh.name> | 2015-05-13 23:38:56 -0400 |
---|---|---|
committer | Joey Hess <joeyh@joeyh.name> | 2015-05-13 23:38:56 -0400 |
commit | cfb2c22906f41d4a4dd1c3404e8e430a35c1cd41 (patch) | |
tree | b2f0271a5fa334530b3ac3051ad42bc02172a708 /IkiWiki | |
parent | ee2905ae0a2786fd5f195fdc1779a34b5f62c1d9 (diff) | |
parent | bf8b7fe2d1602827f7419521d140bac4be6200a8 (diff) | |
download | ikiwiki-cfb2c22906f41d4a4dd1c3404e8e430a35c1cd41.tar ikiwiki-cfb2c22906f41d4a4dd1c3404e8e430a35c1cd41.tar.gz |
Merge branch 'emailauth'
Diffstat (limited to 'IkiWiki')
-rw-r--r-- | IkiWiki/Plugin/comments.pm | 6 | ||||
-rw-r--r-- | IkiWiki/Plugin/emailauth.pm | 193 | ||||
-rw-r--r-- | IkiWiki/Plugin/loginselector.pm | 132 | ||||
-rw-r--r-- | IkiWiki/Plugin/openid.pm | 82 | ||||
-rw-r--r-- | IkiWiki/Plugin/passwordauth.pm | 2 | ||||
-rw-r--r-- | IkiWiki/Setup/Automator.pm | 50 |
6 files changed, 386 insertions, 79 deletions
diff --git a/IkiWiki/Plugin/comments.pm b/IkiWiki/Plugin/comments.pm index fb423e713..eaa924e51 100644 --- a/IkiWiki/Plugin/comments.pm +++ b/IkiWiki/Plugin/comments.pm @@ -198,7 +198,6 @@ sub preprocess { $commentuser = $params{username}; my $oiduser = eval { IkiWiki::openiduser($commentuser) }; - if (defined $oiduser) { # looks like an OpenID $commentauthorurl = $commentuser; @@ -206,6 +205,11 @@ sub preprocess { $commentopenid = $commentuser; } else { + my $emailuser = IkiWiki::emailuser($commentuser); + if (defined $emailuser) { + $commentuser=$emailuser; + } + if (length $config{cgiurl}) { $commentauthorurl = IkiWiki::cgiurl( do => 'goto', diff --git a/IkiWiki/Plugin/emailauth.pm b/IkiWiki/Plugin/emailauth.pm new file mode 100644 index 000000000..aa067238f --- /dev/null +++ b/IkiWiki/Plugin/emailauth.pm @@ -0,0 +1,193 @@ +#!/usr/bin/perl +# Ikiwiki email address as login +package IkiWiki::Plugin::emailauth; + +use warnings; +use strict; +use IkiWiki 3.00; + +sub import { + hook(type => "getsetup", id => "emailauth", "call" => \&getsetup); + hook(type => "cgi", id => "emailauth", "call" => \&cgi); + hook(type => "formbuilder_setup", id => "emailauth", "call" => \&formbuilder_setup); + IkiWiki::loadplugin("loginselector"); + IkiWiki::Plugin::loginselector::register_login_plugin( + "emailauth", + \&email_setup, + \&email_check_input, + \&email_auth, + ); +} + +sub getsetup () { + return + plugin => { + safe => 1, + rebuild => 0, + section => "auth", + }, +} + +sub email_setup ($$) { + my $q=shift; + my $template=shift; + + return 1; +} + +sub email_check_input ($) { + my $cgi=shift; + defined $cgi->param('do') + && $cgi->param("do") eq "signin" + && defined $cgi->param('Email_entry') + && length $cgi->param('Email_entry'); +} + +# Send login link to email. +sub email_auth ($$$$) { + my $cgi=shift; + my $session=shift; + my $errordisplayer=shift; + my $infodisplayer=shift; + + my $email=$cgi->param('Email_entry'); + unless ($email =~ /.\@./) { + $errordisplayer->(gettext("Invalid email address.")); + return; + } + + # Implicit account creation. + my $userinfo=IkiWiki::userinfo_retrieve(); + if (! exists $userinfo->{$email} || ! ref $userinfo->{$email}) { + IkiWiki::userinfo_setall($email, { + 'email' => $email, + 'regdate' => time, + }); + } + + my $token=gentoken($email, $session); + my $template=template("emailauth.tmpl"); + $template->param( + wikiname => $config{wikiname}, + # Intentionally using short field names to keep link short. + authurl => IkiWiki::cgiurl_abs( + 'e' => $email, + 'v' => $token, + ), + ); + + eval q{use Mail::Sendmail}; + error($@) if $@; + sendmail( + To => $email, + From => "$config{wikiname} admin <". + (defined $config{adminemail} ? $config{adminemail} : "") + .">", + Subject => "$config{wikiname} login", + Message => $template->output, + ) or error(gettext("Failed to send mail")); + + $infodisplayer->(gettext("You have been sent an email, with a link you can open to complete the login process.")); +} + +# Finish login process. +sub cgi ($$) { + my $cgi=shift; + + my $email=$cgi->param('e'); + my $v=$cgi->param('v'); + if (defined $email && defined $v && length $email && length $v) { + my $token=gettoken($email); + if ($token eq $v) { + cleartoken($email); + my $session=getsession($email); + IkiWiki::cgi_postsignin($cgi, $session); + } + elsif (length $token ne length $cgi->param('v')) { + error(gettext("Wrong login token length. Please check that you pasted in the complete login link from the email!")); + } + else { + loginfailure(); + } + } +} + +sub formbuilder_setup (@) { + my %params=@_; + my $form=$params{form}; + my $session=$params{session}; + + if ($form->title eq "preferences" && + IkiWiki::emailuser($session->param("name"))) { + $form->field(name => "email", disabled => 1); + } +} + +# Generates the token that will be used in the authurl to log the user in. +# This needs to be hard to guess, and relatively short. Generating a cgi +# session id will make it as hard to guess as any cgi session. +# +# Store token in userinfo; this allows the user to log in +# using a different browser session, if it takes a while for the +# email to get to them. +# +# The postsignin value from the session is also stored in the userinfo +# to allow resuming in a different browser session. +sub gentoken ($$) { + my $email=shift; + my $session=shift; + eval q{use CGI::Session}; + error($@) if $@; + my $token = CGI::Session->new->id; + IkiWiki::userinfo_set($email, "emailauthexpire", time+(60*60*24)); + IkiWiki::userinfo_set($email, "emailauth", $token); + IkiWiki::userinfo_set($email, "emailauthpostsignin", defined $session->param("postsignin") ? $session->param("postsignin") : ""); + return $token; +} + +# Gets the token, checking for expiry. +sub gettoken ($) { + my $email=shift; + my $val=IkiWiki::userinfo_get($email, "emailauth"); + my $expire=IkiWiki::userinfo_get($email, "emailauthexpire"); + if (! length $val || time > $expire) { + loginfailure(); + } + return $val; +} + +# Generate a session to use after successful login. +sub getsession ($) { + my $email=shift; + + IkiWiki::lockwiki(); + IkiWiki::loadindex(); + my $session=IkiWiki::cgi_getsession(); + + my $postsignin=IkiWiki::userinfo_get($email, "emailauthpostsignin"); + IkiWiki::userinfo_set($email, "emailauthpostsignin", ""); + if (defined $postsignin && length $postsignin) { + $session->param(postsignin => $postsignin); + } + + $session->param(name => $email); + my $nickname=$email; + $nickname=~s/@.*//; + $session->param(nickname => Encode::decode_utf8($nickname)); + + IkiWiki::cgi_savesession($session); + + return $session; +} + +sub cleartoken ($) { + my $email=shift; + IkiWiki::userinfo_set($email, "emailauthexpire", 0); + IkiWiki::userinfo_set($email, "emailauth", ""); +} + +sub loginfailure () { + error "Bad email authentication token. Please retry login."; +} + +1 diff --git a/IkiWiki/Plugin/loginselector.pm b/IkiWiki/Plugin/loginselector.pm new file mode 100644 index 000000000..26c80b4ce --- /dev/null +++ b/IkiWiki/Plugin/loginselector.pm @@ -0,0 +1,132 @@ +#!/usr/bin/perl +package IkiWiki::Plugin::loginselector; + +use warnings; +use strict; +use IkiWiki 3.00; + +# Plugins that provide login methods can register themselves here. +# Note that the template and js file also have be be modifed to add a new +# login method. +our %login_plugins; + +sub register_login_plugin ($$$$) { + # Same as the name of the plugin that is registering itself as a + # login plugin. eg, "openid" + my $plugin_name=shift; + # This sub is passed a cgi object and a template object which it + # can manipulate. It should return true if the plugin can be used + # (it might load necessary modules for auth checking, for example). + my $plugin_setup=shift; + # This sub is passed a cgi object, and should return true + # if it looks like the user is logging in using the plugin. + my $plugin_check_input=shift; + # This sub is passed a cgi object, a session object, an error + # display callback, and an info display callback, and should + # handle the actual authentication. It can either exit w/o + # returning, if it is able to handle auth, or it can pass an + # error message to the error display callback to make the + # openid selector form be re-disiplayed with an error message + # on it. + my $plugin_auth=shift; + $login_plugins{$plugin_name}={ + setup => $plugin_setup, + check_input => $plugin_check_input, + auth => $plugin_auth, + }; +} + +sub login_selector { + my $real_cgi_signin=shift; + my $otherform_label=shift; + my $q=shift; + my $session=shift; + + my $template=IkiWiki::template("login-selector.tmpl"); + + foreach my $plugin (keys %login_plugins) { + if (! $login_plugins{$plugin}->{setup}->($template)) { + delete $login_plugins{$plugin}; + } + else { + $template->param("login_selector_$plugin", 1); + } + } + + foreach my $plugin (keys %login_plugins) { + if ($login_plugins{$plugin}->{check_input}->($q)) { + $login_plugins{$plugin}->{auth}->($q, $session, sub { + $template->param(login_error => shift()); + }, sub { + $template->param(login_info => shift()); + }); + last; + } + } + + $template->param( + cgiurl => IkiWiki::cgiurl(), + ($real_cgi_signin ? (otherform => $real_cgi_signin->($q, $session, 1)) : ()), + otherform_label => $otherform_label, + ); + + IkiWiki::printheader($session); + print IkiWiki::cgitemplate($q, "signin", $template->output); + exit; +} + +sub import { + add_underlay("login-selector"); + add_underlay("jquery"); + hook(type => "getsetup", id => "loginselector", call => \&getsetup); + hook(type => "checkconfig", id => "loginselector", call => \&checkconfig); + hook(type => "auth", id => "loginselector", call => \&authstub); +} + +sub checkconfig () { + if ($config{cgi}) { + # Intercept normal signin form, so the login selector + # can be displayed. + # + # When other auth hooks are registered, give the selector + # a reference to the normal signin form. + require IkiWiki::CGI; + my $real_cgi_signin; + my $otherform_label=gettext("Other"); + if (keys %{$IkiWiki::hooks{auth}} > 1) { + $real_cgi_signin=\&IkiWiki::cgi_signin; + # Special case to avoid labeling password auth as + # "Other" when it's the only auth plugin not + # integrated with the loginselector. + my %h=%{$IkiWiki::hooks{auth}}; + foreach my $p (keys %login_plugins) { + delete $h{$p}; + } + delete $h{loginselector}; + if (keys %h == 1 && exists $h{passwordauth}) { + $otherform_label=gettext("Password"); + } + } + inject(name => "IkiWiki::cgi_signin", call => sub ($$) { + login_selector($real_cgi_signin, $otherform_label, @_); + }); + } +} + +sub getsetup () { + return + plugin => { + # this plugin is safe but only makes sense as a + # dependency + safe => 0, + rebuild => 0, + }, +} + +sub authstub ($$) { + # While this hook is not currently used, it needs to exist + # so ikiwiki knows that the wiki supports logins, and will + # enable the Preferences page. +} + +1 diff --git a/IkiWiki/Plugin/openid.pm b/IkiWiki/Plugin/openid.pm index 00652ebb7..cc4b4ba3d 100644 --- a/IkiWiki/Plugin/openid.pm +++ b/IkiWiki/Plugin/openid.pm @@ -7,35 +7,17 @@ use strict; use IkiWiki 3.00; sub import { - add_underlay("openid-selector"); - add_underlay("jquery"); - hook(type => "checkconfig", id => "openid", call => \&checkconfig); hook(type => "getsetup", id => "openid", call => \&getsetup); hook(type => "auth", id => "openid", call => \&auth); hook(type => "formbuilder_setup", id => "openid", call => \&formbuilder_setup, last => 1); -} - -sub checkconfig () { - if ($config{cgi}) { - # Intercept normal signin form, so the openid selector - # can be displayed. - # - # When other auth hooks are registered, give the selector - # a reference to the normal signin form. - require IkiWiki::CGI; - my $real_cgi_signin; - my $nonopenidform_label=gettext("Other"); - if (keys %{$IkiWiki::hooks{auth}} > 1) { - $real_cgi_signin=\&IkiWiki::cgi_signin; - if (keys %{$IkiWiki::hooks{auth}} == 2 && exists $IkiWiki::hooks{auth}->{passwordauth}) { - $nonopenidform_label=gettext("Password"); - } - } - inject(name => "IkiWiki::cgi_signin", call => sub ($$) { - openid_selector($real_cgi_signin, $nonopenidform_label, @_); - }); - } + IkiWiki::loadplugin("loginselector"); + IkiWiki::Plugin::loginselector::register_login_plugin( + "openid", + \&openid_setup, + \&openid_check_input, + \&openid_auth, + ); } sub getsetup () { @@ -59,40 +41,34 @@ sub getsetup () { }, } -sub openid_selector { - my $real_cgi_signin=shift; - my $nonopenidform_label=shift; - my $q=shift; - my $session=shift; - - my $openid_url=$q->param('openid_identifier'); - my $openid_error; +sub openid_setup ($$) { + my $q=shift; + my $template=shift; - if (! load_openid_module()) { - if ($real_cgi_signin) { - $real_cgi_signin->($q, $session); - exit; + if (load_openid_module()) { + my $openid_url=$q->param('openid_identifier'); + if (defined $openid_url) { + $template->param(openid_url => $openid_url); } - error(sprintf(gettext("failed to load openid module: "), @_)); + return 1; } - elsif (defined $q->param("action") && $q->param("action") eq "verify") { - validate($q, $session, $openid_url, sub { - $openid_error=shift; - }); + else { + return 0; } +} - my $template=IkiWiki::template("openid-selector.tmpl"); - $template->param( - cgiurl => IkiWiki::cgiurl(), - (defined $openid_error ? (openid_error => $openid_error) : ()), - (defined $openid_url ? (openid_url => $openid_url) : ()), - ($real_cgi_signin ? (nonopenidform => $real_cgi_signin->($q, $session, 1)) : ()), - nonopenidform_label => $nonopenidform_label, - ); +sub openid_check_input ($) { + my $q=shift; + my $openid_url=$q->param('openid_identifier'); + defined $q->param("action") && $q->param("action") eq "verify" && defined $openid_url && length $openid_url; +} - IkiWiki::printheader($session); - print IkiWiki::cgitemplate($q, "signin", $template->output); - exit; +sub openid_auth ($$$$) { + my $q=shift; + my $session=shift; + my $errordisplayer=shift; + my $openid_url=$q->param('openid_identifier'); + validate($q, $session, $openid_url, $errordisplayer); } sub formbuilder_setup (@) { diff --git a/IkiWiki/Plugin/passwordauth.pm b/IkiWiki/Plugin/passwordauth.pm index 0cf2a26ea..7c01bb3ff 100644 --- a/IkiWiki/Plugin/passwordauth.pm +++ b/IkiWiki/Plugin/passwordauth.pm @@ -277,7 +277,7 @@ sub formbuilder_setup (@) { } elsif ($form->title eq "preferences") { my $user=$session->param("name"); - if (! IkiWiki::openiduser($user)) { + if (! IkiWiki::openiduser($user) && ! IkiWiki::emailuser($user)) { $form->field(name => "name", disabled => 1, value => $user, force => 1, fieldset => "login"); diff --git a/IkiWiki/Setup/Automator.pm b/IkiWiki/Setup/Automator.pm index a8b04d966..9239974ad 100644 --- a/IkiWiki/Setup/Automator.pm +++ b/IkiWiki/Setup/Automator.pm @@ -154,31 +154,33 @@ sub import (@) { foreach my $admin (@{$config{adminuser}}) { next if defined IkiWiki::openiduser($admin); - # Prompt for password w/o echo. - my ($password, $password2); - system('stty -echo 2>/dev/null'); - local $|=1; - print "\n\nCreating wiki admin $admin ...\n"; - for (;;) { - print "Choose a password: "; - chomp($password=<STDIN>); - print "\n"; - print "Confirm password: "; - chomp($password2=<STDIN>); - - last if $password2 eq $password; - - print "Password mismatch.\n\n"; - } - print "\n\n\n"; - system('stty sane 2>/dev/null'); + if (! defined IkiWiki::emailuser($admin)) { + # Prompt for password w/o echo. + my ($password, $password2); + system('stty -echo 2>/dev/null'); + local $|=1; + print "\n\nCreating wiki admin $admin ...\n"; + for (;;) { + print "Choose a password: "; + chomp($password=<STDIN>); + print "\n"; + print "Confirm password: "; + chomp($password2=<STDIN>); + + last if $password2 eq $password; + + print "Password mismatch.\n\n"; + } + print "\n\n\n"; + system('stty sane 2>/dev/null'); - if (IkiWiki::userinfo_setall($admin, { regdate => time }) && - IkiWiki::Plugin::passwordauth::setpassword($admin, $password)) { - IkiWiki::userinfo_set($admin, "email", $config{adminemail}) if defined $config{adminemail}; - } - else { - error("problem setting up $admin user"); + if (IkiWiki::userinfo_setall($admin, { regdate => time }) && + IkiWiki::Plugin::passwordauth::setpassword($admin, $password)) { + IkiWiki::userinfo_set($admin, "email", $config{adminemail}) if defined $config{adminemail}; + } + else { + error("problem setting up $admin user"); + } } } |