1#!/usr/bin/perl 2# Ikiwiki password authentication. 3package IkiWiki::Plugin::passwordauth; 4 5use warnings; 6use strict; 7use IkiWiki 3.00; 8 9sub import { 10 hook(type => "getsetup", id => "passwordauth", "call" => \&getsetup); 11 hook(type => "formbuilder_setup", id => "passwordauth", call => \&formbuilder_setup); 12 hook(type => "formbuilder", id => "passwordauth", call => \&formbuilder); 13 hook(type => "sessioncgi", id => "passwordauth", call => \&sessioncgi); 14 hook(type => "auth", id => "passwordauth", call => \&auth); 15} 16 17sub getsetup () { 18 return 19 plugin => { 20 safe => 1, 21 rebuild => 0, 22 section => "auth", 23 }, 24 account_creation_password => { 25 type => "string", 26 example => "s3cr1t", 27 description => "a password that must be entered when signing up for an account", 28 safe => 1, 29 rebuild => 0, 30 }, 31 password_cost => { 32 type => "integer", 33 example => 8, 34 description => "cost of generating a password using Authen::Passphrase::BlowfishCrypt", 35 safe => 1, 36 rebuild => 0, 37 }, 38} 39 40# Checks if a string matches a user's password, and returns true or false. 41sub checkpassword ($$;$) { 42 my $user=shift; 43 my $password=shift; 44 my $field=shift || "password"; 45 46 # It's very important that the user not be allowed to log in with 47 # an empty password! 48 if (! length $password) { 49 return 0; 50 } 51 52 my $userinfo=IkiWiki::userinfo_retrieve(); 53 if (! length $user || ! defined $userinfo || 54 ! exists $userinfo->{$user} || ! ref $userinfo->{$user}) { 55 return 0; 56 } 57 58 my $ret=0; 59 if (exists $userinfo->{$user}->{"crypt".$field}) { 60 eval q{use Authen::Passphrase}; 61 error $@ if $@; 62 my $p = Authen::Passphrase->from_crypt($userinfo->{$user}->{"crypt".$field}); 63 $ret=$p->match($password); 64 } 65 elsif (exists $userinfo->{$user}->{$field}) { 66 $ret=$password eq $userinfo->{$user}->{$field}; 67 } 68 69 if ($ret && 70 (exists $userinfo->{$user}->{resettoken} || 71 exists $userinfo->{$user}->{cryptresettoken})) { 72 # Clear reset token since the user has successfully logged in. 73 delete $userinfo->{$user}->{resettoken}; 74 delete $userinfo->{$user}->{cryptresettoken}; 75 IkiWiki::userinfo_store($userinfo); 76 } 77 78 return $ret; 79} 80 81sub setpassword ($$;$) { 82 my $user=shift; 83 my $password=shift; 84 my $field=shift || "password"; 85 86 eval q{use Authen::Passphrase::BlowfishCrypt}; 87 if (! $@) { 88 my $p = Authen::Passphrase::BlowfishCrypt->new( 89 cost => $config{password_cost} || 8, 90 salt_random => 1, 91 passphrase => $password, 92 ); 93 IkiWiki::userinfo_set($user, "crypt$field", $p->as_crypt); 94 IkiWiki::userinfo_set($user, $field, ""); 95 } 96 else { 97 IkiWiki::userinfo_set($user, $field, $password); 98 } 99 100 # Setting the password clears any passwordless login token. 101 if ($field ne 'passwordless') { 102 IkiWiki::userinfo_set($user, "passwordless", ""); 103 } 104} 105 106# Generates a token that can be used to log the user in. 107# This needs to be hard to guess. Generating a cgi session id will 108# make it as hard to guess as any cgi session. 109sub gentoken ($$;$) { 110 my $user=shift; 111 my $tokenfield=shift; 112 my $reversable=shift; 113 114 eval q{use CGI::Session}; 115 error($@) if $@; 116 my $token = CGI::Session->new("driver:DB_File", undef, {FileName => "/dev/null"})->id; 117 if (! $reversable) { 118 setpassword($user, $token, $tokenfield); 119 } 120 else { 121 IkiWiki::userinfo_set($user, $tokenfield, $token); 122 } 123 return $token; 124} 125 126# An anonymous user has no normal password, only a passwordless login 127# token. Given an email address, this sets up such a user for that email, 128# unless one already exists, and returns the username. 129sub anonuser ($) { 130 my $email=shift; 131 132 # Want a username for this email that won't overlap with any other. 133 my $user=$email; 134 $user=~s/@/_/g; 135 136 my $userinfo=IkiWiki::userinfo_retrieve(); 137 if (! exists $userinfo->{$user} || ! ref $userinfo->{$user}) { 138 if (IkiWiki::userinfo_setall($user, { 139 'email' => $email, 140 'regdate' => time})) { 141 gentoken($user, "passwordless", 1); 142 return $user; 143 } 144 else { 145 error(gettext("Error creating account.")); 146 } 147 } 148 elsif (defined anonusertoken($userinfo->{$user})) { 149 return $user; 150 } 151 else { 152 return undef; 153 } 154} 155 156sub anonusertoken ($) { 157 my $userhash=shift; 158 if (exists $userhash->{passwordless} && 159 length $userhash->{passwordless}) { 160 return $userhash->{passwordless}; 161 } 162 else { 163 return undef; 164 } 165} 166 167sub formbuilder_setup (@) { 168 my %params=@_; 169 170 my $form=$params{form}; 171 my $session=$params{session}; 172 my $cgi=$params{cgi}; 173 174 my $do_register=defined $cgi->param("do") && $cgi->param("do") eq "register"; 175 176 if ($form->title eq "signin" || $form->title eq "register" || $do_register) { 177 $form->field(name => "name", required => 0); 178 $form->field(name => "password", type => "password", required => 0); 179 180 if ($form->submitted eq "Register" || $form->submitted eq "Create Account" || $do_register) { 181 $form->field(name => "confirm_password", type => "password"); 182 $form->field(name => "account_creation_password", type => "password") 183 if (defined $config{account_creation_password} && 184 length $config{account_creation_password}); 185 $form->field(name => "email", size => 50); 186 $form->title("register"); 187 $form->text(""); 188 189 $form->field(name => "confirm_password", 190 validate => sub { 191 shift eq $form->field("password"); 192 }, 193 ); 194 $form->field(name => "password", 195 validate => sub { 196 shift eq $form->field("confirm_password"); 197 }, 198 ); 199 } 200 201 if ($form->submitted) { 202 my $submittype=$form->submitted; 203 # Set required fields based on how form was submitted. 204 my %required=( 205 "Login" => [qw(name password)], 206 "Register" => [], 207 "Create Account" => [qw(name password confirm_password email)], 208 "Reset Password" => [qw(name)], 209 ); 210 foreach my $opt (@{$required{$submittype}}) { 211 $form->field(name => $opt, required => 1); 212 } 213 214 if ($submittype eq "Create Account") { 215 $form->field( 216 name => "account_creation_password", 217 validate => sub { 218 shift eq $config{account_creation_password}; 219 }, 220 required => 1, 221 ) if (defined $config{account_creation_password} && 222 length $config{account_creation_password}); 223 $form->field( 224 name => "email", 225 validate => "EMAIL", 226 ); 227 } 228 229 # Validate password against name for Login. 230 if ($submittype eq "Login") { 231 $form->field( 232 name => "password", 233 validate => sub { 234 checkpassword(scalar $form->field("name"), shift); 235 }, 236 ); 237 } 238 elsif ($submittype eq "Register" || 239 $submittype eq "Create Account" || 240 $submittype eq "Reset Password") { 241 $form->field(name => "password", validate => 'VALUE'); 242 } 243 244 # And make sure the entered name exists when logging 245 # in or sending email, and does not when registering. 246 if ($submittype eq 'Create Account' || 247 $submittype eq 'Register') { 248 $form->field( 249 name => "name", 250 validate => sub { 251 my $name=shift; 252 length $name && 253 $name=~/$config{wiki_file_regexp}/ && 254 # don't allow registering 255 # accounts that look like 256 # openids, or email 257 # addresses, even if the 258 # file regexp allows it 259 $name!~/[\/:\@]/ && 260 ! IkiWiki::userinfo_get($name, "regdate"); 261 }, 262 ); 263 } 264 elsif ($submittype eq "Login" || 265 $submittype eq "Reset Password") { 266 $form->field( 267 name => "name", 268 validate => sub { 269 my $name=shift; 270 length $name && 271 IkiWiki::userinfo_get($name, "regdate"); 272 }, 273 ); 274 } 275 } 276 else { 277 # First time settings. 278 $form->field(name => "name"); 279 if ($session->param("name")) { 280 $form->field(name => "name", value => $session->param("name")); 281 } 282 } 283 } 284 elsif ($form->title eq "preferences") { 285 my $user=$session->param("name"); 286 if (! IkiWiki::openiduser($user) && ! IkiWiki::emailuser($user)) { 287 $form->field(name => "name", disabled => 1, 288 value => $user, force => 1, 289 fieldset => "login"); 290 $form->field(name => "password", type => "password", 291 fieldset => "login", 292 validate => sub { 293 shift eq $form->field("confirm_password"); 294 }); 295 $form->field(name => "confirm_password", type => "password", 296 fieldset => "login", 297 validate => sub { 298 shift eq $form->field("password"); 299 }); 300 301 my $userpage=IkiWiki::userpage($user); 302 if (exists $pagesources{$userpage}) { 303 $form->text(gettext("Your user page: "). 304 htmllink("", "", $userpage, 305 noimageinline => 1)); 306 } 307 else { 308 $form->text("<a rel=\"nofollow\" href=\"". 309 IkiWiki::cgiurl(do => "edit", page => $userpage). 310 "\">".gettext("Create your user page")."</a>"); 311 } 312 } 313 } 314} 315 316sub formbuilder (@) { 317 my %params=@_; 318 319 my $form=$params{form}; 320 my $session=$params{session}; 321 my $cgi=$params{cgi}; 322 my $buttons=$params{buttons}; 323 324 my $do_register=defined $cgi->param("do") && $cgi->param("do") eq "register"; 325 326 if ($form->title eq "signin" || $form->title eq "register") { 327 if (($form->submitted && $form->validate) || $do_register) { 328 my $user_name = $form->field('name'); 329 330 if ($form->submitted eq 'Login') { 331 $session->param("name", $user_name); 332 IkiWiki::cgi_postsignin($cgi, $session); 333 } 334 elsif ($form->submitted eq 'Create Account') { 335 my $email = $form->field('email'); 336 my $password = $form->field('password'); 337 338 if (IkiWiki::userinfo_setall($user_name, { 339 'email' => $email, 340 'regdate' => time})) { 341 setpassword($user_name, $password); 342 $form->field(name => "confirm_password", type => "hidden"); 343 $form->field(name => "email", type => "hidden"); 344 $form->text(gettext("Account creation successful. Now you can Login.")); 345 } 346 else { 347 error(gettext("Error creating account.")); 348 } 349 } 350 elsif ($form->submitted eq 'Reset Password') { 351 my $email=IkiWiki::userinfo_get($user_name, "email"); 352 if (! length $email) { 353 error(gettext("No email address, so cannot email password reset instructions.")); 354 } 355 356 my $token=gentoken($user_name, "resettoken"); 357 358 my $template=template("passwordmail.tmpl"); 359 $template->param( 360 user_name => $user_name, 361 passwordurl => IkiWiki::cgiurl_abs_samescheme( 362 'do' => "reset", 363 'name' => $user_name, 364 'token' => $token, 365 ), 366 wikiurl => $config{url}, 367 wikiname => $config{wikiname}, 368 remote_addr => $session->remote_addr(), 369 ); 370 371 eval q{use Mail::Sendmail}; 372 error($@) if $@; 373 sendmail( 374 To => IkiWiki::userinfo_get($user_name, "email"), 375 From => "$config{wikiname} admin <". 376 (defined $config{adminemail} ? $config{adminemail} : "") 377 .">", 378 Subject => "$config{wikiname} information", 379 Message => $template->output, 380 ) or error(gettext("Failed to send mail")); 381 382 $form->text(gettext("You have been mailed password reset instructions.")); 383 $form->field(name => "name", required => 0); 384 push @$buttons, "Reset Password"; 385 } 386 elsif ($form->submitted eq "Register" || $do_register) { 387 @$buttons="Create Account"; 388 } 389 } 390 elsif ($form->submitted eq "Create Account") { 391 @$buttons="Create Account"; 392 } 393 else { 394 push @$buttons, "Register", "Reset Password"; 395 } 396 } 397 elsif ($form->title eq "preferences") { 398 if ($form->submitted eq "Save Preferences" && $form->validate) { 399 my $user_name=$form->field('name'); 400 my $password=$form->field('password'); 401 if (defined $password && length $password) { 402 setpassword($user_name, $password); 403 } 404 } 405 } 406} 407 408sub sessioncgi ($$) { 409 my $q=shift; 410 my $session=shift; 411 412 if ($q->param('do') eq 'reset') { 413 my $name=$q->param("name"); 414 my $token=$q->param("token"); 415 416 if (! defined $name || ! defined $token || 417 ! length $name || ! length $token) { 418 error(gettext("incorrect password reset url")); 419 } 420 if (! checkpassword($name, $token, "resettoken")) { 421 error(gettext("password reset denied")); 422 } 423 424 $session->param("name", $name); 425 IkiWiki::cgi_prefs($q, $session); 426 exit; 427 } 428 elsif ($q->param('do') eq 'tokenauth') { 429 my $name=$q->param("name"); 430 my $token=$q->param("token"); 431 432 if (! defined $name || ! defined $token || 433 ! length $name || ! length $token) { 434 error(gettext("incorrect url")); 435 } 436 if (! checkpassword($name, $token, "passwordless")) { 437 error(gettext("access denied")); 438 } 439 440 $session->param("name", $name); 441 IkiWiki::cgi_prefs($q, $session); 442 exit; 443 } 444 elsif ($q->param("do") eq "register") { 445 # After registration, need to go somewhere, so show prefs page. 446 $session->param(postsignin => "do=prefs"); 447 # Due to do=register, this will run in registration-only 448 # mode. 449 IkiWiki::cgi_signin($q, $session); 450 exit; 451 } 452} 453 454sub auth ($$) { 455 # While this hook is not currently used, it needs to exist 456 # so ikiwiki knows that the wiki supports logins, and will 457 # enable the Preferences page. 458} 459 4601 461