1#!/usr/bin/perl 2# Ikiwiki email address as login 3package IkiWiki::Plugin::emailauth; 4 5use warnings; 6use strict; 7use IkiWiki 3.00; 8 9sub import { 10 hook(type => "getsetup", id => "emailauth", "call" => \&getsetup); 11 hook(type => "cgi", id => "emailauth", "call" => \&cgi); 12 hook(type => "formbuilder_setup", id => "emailauth", "call" => \&formbuilder_setup); 13 IkiWiki::loadplugin("loginselector"); 14 IkiWiki::Plugin::loginselector::register_login_plugin( 15 "emailauth", 16 \&email_setup, 17 \&email_check_input, 18 \&email_auth, 19 ); 20} 21 22sub getsetup () { 23 return 24 plugin => { 25 safe => 1, 26 rebuild => 0, 27 section => "auth", 28 }, 29 emailauth_sender => { 30 type => "string", 31 description => "email address to send emailauth mails as (default: adminemail)", 32 safe => 1, 33 rebuild => 0, 34 }, 35} 36 37sub email_setup ($$) { 38 my $q=shift; 39 my $template=shift; 40 41 return 1; 42} 43 44sub email_check_input ($) { 45 my $cgi=shift; 46 defined $cgi->param('do') 47 && $cgi->param("do") eq "signin" 48 && defined $cgi->param('Email_entry') 49 && length $cgi->param('Email_entry'); 50} 51 52# Send login link to email. 53sub email_auth ($$$$) { 54 my $cgi=shift; 55 my $session=shift; 56 my $errordisplayer=shift; 57 my $infodisplayer=shift; 58 59 my $email=$cgi->param('Email_entry'); 60 unless ($email =~ /.\@./) { 61 $errordisplayer->(gettext("Invalid email address.")); 62 return; 63 } 64 65 # Implicit account creation. 66 my $userinfo=IkiWiki::userinfo_retrieve(); 67 if (! exists $userinfo->{$email} || ! ref $userinfo->{$email}) { 68 IkiWiki::userinfo_setall($email, { 69 'email' => $email, 70 'regdate' => time, 71 }); 72 } 73 74 my $token=gentoken($email, $session); 75 my $template=template("emailauth.tmpl"); 76 $template->param( 77 wikiname => $config{wikiname}, 78 # Intentionally using short field names to keep link short. 79 authurl => IkiWiki::cgiurl_abs_samescheme( 80 'e' => $email, 81 'v' => $token, 82 ), 83 ); 84 85 eval q{use Mail::Sendmail}; 86 error($@) if $@; 87 my $shorturl=$config{url}; 88 $shorturl=~s/^https?:\/\///i; 89 my $emailauth_sender=$config{emailauth_sender}; 90 $emailauth_sender=$config{adminemail} unless defined $emailauth_sender; 91 sendmail( 92 To => $email, 93 From => "$config{wikiname} admin <". 94 (defined $emailauth_sender ? $emailauth_sender : "") 95 .">", 96 Subject => "$config{wikiname} login | $shorturl", 97 Message => $template->output, 98 ) or error(sprintf(gettext("Failed to send mail: %s"), $Mail::Sendmail::error)); 99 100 $infodisplayer->(gettext("You have been sent an email, with a link you can open to complete the login process.")); 101} 102 103# Finish login process. 104sub cgi ($$) { 105 my $cgi=shift; 106 107 my $email=$cgi->param('e'); 108 my $v=$cgi->param('v'); 109 if (defined $email && defined $v && length $email && length $v) { 110 my $token=gettoken($email); 111 if ($token eq $v) { 112 cleartoken($email); 113 my $session=getsession($email); 114 IkiWiki::cgi_postsignin($cgi, $session); 115 } 116 elsif (length $token ne length $cgi->param('v')) { 117 error(gettext("Wrong login token length. Please check that you pasted in the complete login link from the email!")); 118 } 119 else { 120 loginfailure(); 121 } 122 } 123} 124 125sub formbuilder_setup (@) { 126 my %params=@_; 127 my $form=$params{form}; 128 my $session=$params{session}; 129 130 if ($form->title eq "preferences" && 131 IkiWiki::emailuser($session->param("name"))) { 132 $form->field(name => "email", disabled => 1); 133 } 134} 135 136# Generates the token that will be used in the authurl to log the user in. 137# This needs to be hard to guess, and relatively short. Generating a cgi 138# session id will make it as hard to guess as any cgi session. 139# 140# Store token in userinfo; this allows the user to log in 141# using a different browser session, if it takes a while for the 142# email to get to them. 143# 144# The postsignin value from the session is also stored in the userinfo 145# to allow resuming in a different browser session. 146sub gentoken ($$) { 147 my $email=shift; 148 my $session=shift; 149 eval q{use CGI::Session}; 150 error($@) if $@; 151 my $token = CGI::Session->new("driver:DB_File", undef, {FileName => "/dev/null"})->id; 152 IkiWiki::userinfo_set($email, "emailauthexpire", time+(60*60*24)); 153 IkiWiki::userinfo_set($email, "emailauth", $token); 154 IkiWiki::userinfo_set($email, "emailauthpostsignin", defined $session->param("postsignin") ? $session->param("postsignin") : ""); 155 return $token; 156} 157 158# Gets the token, checking for expiry. 159sub gettoken ($) { 160 my $email=shift; 161 my $val=IkiWiki::userinfo_get($email, "emailauth"); 162 my $expire=IkiWiki::userinfo_get($email, "emailauthexpire"); 163 if (! length $val || time > $expire) { 164 loginfailure(); 165 } 166 return $val; 167} 168 169# Generate a session to use after successful login. 170sub getsession ($) { 171 my $email=shift; 172 173 IkiWiki::lockwiki(); 174 IkiWiki::loadindex(); 175 my $session=IkiWiki::cgi_getsession(); 176 177 my $postsignin=IkiWiki::userinfo_get($email, "emailauthpostsignin"); 178 IkiWiki::userinfo_set($email, "emailauthpostsignin", ""); 179 if (defined $postsignin && length $postsignin) { 180 $session->param(postsignin => $postsignin); 181 } 182 183 $session->param(name => $email); 184 my $nickname=$email; 185 $nickname=~s/@.*//; 186 $session->param(nickname => Encode::decode_utf8($nickname)); 187 188 IkiWiki::cgi_savesession($session); 189 190 return $session; 191} 192 193sub cleartoken ($) { 194 my $email=shift; 195 IkiWiki::userinfo_set($email, "emailauthexpire", 0); 196 IkiWiki::userinfo_set($email, "emailauth", ""); 197} 198 199sub loginfailure () { 200 error "Bad email authentication token. Please retry login."; 201} 202 2031 204