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