]> sipb.mit.edu Git - ikiwiki.git/blob - IkiWiki/Plugin/passwordauth.pm
passwordauth: Fix url in password recovery email to be absolute.
[ikiwiki.git] / IkiWiki / Plugin / passwordauth.pm
1 #!/usr/bin/perl
2 # Ikiwiki password authentication.
3 package IkiWiki::Plugin::passwordauth;
4
5 use warnings;
6 use strict;
7 use IkiWiki 3.00;
8
9 sub 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
17 sub 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.
41 sub 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
81 sub 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, "cryptpasswordless", "");
103                 IkiWiki::userinfo_set($user, "passwordless", "");
104         }
105 }
106
107 sub formbuilder_setup (@) {
108         my %params=@_;
109
110         my $form=$params{form};
111         my $session=$params{session};
112         my $cgi=$params{cgi};
113
114         my $do_register=defined $cgi->param("do") && $cgi->param("do") eq "register";
115
116         if ($form->title eq "signin" || $form->title eq "register" || $do_register) {
117                 $form->field(name => "name", required => 0);
118                 $form->field(name => "password", type => "password", required => 0);
119                 
120                 if ($form->submitted eq "Register" || $form->submitted eq "Create Account" || $do_register) {
121                         $form->field(name => "confirm_password", type => "password");
122                         $form->field(name => "account_creation_password", type => "password")
123                                  if (defined $config{account_creation_password} &&
124                                      length $config{account_creation_password});
125                         $form->field(name => "email", size => 50);
126                         $form->title("register");
127                         $form->text("");
128                 
129                         $form->field(name => "confirm_password",
130                                 validate => sub {
131                                         shift eq $form->field("password");
132                                 },
133                         );
134                         $form->field(name => "password",
135                                 validate => sub {
136                                         shift eq $form->field("confirm_password");
137                                 },
138                         );
139                 }
140
141                 if ($form->submitted) {
142                         my $submittype=$form->submitted;
143                         # Set required fields based on how form was submitted.
144                         my %required=(
145                                 "Login" => [qw(name password)],
146                                 "Register" => [],
147                                 "Create Account" => [qw(name password confirm_password email)],
148                                 "Reset Password" => [qw(name)],
149                         );
150                         foreach my $opt (@{$required{$submittype}}) {
151                                 $form->field(name => $opt, required => 1);
152                         }
153         
154                         if ($submittype eq "Create Account") {
155                                 $form->field(
156                                         name => "account_creation_password",
157                                         validate => sub {
158                                                 shift eq $config{account_creation_password};
159                                         },
160                                         required => 1,
161                                 ) if (defined $config{account_creation_password} &&
162                                       length $config{account_creation_password});
163                                 $form->field(
164                                         name => "email",
165                                         validate => "EMAIL",
166                                 );
167                         }
168
169                         # Validate password against name for Login.
170                         if ($submittype eq "Login") {
171                                 $form->field(
172                                         name => "password",
173                                         validate => sub {
174                                                 checkpassword($form->field("name"), shift);
175                                         },
176                                 );
177                         }
178                         elsif ($submittype eq "Register" ||
179                                $submittype eq "Create Account" ||
180                                $submittype eq "Reset Password") {
181                                 $form->field(name => "password", validate => 'VALUE');
182                         }
183                         
184                         # And make sure the entered name exists when logging
185                         # in or sending email, and does not when registering.
186                         if ($submittype eq 'Create Account' ||
187                             $submittype eq 'Register') {
188                                 $form->field(
189                                         name => "name",
190                                         validate => sub {
191                                                 my $name=shift;
192                                                 length $name &&
193                                                 $name=~/$config{wiki_file_regexp}/ &&
194                                                 ! IkiWiki::userinfo_get($name, "regdate");
195                                         },
196                                 );
197                         }
198                         elsif ($submittype eq "Login" ||
199                                $submittype eq "Reset Password") {
200                                 $form->field( 
201                                         name => "name",
202                                         validate => sub {
203                                                 my $name=shift;
204                                                 length $name &&
205                                                 IkiWiki::userinfo_get($name, "regdate");
206                                         },
207                                 );
208                         }
209                 }
210                 else {
211                         # First time settings.
212                         $form->field(name => "name");
213                         if ($session->param("name")) {
214                                 $form->field(name => "name", value => $session->param("name"));
215                         }
216                 }
217         }
218         elsif ($form->title eq "preferences") {
219                 my $user=$session->param("name");
220                 if (! IkiWiki::openiduser($user)) {
221                         $form->field(name => "name", disabled => 1, 
222                                 value => $user, force => 1,
223                                 fieldset => "login");
224                         $form->field(name => "password", type => "password",
225                                 fieldset => "login",
226                                 validate => sub {
227                                         shift eq $form->field("confirm_password");
228                                 });
229                         $form->field(name => "confirm_password", type => "password",
230                                 fieldset => "login",
231                                 validate => sub {
232                                         shift eq $form->field("password");
233                                 });
234                         
235                         my $userpage=IkiWiki::userpage($user);
236                         if (exists $pagesources{$userpage}) {
237                                 $form->text(gettext("Your user page: ").
238                                         htmllink("", "", $userpage,
239                                                 noimageinline => 1));
240                         }
241                         else {
242                                 $form->text("<a href=\"".
243                                         IkiWiki::cgiurl(do => "edit", page => $userpage).
244                                         "\">".gettext("Create your user page")."</a>");
245                         }
246                 }
247         }
248 }
249
250 sub formbuilder (@) {
251         my %params=@_;
252
253         my $form=$params{form};
254         my $session=$params{session};
255         my $cgi=$params{cgi};
256         my $buttons=$params{buttons};
257
258         my $do_register=defined $cgi->param("do") && $cgi->param("do") eq "register";
259
260         if ($form->title eq "signin" || $form->title eq "register") {
261                 if (($form->submitted && $form->validate) || $do_register) {
262                         if ($form->submitted eq 'Login') {
263                                 $session->param("name", $form->field("name"));
264                                 IkiWiki::cgi_postsignin($cgi, $session);
265                         }
266                         elsif ($form->submitted eq 'Create Account') {
267                                 my $user_name=$form->field('name');
268                                 if (IkiWiki::userinfo_setall($user_name, {
269                                         'email' => $form->field('email'),
270                                         'regdate' => time})) {
271                                         setpassword($user_name, $form->field('password'));
272                                         $form->field(name => "confirm_password", type => "hidden");
273                                         $form->field(name => "email", type => "hidden");
274                                         $form->text(gettext("Account creation successful. Now you can Login."));
275                                 }
276                                 else {
277                                         error(gettext("Error creating account."));
278                                 }
279                         }
280                         elsif ($form->submitted eq 'Reset Password') {
281                                 my $user_name=$form->field("name");
282                                 my $email=IkiWiki::userinfo_get($user_name, "email");
283                                 if (! length $email) {
284                                         error(gettext("No email address, so cannot email password reset instructions."));
285                                 }
286                                 
287                                 # Store a token that can be used once
288                                 # to log the user in. This needs to be hard
289                                 # to guess. Generating a cgi session id will
290                                 # make it as hard to guess as any cgi session.
291                                 eval q{use CGI::Session};
292                                 error($@) if $@;
293                                 my $token = CGI::Session->new->id;
294                                 setpassword($user_name, $token, "resettoken");
295                                 
296                                 my $template=template("passwordmail.tmpl");
297                                 $template->param(
298                                         user_name => $user_name,
299                                         passwordurl => IkiWiki::cgiurl_abs(
300                                                 'do' => "reset",
301                                                 'name' => $user_name,
302                                                 'token' => $token,
303                                         ),
304                                         wikiurl => $config{url},
305                                         wikiname => $config{wikiname},
306                                         remote_addr => $session->remote_addr(),
307                                 );
308                                 
309                                 eval q{use Mail::Sendmail};
310                                 error($@) if $@;
311                                 sendmail(
312                                         To => IkiWiki::userinfo_get($user_name, "email"),
313                                         From => "$config{wikiname} admin <".
314                                                 (defined $config{adminemail} ? $config{adminemail} : "")
315                                                 .">",
316                                         Subject => "$config{wikiname} information",
317                                         Message => $template->output,
318                                 ) or error(gettext("Failed to send mail"));
319                                 
320                                 $form->text(gettext("You have been mailed password reset instructions."));
321                                 $form->field(name => "name", required => 0);
322                                 push @$buttons, "Reset Password";
323                         }
324                         elsif ($form->submitted eq "Register" || $do_register) {
325                                 @$buttons="Create Account";
326                         }
327                 }
328                 elsif ($form->submitted eq "Create Account") {
329                         @$buttons="Create Account";
330                 }
331                 else {
332                         push @$buttons, "Register", "Reset Password";
333                 }
334         }
335         elsif ($form->title eq "preferences") {
336                 if ($form->submitted eq "Save Preferences" && $form->validate) {
337                         my $user_name=$form->field('name');
338                         if (defined $form->field("password") && length $form->field("password")) {
339                                 setpassword($user_name, $form->field('password'));
340                         }
341                 }
342         }
343 }
344
345 sub sessioncgi ($$) {
346         my $q=shift;
347         my $session=shift;
348
349         if ($q->param('do') eq 'reset') {
350                 my $name=$q->param("name");
351                 my $token=$q->param("token");
352
353                 if (! defined $name || ! defined $token ||
354                     ! length $name  || ! length $token) {
355                         error(gettext("incorrect password reset url"));
356                 }
357                 if (! checkpassword($name, $token, "resettoken")) {
358                         error(gettext("password reset denied"));
359                 }
360
361                 $session->param("name", $name);
362                 IkiWiki::cgi_prefs($q, $session);
363                 exit;
364         }
365         elsif ($q->param("do") eq "register") {
366                 # After registration, need to go somewhere, so show prefs page.
367                 $session->param(postsignin => "do=prefs");
368                 # Due to do=register, this will run in registration-only
369                 # mode.
370                 IkiWiki::cgi_signin($q, $session);
371                 exit;
372         }
373 }
374
375 sub auth ($$) {
376         # While this hook is not currently used, it needs to exist
377         # so ikiwiki knows that the wiki supports logins, and will
378         # enable the Preferences page.
379 }
380
381 1