make password reset work and verify timecodes
[authserver.git] / index.php
CommitLineData
133aecbe
RK
1<?php
2/* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6// Include the common auth system files (including the OAuth2 Server object).
7require_once(__DIR__.'/authsystem.inc.php');
8
d26d08a1
RK
9$errors = array();
10
133aecbe
RK
11// Start HTML document as a DOM object.
12extract(ExtendedDocument::initHTML5()); // sets $document, $html, $head, $title, $body
13$document->formatOutput = true; // we want a nice output
14
15$style = $head->appendElement('link');
16$style->setAttribute('rel', 'stylesheet');
17$style->setAttribute('href', 'authsystem.css');
d26d08a1 18$head->appendJSFile('authsystem.js');
133aecbe
RK
19$title->appendText('KaiRo.at Authentication Server');
20$h1 = $body->appendElement('h1', 'KaiRo.at Authentication Server');
21
d26d08a1
RK
22$running_on_localhost = preg_match('/^((.+\.)?localhost|127\.0\.0\.\d+)$/', $_SERVER['SERVER_NAME']);
23if (($_SERVER['SERVER_PORT'] != 443) && !$running_on_localhost) {
24 $errors[] = _('You are not accessing this site on a secure connection, so authentication doesn\'t work.');
133aecbe 25}
d26d08a1
RK
26
27$para = $body->appendElement('p', _('This login system does not work without JavaScript. Please activate JavaScript for this site to log in.'));
28$para->setAttribute('id', 'jswarning');
29$para->setAttribute('class', 'warn');
30
31if (!count($errors)) {
32 $session = null;
33 $user = array('id' => 0, 'email' => '');
b19743bc 34 $pagetype = 'default';
d26d08a1
RK
35 $db->exec("SET time_zone='+00:00';"); // Execute directly on PDO object, set session to UTC to make our gmdate() values match correctly.
36 if (strlen(@$_COOKIE['sessionkey'])) {
37 // Fetch the session - or at least try to.
38 $result = $db->prepare('SELECT * FROM `auth_sessions` WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
39 $result->execute(array(':sesskey' => $_COOKIE['sessionkey'], ':expire' => gmdate('Y-m-d H:i:s')));
40 $row = $result->fetch(PDO::FETCH_ASSOC);
41 if ($row) {
42 $session = $row;
43
b19743bc
RK
44 if (array_key_exists('logout', $_GET)) {
45 $result = $db->prepare('UPDATE `auth_sessions` SET `logged_in` = FALSE WHERE `id` = :sessid;');
46 if (!$result->execute(array(':sessid' => $session['id']))) {
47 // XXXlog: Unexpected logout failure!
48 $errors[] = _('The email address is invalid.');
49 }
50 $session['logged_in'] = 0;
51 }
52 elseif (array_key_exists('email', $_POST)) {
d26d08a1
RK
53 if (!preg_match('/^[^@]+@[^@]+\.[^@]+$/', $_POST['email'])) {
54 $errors[] = _('The email address is invalid.');
55 }
89975cb9 56 elseif (verifyTimeCode(@$_POST['tcode'], $session)) {
d26d08a1
RK
57 $result = $db->prepare('SELECT `id`, `pwdhash`, `email`, `status`, `verify_hash` FROM `auth_users` WHERE `email` = :email;');
58 $result->execute(array(':email' => $_POST['email']));
59 $user = $result->fetch(PDO::FETCH_ASSOC);
b19743bc 60 if ($user['id'] && array_key_exists('pwd', $_POST)) {
d26d08a1
RK
61 // existing user, check password
62 if (($user['status'] == 'ok') && password_verify(@$_POST['pwd'], $user['pwdhash'])) {
63 // Check if a newer hashing algorithm is available
64 // or the cost has changed
65 if (password_needs_rehash($user['pwdhash'], PASSWORD_DEFAULT, $pwd_options)) {
66 // If so, create a new hash, and replace the old one
67 $newHash = password_hash($_POST['pwd'], PASSWORD_DEFAULT, $pwd_options);
68 $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash WHERE `id` = :userid;');
b19743bc
RK
69 if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $user['id']))) {
70 // XXXlog: Failed to update user hash!
71 }
d26d08a1
RK
72 }
73
74 // Log user in - update session key for that, see https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines#Login
89975cb9 75 $sesskey = createSessionKey();
d26d08a1 76 setcookie('sessionkey', $sesskey, 0, "", "", !$running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost.
b19743bc
RK
77 // If the session has a user set, create a new one - otherwise take existing session entry.
78 if (intval($session['user'])) {
79 $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`, `user`, `logged_in`) VALUES (:sesskey, :expire, :userid, TRUE);');
80 $result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day'))));
81 // After insert, actually fetch the session row from the DB so we have all values.
82 $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
83 $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s')));
84 $row = $result->fetch(PDO::FETCH_ASSOC);
85 if ($row) {
86 $session = $row;
87 }
88 else {
89975cb9 89 // XXXlog: Unexpected failure to create session!
b19743bc
RK
90 $errors[] = _('The session system is not working. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
91 }
92 }
93 else {
94 $result = $db->prepare('UPDATE `auth_sessions` SET `sesskey` = :sesskey, `user` = :userid, `logged_in` = TRUE, `time_expire` = :expire WHERE `id` = :sessid;');
95 if (!$result->execute(array(':sesskey' => $sesskey, ':userid' => $user['id'], ':expire' => gmdate('Y-m-d H:i:s', strtotime('+1 day')), ':sessid' => $session['id']))) {
96 // XXXlog: Unexpected login failure!
97 $errors[] = _('Login failed unexpectedly. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
98 }
99 }
89975cb9
RK
100 // If a verify_hash if set on a verified user, a password reset had been requested. As a login works right now, cancel that reset request by deleting the hash.
101 if (strlen(@$user['verify_hash'])) {
102 $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = \'\' WHERE `id` = :userid;');
103 if (!$result->execute(array(':userid' => $user['id']))) {
104 // XXXlog: verify_hash could not be emptied!
105 }
106 else {
107 $user['verify_hash'] = '';
108 }
109 }
d26d08a1
RK
110 }
111 else {
112 $errors[] = _('This password is invalid or your email is not verified yet. Did you type them correctly?');
113 }
114 }
115 else {
b19743bc
RK
116 // new user: check password, create user and send verification; existing users: re-send verification or send password change instructions
117 if (array_key_exists('pwd', $_POST)) {
e876642c 118 $errors += checkPasswordConstraints(strval($_POST['pwd']), $_POST['email']);
d26d08a1
RK
119 }
120 if (!count($errors)) {
121 // Put user into the DB
b19743bc
RK
122 if (!$user['id']) {
123 $newHash = password_hash($_POST['pwd'], PASSWORD_DEFAULT, $pwd_options);
89975cb9 124 $vcode = createVerificationCode();
b19743bc
RK
125 $result = $db->prepare('INSERT INTO `auth_users` (`email`, `pwdhash`, `status`, `verify_hash`) VALUES (:email, :pwdhash, \'unverified\', :vcode);');
126 if (!$result->execute(array(':email' => $_POST['email'], ':pwdhash' => $newHash, ':vcode' => $vcode))) {
127 // XXXlog: User insertion failure!
128 $errors[] = _('Could not add user. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
129 }
130 $user = array('id' => $db->lastInsertId(),
131 'email' => $_POST['email'],
132 'pwdhash' => $newHash,
133 'status' => 'unverified',
134 'verify_hash' => $vcode);
135 }
136 if ($user['status'] == 'unverified') {
137 // Send email for verification and show message to point to it.
138 $mail = new email();
139 $mail->setCharset('utf-8');
140 $mail->addHeader('X-KAIRO-AUTH', 'email_verification');
141 $mail->addRecipient($user['email']);
142 $mail->setSender('noreply@auth.kairo.at', _('KaiRo.at Authentication Service'));
143 $mail->setSubject('Email Verification for KaiRo.at Authentication');
144 $mail->addMailText(_('Welcome!')."\n\n");
145 $mail->addMailText(sprintf(_('This email address, %s, has been used for registration on "%s".'),
146 $user['email'], _('KaiRo.at Authentication Service'))."\n\n");
147 $mail->addMailText(_('Please confirm that registration by clicking the following link (or calling it up in your browser):')."\n");
148 $mail->addMailText(($running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME'].strstr($_SERVER['REQUEST_URI'], '?', true)
149 .'?email='.rawurlencode($user['email']).'&verification_code='.rawurlencode($user['verify_hash'])."\n\n");
150 $mail->addMailText(_('With this confirmation, you accept that we handle your data for the purpose of logging you into other websites when you request that.')."\n");
151 $mail->addMailText(_('Those websites will get to know your email address but not your password, which we store securely.')."\n");
152 $mail->addMailText(_('If you do not call this confirmation link within 72 hours, your data will be deleted from our database.')."\n\n");
153 $mail->addMailText(sprintf(_('The %s team'), 'KaiRo.at'));
154 //$mail->setDebugAddress("robert@localhost");
155 $mailsent = $mail->send();
156 if ($mailsent) {
157 $pagetype = 'verification_sent';
158 }
159 else {
160 $errors[] = _('The confirmation email could not be sent to you. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
161 }
162 }
163 else {
89975cb9
RK
164 // Password reset requested with "Password forgotten?" function.
165 $vcode = createVerificationCode();
166 $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;');
167 if (!$result->execute(array(':vcode' => $vcode, ':userid' => $user['id']))) {
168 // XXXlog: User insertion failure!
169 $errors[] = _('Could not initiate reset request. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
170 }
171 else {
172 $resetcode = $vcode.dechex($user['id'] + $session['id']).'_'.createTimeCode($session, null, 60);
173 // Send email with instructions for resetting the password.
174 $mail = new email();
175 $mail->setCharset('utf-8');
176 $mail->addHeader('X-KAIRO-AUTH', 'password_reset');
177 $mail->addRecipient($user['email']);
178 $mail->setSender('noreply@auth.kairo.at', _('KaiRo.at Authentication Service'));
179 $mail->setSubject('How to reset your password for KaiRo.at Authentication');
180 $mail->addMailText(_('Hi,')."\n\n");
181 $mail->addMailText(sprintf(_('A request for setting a new password for this email address, %s, has been submitted on "%s".'),
182 $user['email'], _('KaiRo.at Authentication Service'))."\n\n");
183 $mail->addMailText(_('You can set a new password by clicking the following link (or calling it up in your browser):')."\n");
184 $mail->addMailText(($running_on_localhost?'http':'https').'://'.$_SERVER['SERVER_NAME'].strstr($_SERVER['REQUEST_URI'], '?', true)
185 .'?email='.rawurlencode($user['email']).'&reset_code='.rawurlencode($resetcode)."\n\n");
186 $mail->addMailText(_('If you do not call this confirmation link within 1 hour, this link expires and the existing password is being kept in place.')."\n\n");
187 $mail->addMailText(sprintf(_('The %s team'), 'KaiRo.at'));
188 //$mail->setDebugAddress("robert@localhost");
189 $mailsent = $mail->send();
190 if ($mailsent) {
191 $pagetype = 'resetmail_sent';
192 }
193 else {
194 $errors[] = _('The email with password reset instructions could not be sent to you. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
195 }
196 }
b19743bc 197 }
d26d08a1
RK
198 }
199 }
200 }
89975cb9
RK
201 else {
202 $errors[] = _('The form you used was not valid. Possibly it has expired and you need to initiate the action again.');
203 }
d26d08a1 204 }
b19743bc
RK
205 elseif (array_key_exists('reset', $_GET)) {
206 if ($session['logged_in']) {
e876642c
RK
207 $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `id` = :userid;');
208 $result->execute(array(':userid' => $session['user']));
209 $user = $result->fetch(PDO::FETCH_ASSOC);
210 if (!$user['id']) {
89975cb9 211 // XXXlog: Unexpected failure to fetch user data!
e876642c 212 }
b19743bc
RK
213 $pagetype = 'resetpwd';
214 }
215 else {
216 // Display form for entering email.
217 $pagetype = 'resetstart';
218 }
219 }
220 elseif (array_key_exists('verification_code', $_GET)) {
221 $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `email` = :email AND `status` = \'unverified\' AND `verify_hash` = :vcode;');
222 $result->execute(array(':email' => @$_GET['email'], ':vcode' => $_GET['verification_code']));
223 $user = $result->fetch(PDO::FETCH_ASSOC);
224 if ($user['id']) {
225 $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = \'\', `status` = \'ok\' WHERE `id` = :userid;');
226 if (!$result->execute(array(':userid' => $user['id']))) {
89975cb9 227 // XXXlog: Unexpected failure to save verification!
b19743bc
RK
228 $errors[] = _('Could not save confirmation. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
229 }
230 $pagetype = 'verification_done';
231 }
232 else {
233 $errors[] = _('The confirmation link you called is not valid. Possibly it has expired and you need to try registering again.');
234 }
235 }
89975cb9
RK
236 elseif (array_key_exists('reset_code', $_GET)) {
237 $reset_fail = true;
238 $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `email` = :email');
239 $result->execute(array(':email' => @$_GET['email']));
240 $user = $result->fetch(PDO::FETCH_ASSOC);
241 if ($user['id']) {
242 // Deconstruct reset code and verify it.
243 if (preg_match('/^([0-9a-f]{'.strlen($user['verify_hash']).'})([0-9a-f]+)_(\d+\.\d+)$/', $_GET['reset_code'], $regs)) {
244 $tcode_sessid = hexdec($regs[2]) - $user['id'];
245 $result = $db->prepare('SELECT `id`,`sesskey` FROM `auth_sessions` WHERE `id` = :sessid;');
246 $result->execute(array(':sessid' => $tcode_sessid));
247 $row = $result->fetch(PDO::FETCH_ASSOC);
248 if ($row) {
249 $tcode_session = $row;
250 if (($regs[1] == $user['verify_hash']) &&
251 verifyTimeCode($regs[3], $session, 60)) {
252 // Set a new verify_hash for the actual password reset.
253 $user['verify_hash'] = createVerificationCode();
254 $result = $db->prepare('UPDATE `auth_users` SET `verify_hash` = :vcode WHERE `id` = :userid;');
255 if (!$result->execute(array(':vcode' => $user['verify_hash'], ':userid' => $user['id']))) {
256 // XXXlog: Unexpected failure to reset verify_hash!
257 }
258 $result = $db->prepare('UPDATE `auth_sessions` SET `user` = :userid WHERE `id` = :sessid;');
259 if (!$result->execute(array(':userid' => $user['id'], ':sessid' => $session['id']))) {
260 // XXXlog: Unexpected failure to update session!
261 }
262 $pagetype = 'resetpwd';
263 $reset_fail = false;
264 }
265 }
266 }
267 }
268 if ($reset_fail) {
269 $errors[] = _('The password reset link you called is not valid. Possibly it has expired and you need to call the "Password forgotten?" function again.');
270 }
271 }
b19743bc 272 elseif (intval($session['user'])) {
89975cb9 273 $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `id` = :userid;');
b19743bc
RK
274 $result->execute(array(':userid' => $session['user']));
275 $user = $result->fetch(PDO::FETCH_ASSOC);
276 if (!$user['id']) {
89975cb9 277 // XXXlog: Unexpected failure to fetch user data!
b19743bc 278 }
e876642c
RK
279 // Password reset requested.
280 if (array_key_exists('pwd', $_POST) && array_key_exists('reset', $_POST) && array_key_exists('tcode', $_POST)) {
89975cb9
RK
281 // If not logged in, a password reset needs to have the proper vcode set.
282 if (!$session['logged_in'] && (!strlen(@$_POST['vcode']) || ($_POST['vcode'] != $user['verify_hash']))) {
283 $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
284 }
285 // If not logged in, a password reset also needs to have the proper email set.
286 if (!$session['logged_in'] && !count($errors) && (@$_POST['email_hidden'] != $user['email'])) {
287 $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
288 }
289 // Check validity of time code.
290 if (!count($errors) && !verifyTimeCode($_POST['tcode'], $session)) {
291 $errors[] = _('Password reset failed. The reset form you used was not valid. Possibly it has expired and you need to initiate the password reset again.');
292 }
e876642c
RK
293 $errors += checkPasswordConstraints(strval($_POST['pwd']), $user['email']);
294 if (!count($errors)) {
295 $newHash = password_hash($_POST['pwd'], PASSWORD_DEFAULT, $pwd_options);
89975cb9 296 $result = $db->prepare('UPDATE `auth_users` SET `pwdhash` = :pwdhash, `verify_hash` = \'\' WHERE `id` = :userid;');
e876642c
RK
297 if (!$result->execute(array(':pwdhash' => $newHash, ':userid' => $session['user']))) {
298 // XXXlog: Password reset failure!
299 $errors[] = _('Password reset failed. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
300 }
301 else {
89975cb9 302 $pagetype = 'reset_done';
e876642c
RK
303 }
304 }
305 }
b19743bc 306 }
d26d08a1
RK
307 }
308 }
309 if (is_null($session)) {
310 // Create new session and set cookie.
89975cb9 311 $sesskey = createSessionKey();
d26d08a1
RK
312 setcookie('sessionkey', $sesskey, 0, "", "", !$running_on_localhost, true); // Last two params are secure and httponly, secure is not set on localhost.
313 $result = $db->prepare('INSERT INTO `auth_sessions` (`sesskey`, `time_expire`) VALUES (:sesskey, :expire);');
314 $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s', strtotime('+5 minutes'))));
315 // After insert, actually fetch the session row from the DB so we have all values.
316 $result = $db->prepare('SELECT * FROM auth_sessions WHERE `sesskey` = :sesskey AND `time_expire` > :expire;');
317 $result->execute(array(':sesskey' => $sesskey, ':expire' => gmdate('Y-m-d H:i:s')));
318 $row = $result->fetch(PDO::FETCH_ASSOC);
319 if ($row) {
320 $session = $row;
321 }
b19743bc 322 else {
89975cb9 323 // XXXlog: Unexpected failure to create session!
b19743bc
RK
324 $errors[] = _('The session system is not working. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.');
325 }
d26d08a1
RK
326 }
327}
328
329if (!count($errors)) {
b19743bc
RK
330 if ($pagetype == 'verification_sent') {
331 $para = $body->appendElement('p', sprintf(_('An email for confirmation has been sent to %s. Please follow the link provided there to complete the process.'), $user['email']));
332 $para->setAttribute('class', 'verifyinfo pending');
333 }
89975cb9
RK
334 elseif ($pagetype == 'resetmail_sent') {
335 $para = $body->appendElement('p',
336 _('An email has been sent to the requested account with further information. If you do not receive an email then please confirm you have entered the same email address used during account registration.'));
337 $para->setAttribute('class', 'resetinfo pending');
338 }
b19743bc
RK
339 elseif ($pagetype == 'resetstart') {
340 $para = $body->appendElement('p', _('If you forgot your password or didn\'t receive the registration confirmation, please enter your email here.'));
341 $para->setAttribute('class', '');
342 $form = $body->appendForm('?reset', 'POST', 'resetform');
343 $form->setAttribute('id', 'loginform');
344 $form->setAttribute('class', 'loginarea hidden');
345 $ulist = $form->appendElement('ul');
346 $ulist->setAttribute('class', 'flat login');
347 $litem = $ulist->appendElement('li');
348 $inptxt = $litem->appendInputEmail('email', 30, 20, 'login_email');
349 $inptxt->setAttribute('autocomplete', 'email');
350 $inptxt->setAttribute('required', '');
351 $inptxt->setAttribute('placeholder', _('Email'));
352 $litem = $ulist->appendElement('li');
e876642c 353 $litem->appendInputHidden('tcode', createTimeCode($session));
b19743bc
RK
354 $submit = $litem->appendInputSubmit(_('Send instructions to email'));
355 }
356 elseif ($pagetype == 'resetpwd') {
89975cb9 357 $para = $body->appendElement('p', sprintf(_('You can set a new password for %s here.'), $user['email']));
b19743bc 358 $para->setAttribute('class', '');
e876642c 359 $form = $body->appendForm('?', 'POST', 'newpwdform');
b19743bc
RK
360 $form->setAttribute('id', 'loginform');
361 $form->setAttribute('class', 'loginarea hidden');
362 $ulist = $form->appendElement('ul');
363 $ulist->setAttribute('class', 'flat login');
364 $litem = $ulist->appendElement('li');
e876642c
RK
365 $litem->setAttribute('class', 'donotshow');
366 $inptxt = $litem->appendInputEmail('email_hidden', 30, 20, 'login_email', $user['email']);
367 $inptxt->setAttribute('autocomplete', 'email');
368 $inptxt->setAttribute('placeholder', _('Email'));
369 $litem = $ulist->appendElement('li');
b19743bc
RK
370 $inptxt = $litem->appendInputPassword('pwd', 20, 20, 'login_pwd', '');
371 $inptxt->setAttribute('required', '');
372 $inptxt->setAttribute('placeholder', _('Password'));
373 $inptxt->setAttribute('class', 'login');
374 $litem = $ulist->appendElement('li');
e876642c
RK
375 $litem->appendInputHidden('reset', '');
376 $litem->appendInputHidden('tcode', createTimeCode($session));
89975cb9
RK
377 if (!$session['logged_in'] && strlen(@$user['verify_hash'])) {
378 $litem->appendInputHidden('vcode', $user['verify_hash']);
379 }
b19743bc
RK
380 $submit = $litem->appendInputSubmit(_('Save password'));
381 }
382 elseif ($session['logged_in']) {
e876642c
RK
383 if ($pagetype == 'reset_done') {
384 $para = $body->appendElement('p', _('Your password has successfully been reset.'));
385 $para->setAttribute('class', 'resetinfo done');
386 }
d26d08a1
RK
387 $div = $body->appendElement('div', $user['email']);
388 $div->setAttribute('class', 'loginheader');
389 $div = $body->appendElement('div');
390 $div->setAttribute('class', 'loginlinks');
b19743bc
RK
391 $ulist = $div->appendElement('ul');
392 $ulist->setAttribute('class', 'flat');
393 $litem = $ulist->appendElement('li');
394 $link = $litem->appendLink('?logout', _('Log out'));
395 $litem = $ulist->appendElement('li');
396 $litem->appendLink('?reset', _('Set new password'));
d26d08a1
RK
397 }
398 else { // not logged in
b19743bc
RK
399 if ($pagetype == 'verification_done') {
400 $para = $body->appendElement('p', _('Hooray! Your email was successfully confirmed! You can log in now.'));
401 $para->setAttribute('class', 'verifyinfo done');
402 }
e876642c
RK
403 elseif ($pagetype == 'reset_done') {
404 $para = $body->appendElement('p', _('Your password has successfully been reset. You can log in now with the new password.'));
405 $para->setAttribute('class', 'resetinfo done');
406 }
b19743bc 407 $form = $body->appendForm('?', 'POST', 'loginform');
d26d08a1
RK
408 $form->setAttribute('id', 'loginform');
409 $form->setAttribute('class', 'loginarea hidden');
410 $ulist = $form->appendElement('ul');
411 $ulist->setAttribute('class', 'flat login');
412 $litem = $ulist->appendElement('li');
413 $inptxt = $litem->appendInputEmail('email', 30, 20, 'login_email', (intval($user['id'])?$user['email']:''));
414 $inptxt->setAttribute('autocomplete', 'email');
415 $inptxt->setAttribute('required', '');
416 $inptxt->setAttribute('placeholder', _('Email'));
417 $inptxt->setAttribute('class', 'login');
418 $litem = $ulist->appendElement('li');
419 $inptxt = $litem->appendInputPassword('pwd', 20, 20, 'login_pwd', '');
b19743bc 420 $inptxt->setAttribute('required', '');
d26d08a1
RK
421 $inptxt->setAttribute('placeholder', _('Password'));
422 $inptxt->setAttribute('class', 'login');
423 $litem = $ulist->appendElement('li');
b19743bc
RK
424 $litem->appendLink('?reset', _('Forgot password?'));
425 $litem = $ulist->appendElement('li');
d26d08a1
RK
426 $cbox = $litem->appendInputCheckbox('remember', 'login_remember', 'true', false);
427 $cbox->setAttribute('class', 'logincheck');
428 $label = $litem->appendLabel('login_remember', _('Remember me'));
429 $label->setAttribute('id', 'rememprompt');
430 $label->setAttribute('class', 'loginprompt');
431 $litem = $ulist->appendElement('li');
e876642c
RK
432 $litem->appendInputHidden('tcode', createTimeCode($session));
433 $submit = $litem->appendInputSubmit(_('Log in / Register'));
d26d08a1
RK
434 $submit->setAttribute('class', 'loginbutton');
435 }
436}
437
438if (count($errors)) {
439 $body->appendElement('p', ((count($errors) <= 1)
440 ?_('The following error was detected')
441 :_('The following errors were detected')).':');
442 $list = $body->appendElement('ul');
443 $list->setAttribute('class', 'flat warn');
444 foreach ($errors as $msg) {
445 $item = $list->appendElement('li', $msg);
446 }
b19743bc 447 $body->appendButton(_('Back'), 'history.back();');
133aecbe
RK
448}
449
450// Send HTML to client.
451print($document->saveHTML());
e876642c
RK
452
453// ********** helper functions **********
454
455function checkPasswordConstraints($new_password, $user_email) {
456 $errors = array();
457 if ($new_password != trim($new_password)) {
458 $errors[] = _('Password must not start or end with a whitespace character like a space.');
459 }
460 if (strlen($new_password) < 8) { $errors[] = sprintf(_('Password too short (min. %s characters).'), 8); }
461 if (strlen($new_password) > 70) { $errors[] = sprintf(_('Password too long (max. %s characters).'), 70); }
462 if ((strtolower($new_password) == strtolower($user_email)) ||
463 in_array(strtolower($new_password), preg_split("/[@\.]+/", strtolower($user_email)))) {
464 $errors[] = _('The passwort can not be equal to your email or any part of it.');
465 }
466 if ((strlen($new_password) < 15) && (preg_match('/^[a-zA-Z]+$/', $new_password))) {
467 $errors[] = sprintf(_('Your password must use characters other than normal letters or contain least %s characters.'), 15);
468 }
469 if (preg_match('/^\d+$/', $new_password)) {
470 $errors[] = sprintf(_('Your password cannot consist only of numbers.'), 15);
471 }
472 if (strlen(count_chars($new_password, 3)) < 5) {
473 $errors[] = sprintf(_('Password does have to contain at least %s different characters.'), 5);
474 }
475 return $errors;
476}
477
89975cb9
RK
478function createSessionKey() {
479 return bin2hex(openssl_random_pseudo_bytes(512 / 8)); // Get 512 bits of randomness (128 byte hex string).
480}
481
482function createVerificationCode() {
483 return bin2hex(openssl_random_pseudo_bytes(512 / 8)); // Get 512 bits of randomness (128 byte hex string).
484}
485
486function createTimeCode($session, $offset = null, $validity_minutes = 10) {
e876642c 487 // Matches TOTP algorithms, see https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
89975cb9
RK
488 $valid_seconds = intval($validity_minutes) * 60;
489 if ($valid_seconds < 60) { $valid_seconds = 60; }
490 $code_digits = 8;
e876642c 491 $time = time();
89975cb9 492 $rest = is_null($offset)?($time % $valid_seconds):intval($offset); // T0, will be sent as part of code to make it valid for the full duration.
e876642c 493 $counter = floor(($time - $rest) / $valid_seconds);
89975cb9 494 $hmac = mhash(MHASH_SHA1, $counter, $session['id'].$session['sesskey']);
e876642c
RK
495 $offset = hexdec(substr(bin2hex(substr($hmac, -1)), -1)); // Get the last 4 bits as a number.
496 $totp = hexdec(bin2hex(substr($hmac, $offset, 4))) & 0x7FFFFFFF; // Take 4 bytes at the offset, discard highest bit.
497 $totp_value = sprintf('%0'.$code_digits.'d', substr($totp, -$code_digits));
498 return $rest.'.'.$totp_value;
499}
500
89975cb9
RK
501function verifyTimeCode($timecode_to_verify, $session, $validity_minutes = 10) {
502 if (preg_match('/^(\d+)\.\d+$/', $timecode_to_verify, $regs)) {
503 return ($timecode_to_verify === createTimeCode($session, $regs[1], $validity_minutes));
504 }
505 return false;
506}
507
133aecbe 508?>