blob: d10b5c04f5884a26cdd1136c0eabf8c33f412a71 [file] [log] [blame]
Nico Huberee52fbc2023-06-24 11:52:57 +00001<?php
2/*
3 * SimpleID
4 *
5 * Copyright (C) Kelvin Mo 2007-8
6 *
7 * Includes code Drupal OpenID module (http://drupal.org/project/openid)
8 * Rowan Kerr <rowan@standardinteractive.com>
9 * James Walker <james@bryght.com>
10 *
11 * Copyright (C) Rowan Kerr and James Walker
12 *
13 * This program is free software; you can redistribute it and/or
14 * modify it under the terms of the GNU General Public
15 * License as published by the Free Software Foundation; either
16 * version 2 of the License, or (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 * General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public
24 * License along with this program; if not, write to the Free
25 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
26 *
27 * $Id$
28 */
29
30/**
31 * User functions.
32 *
33 * @package simpleid
34 * @filesource
35 */
36
37/**
38 * The time the nonce used in the login process will last.
39 */
40define('SIMPLEID_LOGIN_NONCE_EXPIRES_IN', 3600);
41
42/**
43 * The time (in seconds) the auto login cookie will last. This is currently
44 * set as 2 weeks.
45 */
46define('SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN', 1209600);
47
48/**
49 * This variable holds data on the currently logged-in user. If the user is
50 * not logged in, this variable is NULL.
51 *
52 * @global array $user
53 */
54$user = NULL;
55
56/**
57 * Initialises the user system. Loads data for the currently logged-in user,
58 * if any.
59 *
60 * @param string $q the SimpleID command, if any
61 */
62function user_init($q = NULL) {
63 global $user;
64 global $xtpl;
65
66 log_debug('user_init');
67
68 $user = NULL;
69
70 // session_name() has to be called before session_set_cookie_params()
71 session_name(simpleid_cookie_name('sess'));
72
73 // Note the last parameter (httponly) requires PHP 5.2
74 session_set_cookie_params(0, get_base_path(), ini_get('session.cookie_domain'), false, true);
75 session_start();
76
77 if (isset($_SESSION['user']) && (cache_get('user', $_SESSION['user']) == session_id())) {
78 $user = user_load($_SESSION['user']);
79
80 // If user has just been actively been authenticated in the previous request, then we
81 // make it as actively authenticated in this request.
82 if (isset($_SESSION['user_auth_active']) && $_SESSION['user_auth_active']) {
83 $user['auth_active'] = true;
84 unset($_SESSION['user_auth_active']);
85 }
86 } else {
87 if (($q == 'login') || ($q == 'logout')) return;
88 user_auto_login();
89 }
90}
91
92/**
93 * Attempts to automatically login using credentials presented by the user agent.
94 *
95 * The user agent may present various credentials as part of its request. These
96 * may include cookies and SSL client certificates. This function calls the
97 * {@link hook_user_auto_login()} hook of enabled extensions to see if any
98 * of these credentials can be used to automatically login a user.
99 */
100function user_auto_login() {
101 global $simpleid_extensions;
102
103 $extensions = $simpleid_extensions;
104
105 if (!in_array('user_cookieauth', $extensions)) $extensions[] = 'user_cookieauth';
106
107 foreach ($extensions as $extension) {
108 $test_user = extension_invoke($extension, 'user_auto_login');
109 if ($test_user != NULL) {
110 _user_login($test_user);
111 }
112 }
113}
114
115/**
116 * Loads user data for a specified user name.
117 *
118 * @param string $uid the name of the user to load
119 * @return mixed data for the specified user, or NULL if the user name does not
120 * exist
121 * @see user_load_from_identity()
122 */
123function user_load($uid) {
124 if (store_user_exists($uid)) {
125 $user = store_user_load($uid);
126 $user["uid"] = $uid;
127
128 if (isset($user["identity"])) {
129 $user["local_identity"] = true;
130 } else {
131 $user["identity"] = simpleid_url('user/' . rawurlencode($uid));
132 $user["local_identity"] = false;
133 }
134
135 return $user;
136 } else {
137 return NULL;
138 }
139}
140
141/**
142 * Loads user data for a specified OpenID Identity URI.
143 *
144 * @param string $identity the Identity URI of the user to load
145 * @return mixed data for the specified user, or NULL if the user name does not
146 * exist
147 * @see user_load()
148 */
149function user_load_from_identity($identity) {
150 $uid = store_get_uid($identity);
151 if ($uid !== NULL) return user_load($uid);
152
153 return NULL;
154}
155
156/**
157 * Stores user data for a specified user name.
158 *
159 * @param array $user the user to save
160 */
161function user_save($user) {
162 $uid = $user['uid'];
163 store_user_save($uid, $user, array('uid', 'identity', 'pass'));
164}
165
166/**
167 * Attempts to log in a user, using the user name and password specified in the
168 * HTTP request.
169 */
170function user_login() {
171 global $user, $GETPOST;
172
173 // If the user is already logged in, return
174 if (isset($user['uid'])) openid_indirect_response(simpleid_url(), '');
175
176 // Require HTTPS or return an error
177 check_https('error', true);
178
179 $destination = (isset($GETPOST['destination'])) ? $GETPOST['destination'] : '';
180 $state = (isset($GETPOST['s'])) ? $GETPOST['s'] : '';
181 $fixed_uid = (isset($_POST['fixed_uid'])) ? $_POST['name'] : NULL;
182 $mode = $_POST['mode'];
183
184 $query = ($state) ? 's=' . rawurlencode($state) : '';
185
186 if (isset($_POST['op']) && $_POST['op'] == t('Cancel')) {
187 global $version;
188
189 $request = unpickle($state);
190 $version = openid_get_version($request);
191
192 if (isset($request['openid.return_to'])) {
193 $return_to = $request['openid.return_to'];
194 $response = simpleid_checkid_error(FALSE);
195 simpleid_assertion_response($response, $return_to);
196 } else {
197 indirect_fatal_error(t('Login cancelled without a proper OpenID request.'));
198 }
199 return;
200 }
201
202 if (!isset($_POST['mode']) || !in_array($_POST['mode'], array('credentials', 'otp'))) {
203 set_message(t('SimpleID detected a potential security attack on your log in. Please log in again.'));
204 user_login_form($destination, $state, $fixed_uid);
205 return;
206 }
207
208 if (!isset($_POST['nonce'])) {
209 if (isset($_POST['destination'])) {
210 // User came from a log in form.
211 set_message(t('You seem to be attempting to log in from another web page. You must use this page to log in.'));
212 }
213 user_login_form($destination, $state, $fixed_uid, $mode);
214 return;
215 }
216
217 $time = strtotime(substr($_POST['nonce'], 0, 20));
218 // Some old versions of PHP does not recognise the T in the ISO 8601 date. We may need to convert the T to a space
219 if (($time == -1) || ($time === FALSE)) $time = strtotime(strtr(substr($_POST['nonce'], 0, 20), 'T', ' '));
220 $nonce = cache_get('user-nonce', $_POST['nonce']);
221
222 if (!$nonce) {
223 log_warn('Login attempt: Nonce ' . $_POST['nonce'] . ' not issued or is being reused.');
224 set_message(t('SimpleID detected a potential security attack on your log in. Please log in again.'));
225 user_login_form($destination, $state, $fixed_uid, $mode);
226 return;
227 } elseif ($time < time() - SIMPLEID_LOGIN_NONCE_EXPIRES_IN) {
228 log_notice('Login attempt: Nonce ' . $_POST['nonce'] . ' expired.');
229 set_message(t('The log in page has expired. Please log in again.'));
230 user_login_form($destination, $state, $fixed_uid, $mode);
231 return;
232 } elseif ($nonce['mode'] != $mode) {
233 log_warn('Login attempt: Mode saved with nonce ' . $_POST['nonce'] . ' (' . $nonce['mode'] . ') does not match ' . $mode);
234 set_message(t('SimpleID detected a potential security attack on your log in. Please log in again.'));
235 user_login_form($destination, $state, $fixed_uid, $mode);
236 } else {
237 cache_delete('user-nonce', $_POST['nonce']);
238 }
239
240 switch ($mode) {
241 case 'credentials':
242 if (!isset($_POST['name'])) $_POST['name'] = '';
243 if (!isset($_POST['pass'])) $_POST['pass'] = '';
244
245 if (($_POST['name'] == '') || ($_POST['pass'] == '')) {
246 if (isset($_POST['destination'])) {
247 // User came from a log in form.
248 set_message(t('You need to supply the user name and the password in order to log in.'));
249 }
250 if (isset($_POST['nonce'])) cache_delete('user-nonce', $_POST['nonce']);
251 user_login_form($destination, $state, $fixed_uid);
252 return;
253 }
254
255 if (user_verify_credentials($_POST['name'], $_POST) === false) {
256 set_message(t('The user name or password is not correct.'));
257 user_login_form($destination, $state, $fixed_uid);
258 return;
259 }
260
261 $test_user = user_load($_POST['name']);
262 if (isset($test_user['otp']) && ($test_user['otp']['type'] != 'recovery')) {
263 log_info('One time password required');
264 user_login_form($destination, $state, $test_user['uid'], 'otp');
265 return;
266 }
267 break;
268 case 'otp':
269 if (!isset($_POST['otp']) || ($_POST['otp'] == '')) {
270 set_message(t('You need to enter the verification code in order to log in.'));
271 if (isset($_POST['nonce'])) cache_delete('user-nonce', $_POST['nonce']);
272 user_login_form($destination, $state, $nonce['uid'], 'otp');
273 return;
274 }
275
276 $test_user = user_load($nonce['uid']);
277
278 if (user_verify_otp($test_user['otp'], $_POST['otp']) === false) {
279 set_message(t('The verification code is not correct.'));
280 user_login_form($destination, $state, $nonce['uid'], 'otp');
281 return;
282 }
283 user_save($test_user); // Save the drift
284
285 break;
286 }
287
288 _user_login($test_user, true);
289
290 openid_indirect_response(simpleid_url($destination, $query), '');
291}
292
293/**
294 * Verifies a set of credentials for a specified user.
295 *
296 * A set of credentials comprises:
297 *
298 * - A user name
299 * - Some kind of verifying information, such as a plaintext password, a hashed
300 * password (e.g. digest) or some other kind of identifying information.
301 *
302 * The user name is passed to this function using the $uid parameter. The user
303 * name may or may not exist. If the user name does not exist, this function
304 * <strong>must</strong> return false.
305 *
306 * The credentials are supplied as an array using the $credentials parameter.
307 * Typically this array will be a subset of the $_POST superglobal passed to the
308 * {@link user_login()} function. Thus it will generally contain the keys 'pass' and
309 * 'digest'.
310 *
311 * This function calls the {@link hook_user_verify_credentials()} hook to
312 * check whether the credentials supplied matches the credentials
313 * for the specified user in the store.
314 *
315 * @param string $uid the name of the user to verify
316 * @param array $credentials the credentials supplied by the browser
317 * @return bool whether the credentials supplied matches those for the specified
318 * user
319 */
320function user_verify_credentials($uid, $credentials) {
321 global $simpleid_extensions;
322
323 $extensions = $simpleid_extensions;
324
325 if (!in_array('user_passauth', $extensions)) $extensions[] = 'user_passauth';
326
327 foreach ($extensions as $extension) {
328 $result = extension_invoke($extension, 'user_verify_credentials', $uid, $credentials);
329 if ($result === true) {
330 return true;
331 }
332 }
333
334 return false;
335}
336
337/**
338 * Verifies a one time password (OTP) specified by the user.
339 *
340 * This function compares an OTP supplied by a user with the OTP
341 * calculated based on the current time and the parameters of the
342 * algorithm. The parameters, such as the secret key, are supplied
343 * using in $params. These parameters are typically stored for each
344 * user in the user store.
345 *
346 * To allow for clocks going out of sync, the current time will be
347 * by a number (in time steps) specified in $params['drift']. If
348 * the OTP supplied by the user is accepted, $params['drift'] will
349 * be also be updated with the latest difference.
350 *
351 * To allow for network delay, the function will accepts OTPs which
352 * is a number of time steps away from the OTP calculated from the
353 * adjusted time. The maximum number of time steps is specified in
354 * the $max_drift parameter.
355 *
356 * @param array &$params the OTP parameters stored
357 * @param string $code the OTP supplied by the user
358 * @param int $max_drift the maximum drift allowed for network delay, in
359 * time steps
360 * @return bool whether the OTP supplied matches the OTP generated based on
361 * the specified parameters, within the maximum drift
362 */
363function user_verify_otp(&$params, $code, $max_drift = 1) {
364 switch ($params['type']) {
365 case 'totp':
366 $time = time();
367
368 $test_code = user_totp($params['secret'], $time, $params['period'], $params['drift'], $params['algorithm'], $params['digits']);
369
370 if ($test_code == intval($code)) return true;
371
372 for ($i = -$max_drift; $i <= $max_drift; $i++) {
373 $test_code = user_totp($params['secret'], $time, $params['period'], $params['drift'] + $i, $params['algorithm'], $params['digits']);
374 if ($test_code == intval($code)) {
375 $params['drift'] = $i;
376 return true;
377 }
378 }
379 return false;
380 break;
381 default:
382 return false;
383 }
384}
385
386
387/**
388 * Sets the user specified by the parameter as the active user.
389 *
390 * @param array $login_user the user to log in
391 * @param bool $auth_active whether the user has been actively authenticated
392 * in this session
393 *
394 */
395function _user_login($login_user, $auth_active = false) {
396 global $user;
397
398 if ($auth_active) {
399 // Set the current authentication time
400 $login_user['auth_time'] = time();
401 user_save($login_user);
402
403 // Set user has been actively authenticated this and the next request only
404 $login_user['auth_active'] = true;
405 $_SESSION['user_auth_active'] = true;
406 log_info('Login successful: ' . $login_user['uid'] . '['. gmstrftime('%Y-%m-%dT%H:%M:%SZ', $login_user['auth_time']) . ']');
407
408 }
409
410 $user = $login_user;
411 $_SESSION['user'] = $login_user['uid'];
412 cache_set('user', $login_user['uid'], session_id());
413
414
415 if ($auth_active) {
416 if (isset($_POST['autologin']) && ($_POST['autologin'] == 1)) user_cookieauth_create_cookie();
417 }
418}
419
420/**
421 * Attempts to log out a user and returns to the login form.
422 *
423 * @param string $destination the destination value to be included in the
424 * login form
425 */
426function user_logout($destination = NULL) {
427 global $user, $GETPOST;
428
429 // Require HTTPS, redirect if necessary
430 check_https('redirect', true);
431
432 $state = (isset($GETPOST['s'])) ? $GETPOST['s'] : '';
433 if ($destination == NULL) {
434 if (isset($GETPOST['destination'])) {
435 $destination = $GETPOST['destination'];
436 } else {
437 $destination = '';
438 }
439 }
440
441 _user_logout();
442
443 set_message(t('You have been logged out.'));
444
445 user_login_form($destination, $state);
446}
447
448/**
449 * Logs out the user by deleting the relevant session information.
450 */
451function _user_logout() {
452 global $user;
453
454 $uid = $user['uid'];
455
456 user_cookieauth_invalidate();
457 session_destroy();
458
459 cache_delete('user', $uid);
460 unset($_SESSION['user']);
461 $user = NULL;
462
463 log_info('Logout successful: ' . $uid);
464}
465
466/**
467 * Displays a user login or a login verification form.
468 *
469 * @param string $destination the SimpleID location to which the user is directed
470 * if login is successful
471 * @param string $state the current SimpleID state, if required by the location
472 * @param string $fixed_uid the user name to be included in the login form; if NULL, the user
473 * is asked to supply the user name. If $mode is otp this cannot be null
474 * @param string $mode either credentials (login form) or otp (login verification
475 * form)
476 */
477function user_login_form($destination = '', $state = NULL, $fixed_uid = NULL, $mode = 'credentials') {
478 global $xtpl;
479
480 // Require HTTPS, redirect if necessary
481 check_https('redirect', true);
482
483 if ($state) {
484 $xtpl->assign('state', htmlspecialchars($state, ENT_QUOTES, 'UTF-8'));
485 $xtpl->assign('cancel_button', t('Cancel'));
486 $xtpl->parse('main.login.state');
487 }
488
489 cache_expire(array('user-nonce' => SIMPLEID_LOGIN_NONCE_EXPIRES_IN));
490 $nonce = openid_nonce();
491 cache_set('user-nonce', $nonce, array('mode' => $mode, 'uid' => $fixed_uid));
492
493 $base_path = get_base_path();
494 $xtpl->assign('javascript', '<script src="' . $base_path . 'html/user-login.js" type="text/javascript"></script>');
495
496 header('X-Frame-Options: DENY');
497
498 switch ($mode) {
499 case 'credentials':
500 $security_class = (SIMPLEID_ALLOW_AUTOCOMPLETE) ? 'allow-autocomplete ' : '';
501 if (is_https()) {
502 $security_class .= 'secure';
503 $xtpl->assign('security_message', t('Secure login using <strong>HTTPS</strong>.'));
504 } elseif (SIMPLEID_ALLOW_PLAINTEXT) {
505 $security_class .= 'unsecure';
506 $xtpl->assign('security_message', t('<strong>WARNING:</strong> Your password will be sent to SimpleID as plain text.'));
507 }
508 $xtpl->assign('security_class', $security_class);
509 $xtpl->parse('main.login.login_security');
510
511 extension_invoke_all('user_login_form', $destination, $state);
512
513 $xtpl->assign('name_label', t('User name:'));
514 $xtpl->assign('pass_label', t('Password:'));
515 $xtpl->assign('autologin_label', t('Remember me on this computer for two weeks.'));
516
517 if ($fixed_uid == NULL) {
518 $xtpl->parse('main.login.credentials.input_uid');
519 } else {
520 $xtpl->assign('uid', htmlspecialchars($fixed_uid, ENT_QUOTES, 'UTF-8'));
521 $xtpl->parse('main.login.credentials.fixed_uid');
522 }
523
524 $xtpl->parse('main.login.credentials');
525 $xtpl->assign('submit_button', t('Log in'));
526 $xtpl->assign('title', t('Log In'));
527 break;
528 case 'otp':
529 // Note this is called from user_login(), so $_POST is always filled
530 $xtpl->assign('otp_instructions_label', t('To verify your identity, enter the verification code.'));
531 $xtpl->assign('otp_recovery_label', t('If you have lost your verification code, you can <a href="!url">recover your account</a>.',
532 array('!url' => 'http://simpleid.org/docs/1/common-problems/#otp')
533 ));
534
535 $xtpl->assign('otp_label', t('Verification code:'));
536 $xtpl->assign('autologin', (isset($_POST['autologin']) && ($_POST['autologin'] == 1)) ? '1' : '0');
537 $xtpl->parse('main.login.otp');
538
539 $xtpl->assign('submit_button', t('Verify'));
540 $xtpl->assign('title', t('Enter Verification Code'));
541 default:
542 }
543
544
545 $xtpl->assign('mode', $mode);
546 $xtpl->assign('page_class', 'dialog-page');
547 $xtpl->assign('destination', htmlspecialchars($destination, ENT_QUOTES, 'UTF-8'));
548 $xtpl->assign('nonce', htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8'));
549
550 $xtpl->parse('main.login');
551 $xtpl->parse('main.framekiller');
552 $xtpl->parse('main');
553 $xtpl->out('main');
554}
555
556/**
557 * Displays the page used to set up login verification using one-time
558 * passwords.
559 */
560function user_otp_page() {
561 global $xtpl, $user;
562
563 // Require HTTPS, redirect if necessary
564 check_https('redirect', true);
565
566 if ($user == NULL) {
567 user_login_form('my/profile');
568 return;
569 }
570
571 if ($_POST['op'] == t('Disable')) {
572 if (!isset($_POST['tk']) || !validate_form_token($_POST['tk'], 'dashboard_otp')) {
573 set_message(t('SimpleID detected a potential security attack. Please try again.'));
574 page_dashboard();
575 return;
576 }
577
578 if (isset($user['otp'])) {
579 unset($user['otp']);
580 user_save($user);
581 }
582 set_message('Login verification has been disabled.');
583 page_dashboard();
584 return;
585 } elseif ($_POST['op'] == t('Verify')) {
586 $params = $_SESSION['otp_setup'];
587
588 if (!isset($_POST['tk']) || !validate_form_token($_POST['tk'], 'otp')) {
589 set_message(t('SimpleID detected a potential security attack. Please try again.'));
590 page_dashboard();
591 return;
592 } elseif (!isset($_POST['otp']) || ($_POST['otp'] == '')) {
593 set_message(t('You need to enter the verification code to complete enabling login verification.'));
594 } elseif (user_verify_otp($params, $_POST['otp'], 10) === false) {
595 set_message(t('The verification code is not correct.'));
596 } else {
597 unset($_SESSION['otp_setup']);
598 $user['otp'] = $params;
599 user_save($user);
600
601 set_message('Login verification has been enabled.');
602 page_dashboard();
603 return;
604 }
605 } else {
606 $params = array(
607 'type' => 'totp',
608 'secret' => random_bytes(10),
609 'algorithm' => 'sha1',
610 'digits' => 6,
611 'period' => 30,
612 'drift' => 0,
613 );
614 $_SESSION['otp_setup'] = $params;
615 }
616
617 $code = strtr(bignum_val(bignum_new($params['secret'], 256), 32), '0123456789abcdefghijklmnopqrstuv', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567');
618 for ($i = 0; $i < strlen($code); $i += 4) {
619 $xtpl->assign('secret' . ($i + 1), substr($code, $i, 4));
620 }
621
622 $url = 'otpauth://totp/SimpleID?secret=' . $code . '&digits=' . $params['digits'] . '&period=' . $params['period'];
623 $xtpl->assign('qr', addslashes($url));
624
625 $xtpl->assign('about_otp', t('Login verification adds an extra layer of protection to your account. When enabled, you will need to enter an additional security code whenever you log into SimpleID.'));
626 $xtpl->assign('otp_warning', t('<strong>WARNING:</strong> If you enable login verification and lose your authenticator app, you will need to <a href="!url">edit your identity file manually</a> before you can log in again.',
627 array('!url' => 'http://simpleid.org/docs/1/common-problems/#otp')
628 ));
629
630 $xtpl->assign('setup_otp', t('To set up login verification, following these steps.'));
631 $xtpl->assign('download_app', t('Download an authenticator app that supports TOTP for your smartphone, such as Google Authenticator.'));
632 $xtpl->assign('add_account', t('Add your SimpleID account to authenticator app using this key. If you are viewing this page on your smartphone you can use <a href="!url">this link</a> or scan the QR code to add your account.',
633 array('!url' => $url)
634 ));
635 $xtpl->assign('verify_code', t('To check that your account has been added properly, enter the verification code from your phone into the box below, and click Verify.'));
636
637 $xtpl->assign('token', get_form_token('otp'));
638 $xtpl->assign('otp_label', t('Verification code:'));
639 $xtpl->assign('submit_button', t('Verify'));
640
641 $xtpl->assign('page_class', 'dialog-page');
642 $xtpl->assign('title', t('Login Verification'));
643
644 $xtpl->parse('main.otp');
645 $xtpl->parse('main.framekiller');
646
647 $xtpl->parse('main');
648 $xtpl->out('main');
649
650}
651
652
653/**
654 * Returns the user's public page.
655 *
656 * @param string $uid the user ID
657 */
658function user_public_page($uid = NULL) {
659 global $xtpl, $user;
660
661 $xtpl->assign('title', t('User Page'));
662 if ($uid == NULL) {
663 header_response_code('400 Bad Request');
664 set_message(t('No user specified.'));
665 } else {
666 $user = user_load($uid);
667
668 if ($user == NULL) {
669 header_response_code('404 Not Found');
670 set_message(t('User %uid not found.', array('%uid' => $uid)));
671 } else {
672 header('Vary: Accept');
673
674 $content_type = negotiate_content_type(array('text/html', 'application/xml', 'application/xhtml+xml', 'application/xrds+xml'));
675
676 if ($content_type == 'application/xrds+xml') {
677 user_xrds($uid);
678 return;
679 } else {
680 header('X-XRDS-Location: ' . simpleid_url('xrds/' . rawurlencode($uid)));
681
682 set_message(t('This is the user %uid\'s SimpleID page. It contains hidden information for the use by OpenID consumers.', array('%uid' => $uid)));
683
684 $xtpl->assign('title', htmlspecialchars($uid, ENT_QUOTES, 'UTF-8'));
685 $xtpl->assign('provider', htmlspecialchars(simpleid_url(), ENT_QUOTES, 'UTF-8'));
686 $xtpl->assign('xrds', htmlspecialchars(simpleid_url('xrds/' . rawurlencode($uid)), ENT_QUOTES, 'UTF-8'));
687 if ($user["local_identity"]) {
688 $xtpl->assign('local_id', htmlspecialchars($user["identity"], ENT_QUOTES, 'UTF-8'));
689 }
690 }
691 }
692 }
693
694 $xtpl->parse('main.provider');
695 if ($user["local_identity"]) $xtpl->parse('main.local_id');
696 $xtpl->parse('main');
697 $xtpl->out('main');
698}
699
700/**
701 * Returns the public page for a private personal ID.
702 *
703 * @param string $ppid the PPID
704 */
705function user_ppid_page($ppid = NULL) {
706 global $xtpl;
707
708 header('Vary: Accept');
709
710 $content_type = negotiate_content_type(array('text/html', 'application/xml', 'application/xhtml+xml', 'application/xrds+xml'));
711
712 if (($content_type == 'application/xrds+xml') || ($_GET['format'] == 'xrds')) {
713 header('Content-Type: application/xrds+xml');
714 header('Content-Disposition: inline; filename=yadis.xml');
715
716 $xtpl->assign('simpleid_base_url', htmlspecialchars(simpleid_url(), ENT_QUOTES, 'UTF-8'));
717 $xtpl->parse('xrds.user_xrds');
718 $xtpl->parse('xrds');
719 $xtpl->out('xrds');
720 return;
721 } else {
722 header('X-XRDS-Location: ' . simpleid_url('ppid/' . rawurlencode($ppid), 'format=xrds'));
723
724 $xtpl->assign('title', t('Private Personal Identifier'));
725
726 set_message(t('This is a private personal identifier.'));
727
728 $xtpl->parse('main');
729 $xtpl->out('main');
730 }
731}
732
733/**
734 * Returns the user's public XRDS page.
735 *
736 * @param string $uid the user ID
737 */
738function user_xrds($uid) {
739 global $xtpl;
740
741 $user = user_load($uid);
742
743 if ($user != NULL) {
744 header('Content-Type: application/xrds+xml');
745 header('Content-Disposition: inline; filename=yadis.xml');
746
747 if (($user != NULL) && ($user["local_identity"])) {
748 $xtpl->assign('local_id', htmlspecialchars($user["identity"], ENT_QUOTES, 'UTF-8'));
749 $xtpl->parse('xrds.user_xrds.local_id');
750 $xtpl->parse('xrds.user_xrds.local_id2');
751 }
752
753 $xtpl->assign('simpleid_base_url', htmlspecialchars(simpleid_url(), ENT_QUOTES, 'UTF-8'));
754 $xtpl->parse('xrds.user_xrds');
755 $xtpl->parse('xrds');
756 $xtpl->out('xrds');
757 } else {
758 if (substr(PHP_SAPI, 0,3) === 'cgi') {
759 header('Status: 404 Not Found');
760 } else {
761 header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
762 }
763
764 set_message('User <strong>' . htmlspecialchars($uid, ENT_QUOTES, 'UTF-8') . '</strong> not found.');
765 $xtpl->parse('main');
766 $xtpl->out('main');
767 }
768}
769
770/**
771 * Returns a block containing OpenID Connect user information.
772 *
773 * @return array the OpenID Connect user information block
774 */
775function _user_page_profile() {
776 global $user;
777
778 $html = '<p>' . t('SimpleID may, with your consent, send the following information to sites which supports OpenID Connect.') . '</p>';
779 $html .= '<p>' . t('To change these, <a href="!url">edit your identity file</a>.', array('!url' => 'http://simpleid.org/docs/1/identity-files/')) . '</p>';
780
781 $html .= "<table><tr><th>" . t('Member') . "</th><th>" . t('Value') . "</th></tr>";
782
783 if (isset($user['user_info'])) {
784 foreach ($user['user_info'] as $member => $value) {
785 if (is_array($value)) {
786 foreach ($value as $submember => $subvalue) {
787 $html .= "<tr><td>" . htmlspecialchars($member, ENT_QUOTES, 'UTF-8') . " (" .htmlspecialchars($submember, ENT_QUOTES, 'UTF-8') . ")</td><td>" . htmlspecialchars($subvalue, ENT_QUOTES, 'UTF-8') . "</td></tr>";
788 }
789 } else {
790 $html .= "<tr><td>" . htmlspecialchars($member, ENT_QUOTES, 'UTF-8') . "</td><td>" . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . "</td></tr>";
791 }
792 }
793 }
794
795 $html .= "</table>";
796
797 return array(array(
798 'id' => 'userinfo',
799 'title' => t('OpenID Connect'),
800 'content' => $html
801 ));
802}
803
804/**
805 * Set up the user section in the header, showing the currently logged in user.
806 *
807 * @param string $state the SimpleID state to retain once the user has logged out,
808 * if required.
809 */
810function user_header($state = NULL) {
811 global $user;
812 global $xtpl;
813
814 if ($user != NULL) {
815 $xtpl->assign('uid', htmlspecialchars($user['uid'], ENT_QUOTES, 'UTF-8'));
816 $xtpl->assign('identity', htmlspecialchars($user['identity'], ENT_QUOTES, 'UTF-8'));
817 if ($state != NULL) {
818 $xtpl->assign('url', htmlspecialchars(simpleid_url('logout', 'destination=continue&s=' . rawurlencode($state), true)));
819 $xtpl->assign('logout', t('Log out and log in as a different user'));
820 } else {
821 $xtpl->assign('url', htmlspecialchars(simpleid_url('logout', '', true)));
822 $xtpl->assign('logout', t('Log out'));
823 }
824 $xtpl->parse('main.user.logout');
825 $xtpl->parse('main.user');
826 }
827}
828
829/**
830 * Verifies a set of credentials using the default user name-password authentication
831 * method.
832 *
833 * @param string $uid the name of the user to verify
834 * @param array $credentials the credentials supplied by the browser
835 * @return bool whether the credentials supplied matches those for the specified
836 * user
837 */
838function user_passauth_user_verify_credentials($uid, $credentials) {
839 $allowed_algorithms = array('md5', 'sha1');
840 if (function_exists('hash_algos')) $allowed_algorithms = array_merge($allowed_algorithms, hash_algos());
841 if (function_exists('hash_pbkdf2')) $allowed_algorithms[] = 'pbkdf2';
842
843 $test_user = user_load($uid);
844
845 if ($test_user == NULL) return false;
846
847 $hash_function_salt = explode(':', $test_user['pass'], 3);
848
849 $hash = $hash_function_salt[0];
850 $function = (isset($hash_function_salt[1])) ? $hash_function_salt[1] : 'md5';
851 if (!in_array($function, $allowed_algorithms)) $function = 'md5';
852 $salt_suffix = (isset($hash_function_salt[2])) ? ':' . $hash_function_salt[2] : '';
853
854 switch ($function) {
855 case 'pbkdf2':
856 list ($algo, $iterations, $salt) = explode(':', $hash_function_salt[2]);
857 $length = (function_exists('hash')) ? strlen(hash($algo, '')) : 0;
858 $test_hash = hash_pbkdf2($algo, $credentials['pass'], $salt, $iterations, $length);
859 break;
860 case 'md5':
861 case 'sha1':
862 $test_hash = call_user_func($function, $credentials['pass'] . $salt_suffix);
863 break;
864 default:
865 $test_hash = hash($function, $credentials['pass'] . $salt_suffix);
866 }
867
868 return secure_compare($test_hash, $hash);
869}
870
871/**
872 * Creates a auto login cookie. The login cookie will be based on the
873 * current log in user.
874 *
875 * @param string $id the ID of the series of auto login cookies, Cookies
876 * belonging to the same user and computer have the same ID. If none is specified,
877 * one will be generated
878 * @param int $expires the time at which the cookie will expire. If none is specified
879 * the time specified in {@link SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN} will be
880 * used
881 *
882 */
883function user_cookieauth_create_cookie($id = NULL, $expires = NULL) {
884 global $user;
885
886 if ($expires == NULL) {
887 log_debug('Automatic login token created for ' . $user['uid']);
888 } else {
889 log_debug('Automatic login token renewed for ' . $user['uid']);
890 }
891
892 if ($id == NULL) $id = random_id();
893 if ($expires == NULL) $expires = time() + SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN;
894 $token = random_secret();
895 $uid_hash = get_form_token($user['uid'], FALSE);
896
897 $data = array(
898 'uid' => $user['uid'],
899 'token' => $token,
900 'expires' => $expires,
901 'uaid' => get_user_agent_id(),
902 'ip' => $_SERVER['REMOTE_ADDR']
903 );
904
905 cache_set('autologin-'. $uid_hash, $id, $data);
906
907 // Note the last parameter (httponly) requires PHP 5.2
908 setcookie(simpleid_cookie_name('auth'), 'cookieauth:' . $uid_hash . ':' . $id . ':' . $token, $expires, get_base_path(), '', false, true);
909}
910
911/**
912 * Verifies a auto login cookie. If valid, log in the user automatically.
913 */
914function user_cookieauth_user_auto_login() {
915 if (!isset($_COOKIE[simpleid_cookie_name('auth')])) return NULL;
916
917 $cookie = $_COOKIE[simpleid_cookie_name('auth')];
918
919 list($authtype, $uid_hash, $id, $token) = explode(':', $cookie);
920 if ($authtype != 'cookieauth') return NULL;
921
922 log_debug('Automatic login token detected: ' . implode(':', ['cookieauth', $uid_hash, $id]));
923
924 cache_expire(array('autologin-' . $uid_hash => SIMPLEID_USER_AUTOLOGIN_EXPIRES_IN));
925 $data = cache_get('autologin-' . $uid_hash, $id);
926
927 if (!$data) { // Cookie doesn't exist
928 log_notice('Automatic login: Token does not exist on server');
929 return NULL;
930 }
931
932 if ($data['expires'] < time()) { // Cookie expired
933 log_notice('Automatic login: Token on server expired');
934 return NULL;
935 }
936
937 if ($data['token'] != $token) {
938 log_warn('Automatic login: Token on server does not match');
939 // Token not the same - panic
940 cache_expire(array('autologin-' . $uid_hash => 0));
941 user_cookieauth_invalidate();
942 return NULL;
943 }
944
945 if ($data['uaid'] != get_user_agent_id()) {
946 log_warn('Automatic login: User agent ID does not match');
947 // Token not the same - panic
948 cache_expire(array('autologin-' . $uid_hash => 0));
949 user_cookieauth_invalidate();
950 return NULL;
951 }
952
953 // Load the user, tag it as an auto log in
954 $test_user = user_load($data['uid']);
955
956 if ($test_user != NULL) {
957 log_debug('Automatic login token accepted for ' . $data['uid']);
958
959 $test_user['autologin'] = TRUE;
960
961 // Renew the token
962 user_cookieauth_create_cookie($id, $data['expires']);
963
964 return $test_user;
965 } else {
966 log_warn('Automatic login token accepted for ' . $data['uid'] . ', but no such user exists');
967 return NULL;
968 }
969}
970
971/**
972 * Removes the auto login cookie from the user agent and the SimpleID
973 * cache.
974 */
975function user_cookieauth_invalidate() {
976 if (isset($_COOKIE[simpleid_cookie_name('auth')])) {
977 $cookie = $_COOKIE[simpleid_cookie_name('auth')];
978
979 list($uid_hash, $id, $token) = explode(':', $cookie);
980
981 cache_delete('autologin-' . $uid_hash, $id);
982
983 setcookie(simpleid_cookie_name('auth'), "", time() - 3600);
984 }
985}
986
987/**
988 * Calculates a Time-Based One-Time Password (TOTP) based on RFC 6238.
989 *
990 * This function returns an integer calculated from the TOTP algorithm.
991 * The returned integer may need to be zero-padded to return a string
992 * with the required number of digits
993 *
994 * @param string $secret the shared secret as a binary string
995 * @param int $time the time to use in the HOTP algorithm. If NULL, the
996 * current time is used
997 * @param int $period the time step in seconds
998 * @param int $drift the number of time steps to be added to the time to
999 * adjust for transmission delay
1000 * @param string $algorithm the hashing algorithm as supported by
1001 * the hash_hmac() function
1002 * @param int $digits the number of digits in the one-time password
1003 * @return int the one-time password
1004 * @link http://tools.ietf.org/html/rfc6238
1005 */
1006function user_totp($secret, $time = NULL, $period = 30, $drift = 0, $algorithm = 'sha1', $digits = 6) {
1007 if ($time == NULL) $time = time();
1008 $counter = floor($time / $period) + $drift;
1009 $data = pack('NN', 0, $counter);
1010 return user_hotp($secret, $data, $algorithm, $digits);
1011}
1012
1013/**
1014 * Calculates a HMAC-Based One-Time Password (HOTP) based on RFC 4226.
1015 *
1016 * This function returns an integer calculated from the HOTP algorithm.
1017 * The returned integer may need to be zero-padded to return a string
1018 * with the required number of digits
1019 *
1020 * @param string $secret the shared secret as a binary string
1021 * @param string $data the counter value as a 64 bit in
1022 * big endian encoding
1023 * @param string $algorithm the hashing algorithm as supported by
1024 * the hash_hmac() function
1025 * @param int $digits the number of digits in the one-time password
1026 * @return int the one-time password
1027 * @link http://tools.ietf.org/html/rfc4226
1028 */
1029function user_hotp($secret, $data, $algorithm = 'sha1', $digits = 6) {
1030 // unpack produces a 1-based array, we use array_merge to convert it to 0-based
1031 $hmac = array_merge(unpack('C*', hash_hmac(strtolower($algorithm), $data, $secret, true)));
1032 $offset = $hmac[19] & 0xf;
1033 $code = ($hmac[$offset + 0] & 0x7F) << 24 |
1034 ($hmac[$offset + 1] & 0xFF) << 16 |
1035 ($hmac[$offset + 2] & 0xFF) << 8 |
1036 ($hmac[$offset + 3] & 0xFF);
1037 return $code % pow(10, $digits);
1038}
1039
1040
1041if (!function_exists('hash_pbkdf2') && function_exists('hash_hmac')) {
1042 function hash_pbkdf2($algo, $password, $salt, $iterations, $length = 0, $raw_output = false) {
1043 $result = '';
1044 $hLen = strlen(hash($algo, '', true));
1045 if ($length == 0) {
1046 $length = $hLen;
1047 if (!$raw_output) $length *= 2;
1048 }
1049 $l = ceil($length / $hLen);
1050
1051 for ($i = 1; $i <= $l; $i++) {
1052 $U = hash_hmac($algo, $salt . pack('N', $i), $password, true);
1053 $T = $U;
1054 for ($j = 1; $j < $iterations; $j++) {
1055 $T ^= ($U = hash_hmac($algo, $U, $password, true));
1056 }
1057 $result .= $T;
1058 }
1059
1060 return substr(($raw_output) ? $result : bin2hex($result), 0, $length);
1061 }
1062}
1063
1064
1065?>