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