po(refreshpot): define the input charset before read()'ing
[ikiwiki] / 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 2.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 } # }}}
15
16 sub getsetup () { #{{{
17         return
18                 plugin => {
19                         safe => 1,
20                         rebuild => 0,
21                 },
22                 account_creation_password => {
23                         type => "string",
24                         example => "s3cr1t",
25                         description => "a password that must be entered when signing up for an account",
26                         safe => 1,
27                         rebuild => 0,
28                 },
29                 password_cost => {
30                         type => "integer",
31                         example => 8,
32                         description => "cost of generating a password using Authen::Passphrase::BlowfishCrypt",
33                         safe => 1,
34                         rebuild => 0,
35                 },
36 } #}}}
37
38 # Checks if a string matches a user's password, and returns true or false.
39 sub checkpassword ($$;$) { #{{{
40         my $user=shift;
41         my $password=shift;
42         my $field=shift || "password";
43
44         # It's very important that the user not be allowed to log in with
45         # an empty password!
46         if (! length $password) {
47                 return 0;
48         }
49
50         my $userinfo=IkiWiki::userinfo_retrieve();
51         if (! length $user || ! defined $userinfo ||
52             ! exists $userinfo->{$user} || ! ref $userinfo->{$user}) {
53                 return 0;
54         }
55
56         my $ret=0;
57         if (exists $userinfo->{$user}->{"crypt".$field}) {
58                 eval q{use Authen::Passphrase};
59                 error $@ if $@;
60                 my $p = Authen::Passphrase->from_crypt($userinfo->{$user}->{"crypt".$field});
61                 $ret=$p->match($password);
62         }
63         elsif (exists $userinfo->{$user}->{$field}) {
64                 $ret=$password eq $userinfo->{$user}->{$field};
65         }
66
67         if ($ret &&
68             (exists $userinfo->{$user}->{resettoken} ||
69              exists $userinfo->{$user}->{cryptresettoken})) {
70                 # Clear reset token since the user has successfully logged in.
71                 delete $userinfo->{$user}->{resettoken};
72                 delete $userinfo->{$user}->{cryptresettoken};
73                 IkiWiki::userinfo_store($userinfo);
74         }
75
76         return $ret;
77 } #}}}
78
79 sub setpassword ($$;$) { #{{{
80         my $user=shift;
81         my $password=shift;
82         my $field=shift || "password";
83
84         eval q{use Authen::Passphrase::BlowfishCrypt};
85         if (! $@) {
86                 my $p = Authen::Passphrase::BlowfishCrypt->new(
87                         cost => $config{password_cost} || 8,
88                         salt_random => 1,
89                         passphrase => $password,
90                 );
91                 IkiWiki::userinfo_set($user, "crypt$field", $p->as_crypt);
92                 IkiWiki::userinfo_set($user, $field, "");
93         }
94         else {
95                 IkiWiki::userinfo_set($user, $field, $password);
96         }
97 } #}}}
98
99 sub formbuilder_setup (@) { #{{{
100         my %params=@_;
101
102         my $form=$params{form};
103         my $session=$params{session};
104         my $cgi=$params{cgi};
105
106         if ($form->title eq "signin" || $form->title eq "register") {
107                 $form->field(name => "name", required => 0);
108                 $form->field(name => "password", type => "password", required => 0);
109                 
110                 if ($form->submitted eq "Register" || $form->submitted eq "Create Account") {
111                         $form->field(name => "confirm_password", type => "password");
112                         $form->field(name => "account_creation_password", type => "password")
113                                  if (defined $config{account_creation_password} &&
114                                      length $config{account_creation_password});
115                         $form->field(name => "email", size => 50);
116                         $form->title("register");
117                         $form->text("");
118                 
119                         $form->field(name => "confirm_password",
120                                 validate => sub {
121                                         shift eq $form->field("password");
122                                 },
123                         );
124                         $form->field(name => "password",
125                                 validate => sub {
126                                         shift eq $form->field("confirm_password");
127                                 },
128                         );
129                 }
130
131                 if ($form->submitted) {
132                         my $submittype=$form->submitted;
133                         # Set required fields based on how form was submitted.
134                         my %required=(
135                                 "Login" => [qw(name password)],
136                                 "Register" => [],
137                                 "Create Account" => [qw(name password confirm_password email)],
138                                 "Reset Password" => [qw(name)],
139                         );
140                         foreach my $opt (@{$required{$submittype}}) {
141                                 $form->field(name => $opt, required => 1);
142                         }
143         
144                         if ($submittype eq "Create Account") {
145                                 $form->field(
146                                         name => "account_creation_password",
147                                         validate => sub {
148                                                 shift eq $config{account_creation_password};
149                                         },
150                                         required => 1,
151                                 ) if (defined $config{account_creation_password} &&
152                                       length $config{account_creation_password});
153                                 $form->field(
154                                         name => "email",
155                                         validate => "EMAIL",
156                                 );
157                         }
158
159                         # Validate password against name for Login.
160                         if ($submittype eq "Login") {
161                                 $form->field(
162                                         name => "password",
163                                         validate => sub {
164                                                 checkpassword($form->field("name"), shift);
165                                         },
166                                 );
167                         }
168                         elsif ($submittype eq "Register" ||
169                                $submittype eq "Create Account" ||
170                                $submittype eq "Reset Password") {
171                                 $form->field(name => "password", validate => 'VALUE');
172                         }
173                         
174                         # And make sure the entered name exists when logging
175                         # in or sending email, and does not when registering.
176                         if ($submittype eq 'Create Account' ||
177                             $submittype eq 'Register') {
178                                 $form->field(
179                                         name => "name",
180                                         validate => sub {
181                                                 my $name=shift;
182                                                 length $name &&
183                                                 $name=~/$config{wiki_file_regexp}/ &&
184                                                 ! IkiWiki::userinfo_get($name, "regdate");
185                                         },
186                                 );
187                         }
188                         elsif ($submittype eq "Login" ||
189                                $submittype eq "Reset Password") {
190                                 $form->field( 
191                                         name => "name",
192                                         validate => sub {
193                                                 my $name=shift;
194                                                 length $name &&
195                                                 IkiWiki::userinfo_get($name, "regdate");
196                                         },
197                                 );
198                         }
199                 }
200                 else {
201                         # First time settings.
202                         $form->field(name => "name");
203                         if ($session->param("name")) {
204                                 $form->field(name => "name", value => $session->param("name"));
205                         }
206                 }
207         }
208         elsif ($form->title eq "preferences") {
209                 $form->field(name => "name", disabled => 1, 
210                         value => $session->param("name"), force => 1,
211                         fieldset => "login");
212                 $form->field(name => "password", type => "password",
213                         fieldset => "login",
214                         validate => sub {
215                                 shift eq $form->field("confirm_password");
216                         }),
217                 $form->field(name => "confirm_password", type => "password",
218                         fieldset => "login",
219                         validate => sub {
220                                 shift eq $form->field("password");
221                         }),
222         }
223 }
224
225 sub formbuilder (@) { #{{{
226         my %params=@_;
227
228         my $form=$params{form};
229         my $session=$params{session};
230         my $cgi=$params{cgi};
231         my $buttons=$params{buttons};
232
233         if ($form->title eq "signin" || $form->title eq "register") {
234                 if ($form->submitted && $form->validate) {
235                         if ($form->submitted eq 'Login') {
236                                 $session->param("name", $form->field("name"));
237                                 IkiWiki::cgi_postsignin($cgi, $session);
238                         }
239                         elsif ($form->submitted eq 'Create Account') {
240                                 my $user_name=$form->field('name');
241                                 if (IkiWiki::userinfo_setall($user_name, {
242                                         'email' => $form->field('email'),
243                                         'regdate' => time})) {
244                                         setpassword($user_name, $form->field('password'));
245                                         $form->field(name => "confirm_password", type => "hidden");
246                                         $form->field(name => "email", type => "hidden");
247                                         $form->text(gettext("Account creation successful. Now you can Login."));
248                                 }
249                                 else {
250                                         error(gettext("Error creating account."));
251                                 }
252                         }
253                         elsif ($form->submitted eq 'Reset Password') {
254                                 my $user_name=$form->field("name");
255                                 my $email=IkiWiki::userinfo_get($user_name, "email");
256                                 if (! length $email) {
257                                         error(gettext("No email address, so cannot email password reset instructions."));
258                                 }
259                                 
260                                 # Store a token that can be used once
261                                 # to log the user in. This needs to be hard
262                                 # to guess. Generating a cgi session id will
263                                 # make it as hard to guess as any cgi session.
264                                 eval q{use CGI::Session};
265                                 error($@) if $@;
266                                 my $token = CGI::Session->new->id;
267                                 setpassword($user_name, $token, "resettoken");
268                                 
269                                 my $template=template("passwordmail.tmpl");
270                                 $template->param(
271                                         user_name => $user_name,
272                                         passwordurl => IkiWiki::cgiurl(
273                                                 'do' => "reset",
274                                                 'name' => $user_name,
275                                                 'token' => $token,
276                                         ),
277                                         wikiurl => $config{url},
278                                         wikiname => $config{wikiname},
279                                         REMOTE_ADDR => $ENV{REMOTE_ADDR},
280                                 );
281                                 
282                                 eval q{use Mail::Sendmail};
283                                 error($@) if $@;
284                                 sendmail(
285                                         To => IkiWiki::userinfo_get($user_name, "email"),
286                                         From => "$config{wikiname} admin <".
287                                                 (defined $config{adminemail} ? $config{adminemail} : "")
288                                                 .">",
289                                         Subject => "$config{wikiname} information",
290                                         Message => $template->output,
291                                 ) or error(gettext("Failed to send mail"));
292                                 
293                                 $form->text(gettext("You have been mailed password reset instructions."));
294                                 $form->field(name => "name", required => 0);
295                                 push @$buttons, "Reset Password";
296                         }
297                         elsif ($form->submitted eq "Register") {
298                                 @$buttons="Create Account";
299                         }
300                 }
301                 elsif ($form->submitted eq "Create Account") {
302                         @$buttons="Create Account";
303                 }
304                 else {
305                         push @$buttons, "Register", "Reset Password";
306                 }
307         }
308         elsif ($form->title eq "preferences") {
309                 if ($form->submitted eq "Save Preferences" && $form->validate) {
310                         my $user_name=$form->field('name');
311                         if ($form->field("password") && length $form->field("password")) {
312                                 setpassword($user_name, $form->field('password'));
313                         }
314                 }
315         }
316 } #}}}
317
318 sub sessioncgi ($$) { #{{{
319         my $q=shift;
320         my $session=shift;
321
322         if ($q->param('do') eq 'reset') {
323                 my $name=$q->param("name");
324                 my $token=$q->param("token");
325
326                 if (! defined $name || ! defined $token ||
327                     ! length $name  || ! length $token) {
328                         error(gettext("incorrect password reset url"));
329                 }
330                 if (! checkpassword($name, $token, "resettoken")) {
331                         error(gettext("password reset denied"));
332                 }
333
334                 $session->param("name", $name);
335                 IkiWiki::cgi_prefs($q, $session);
336                 exit;
337         }
338 } #}}}
339
340 1