4 * The outside frame that holds all of the OpenEMR User Interface.
7 * @link http://www.open-emr.org
8 * @author Rod Roark <rod@sunsetsystems.com>
9 * @author Brady Miller <brady.g.miller@gmail.com>
10 * @author Ranganath Pathak <pathak@scrs1.org>
11 * @copyright Copyright (c) 2018 Rod Roark <rod@sunsetsystems.com>
12 * @copyright Copyright (c) 2018-2019 Brady Miller <brady.g.miller@gmail.com>
13 * @copyright Copyright (c) 2019 Ranganath Pathak <pathak@scrs1.org>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 // Set $sessionAllowWrite to true to prevent session concurrency issues during authorization and app setup related code
18 $sessionAllowWrite = true;
19 require_once('../globals.php');
21 use OpenEMR\Common\Auth\AuthUtils
;
22 use OpenEMR\Common\Crypto\CryptoGen
;
23 use OpenEMR\Common\Csrf\CsrfUtils
;
24 use OpenEMR\Common\Session\SessionTracker
;
25 use OpenEMR\Common\Utils\RandomGenUtils
;
26 use OpenEMR\Core\Header
;
27 use OpenEMR\Services\FacilityService
;
28 use OpenEMR\Services\ListService
;
29 use u2flib_server\U2F
;
31 ///////////////////////////////////////////////////////////////////////
32 // Functions to support MFA.
33 ///////////////////////////////////////////////////////////////////////
35 function posted_to_hidden($name)
37 if (isset($_POST[$name])) {
38 echo "<input type='hidden' name='" . attr($name) . "' value='" . attr($_POST[$name]) . "' />\r\n";
42 function generate_html_start()
47 <?php Header
::setupHeader(); ?
>
48 <title
><?php
echo xlt("MFA Authorization"); ?
></title
>
58 function generate_html_u2f()
62 <script src
="<?php echo $GLOBALS['webroot'] ?>/library/js/u2f-api.js"></script
>
65 var f
= document
.getElementById("u2fform");
66 var requests
= JSON
.parse(f
.form_requests
.value
);
67 // The server's getAuthenticateData() repeats the same challenge in all requests.
68 var challenge
= requests
[0].challenge
;
69 var registeredKeys
= new Array();
70 for (var i
= 0; i
< requests
.length
; ++i
) {
71 registeredKeys
[i
] = {"version": requests
[i
].version
, "keyHandle": requests
[i
].keyHandle
};
74 <?php
echo js_escape($appId); ?
>,
78 if (data
.errorCode
&& data
.errorCode
!= 0) {
79 alert(<?php
echo xlj("Key access failed with error"); ?
> +
' ' + data
.errorCode
);
82 f
.form_response
.value
= JSON
.stringify(data
);
92 function input_focus()
104 function generate_html_top()
110 function generate_html_middle()
112 posted_to_hidden('new_login_session_management');
113 posted_to_hidden('languageChoice');
114 posted_to_hidden('authUser');
115 posted_to_hidden('clearPass');
118 require_once(dirname(__FILE__
) . "/../../src/Common/Session/SessionUtil.php");
119 function generate_html_end()
121 // to be safe, remove clearPass from memory now (if it is not empty yet)
122 if (!empty($_POST["clearPass"])) {
123 if (function_exists('sodium_memzero')) {
124 sodium_memzero($_POST["clearPass"]);
126 $_POST["clearPass"] = '';
129 echo "</div></body></html>\n";
130 OpenEMR\Common\Session\SessionUtil
::coreSessionDestroy();
134 if (isset($_POST['new_login_session_management'])) {
135 ///////////////////////////////////////////////////////////////////////
136 // Begin code to support U2F and APP Based TOTP logic.
137 ///////////////////////////////////////////////////////////////////////
139 $regs = array(); // for mapping device handles to their names
140 $registrations = array(); // the array of stored registration objects
141 $res1 = sqlStatement(
142 "SELECT a.name, a.method, a.var1 FROM login_mfa_registrations AS a " .
143 "WHERE a.user_id = ? AND (a.method = 'TOTP' OR a.method = 'U2F') ORDER BY a.name",
144 array($_SESSION['authUserID'])
147 $registrationAttempt = false;
150 while ($row1 = sqlFetchArray($res1)) {
151 $registrationAttempt = true;
152 if ($row1['method'] == 'U2F') {
154 $regobj = json_decode($row1['var1']);
155 $regs[json_encode($regobj->keyHandle
)] = $row1['name'];
156 $registrations[] = $regobj;
157 } else { // $row1['method'] == 'TOTP'
162 if ($registrationAttempt) {
166 // There is at least one U2F key registered so we have to request or verify key data.
167 // https is required, and with a proxy the server might not see it.
168 $scheme = "https://"; // isset($_SERVER['HTTPS']) ? "https://" : "http://";
169 $appId = $scheme . $_SERVER['HTTP_HOST'];
170 $u2f = new u2flib_server\
U2F($appId);
172 $userid = $_SESSION['authUserID'];
173 $form_response = empty($_POST['form_response']) ?
'' : $_POST['form_response'];
174 if ($form_response) {
175 // TOTP METHOD enabled if TOTP is visible in post request
176 if (isset($_POST['totp']) && trim($_POST['totp']) != "" && $isTOTP) {
182 "SELECT a.var1 FROM login_mfa_registrations AS a WHERE a.user_id = ? AND a.method = 'TOTP'",
183 array($_SESSION['authUserID'])
185 $registrationSecret = false;
186 if (!empty($res1['var1'])) {
187 $registrationSecret = $res1['var1'];
190 // Decrypt the secret
191 // First, try standard method that uses standard key
192 $cryptoGen = new CryptoGen();
193 $secret = $cryptoGen->decryptStandard($registrationSecret);
194 if (empty($secret)) {
195 // Second, try the password hash, which was setup during install and is temporary
196 $passwordResults = privQuery(
197 "SELECT password FROM users_secure WHERE username = ?",
198 array($_POST["authUser"])
200 if (!empty($passwordResults["password"])) {
201 $secret = $cryptoGen->decryptStandard($registrationSecret, $passwordResults["password"]);
202 if (!empty($secret)) {
203 error_log("Disregard the decryption failed authentication error reported above this line; it is not an error.");
204 // Re-encrypt with the more secure standard key
205 $secretEncrypt = $cryptoGen->encryptStandard($secret);
207 "UPDATE login_mfa_registrations SET var1 = ? where user_id = ? AND method = 'TOTP'",
208 array($secretEncrypt, $userid)
214 if (!empty($secret)) {
215 $googleAuth = new Totp($secret);
216 $form_response = $googleAuth->validateCode($_POST['totp']);
219 if ($form_response) {
220 // Keep track of when challenges were last answered correctly.
222 "UPDATE users_secure SET last_challenge_response = NOW() WHERE id = ?",
223 array($_SESSION['authUserID'])
226 $errormsg = xl("The code you entered was not valid");
229 } elseif ($isU2F) { // Otherwise use U2F METHOD
230 // We have key data, check if it matches what was registered.
231 $tmprow = sqlQuery("SELECT login_work_area FROM users_secure WHERE id = ?", array($userid));
233 $registration = $u2f->doAuthenticate(
234 json_decode($tmprow['login_work_area']), // these are the original challenge requests
236 json_decode($_POST['form_response'])
238 // Stored registration data needs to be updated because the usage count has changed.
239 // We have to use the matching registered key.
240 $strhandle = json_encode($registration->keyHandle
);
241 if (isset($regs[$strhandle])) {
243 "UPDATE login_mfa_registrations SET `var1` = ? WHERE " .
244 "`user_id` = ? AND `method` = 'U2F' AND `name` = ?",
245 array(json_encode($registration), $userid, $regs[$strhandle])
248 error_log("Unexpected keyHandle returned from doAuthenticate(): '" . errorLogEscape($strhandle) . "'");
250 // Keep track of when challenges were last answered correctly.
252 "UPDATE users_secure SET last_challenge_response = NOW() WHERE id = ?",
253 array($_SESSION['authUserID'])
255 } catch (u2flib_server\Error
$e) {
256 // Authentication failed so we will build the U2F form again.
258 $errormsg = xl('U2F Key Authentication error') . ": " . $e->getMessage();
266 if (!$form_response) {
267 generate_html_start();
276 echo '<div class="container">';
277 echo '<div class="row">';
278 echo ' <div class="col-sm-12">';
279 echo ' <h2>' . xlt('TOTP Verification') . '</h2>';
282 if ($errormsg && $errortype == "TOTP") {
283 echo '<div class="row"><div class="col-sm-12"><div class="alert alert-danger alert-msg">' . text($errormsg) . '</div></div></div>';
286 echo '<div class="row">';
287 echo ' <div class="col-sm-12">';
288 echo ' <form method="post" action="main_screen.php?auth=login&site=' . attr_url($_GET['site']) . '" target="_top" name="challenge_form" id=="challenge_form">';
290 echo ' <legend>' . xlt('Provide TOTP code') . '</legend>';
291 echo ' <div class="form-group">';
292 echo ' <div class="col-sm-6 offset-sm-3">';
293 echo ' <label for="totp">' . xlt('Enter the code from your authentication application on your device') . ':</label>';
294 echo ' <input type="text" name="totp" class="form-control input-lg" id="totp" maxlength="12" required>';
295 echo ' <input type="hidden" name="form_response" value="true" />';
296 generate_html_middle();
299 echo ' <div class="form-group clearfix">';
300 echo ' <div class="col-sm-12 text-left position-override">';
301 echo ' <button type="submit" class="btn btn-secondary btn-save">' . xlt('Authenticate TOTP') . '</button>';
310 // There is no key data yet or authentication failed, so we need to solicit it.
311 $requests = json_encode($u2f->getAuthenticateData($registrations));
312 // Persist the challenge also in the database because the browser is untrusted.
314 "UPDATE users_secure SET login_work_area = ? WHERE id = ?",
315 array($requests, $userid)
318 echo '<div class="container">';
319 echo '<div class="row">';
320 echo ' <div class="col-sm-12">';
321 echo ' <h2>' . xlt('U2F Key Verification') . '</h2>';
324 if ($errormsg && $errortype == "U2F") {
325 echo '<div class="row"><div class="col-sm-12"><div class="alert alert-danger alert-msg">' . text($errormsg) . '</div></div></div>';
327 echo '<div class="row">';
328 echo ' <div class="col-sm-12">';
329 echo ' <form method="post" name="u2fform" id="u2fform" action="main_screen.php?auth=login&site=' . attr_url($_GET['site']) . '" target="_top">';
331 echo ' <legend>' . xlt('Insert U2F Key') . '</legend>';
332 echo ' <div class="form-group">';
333 echo ' <div class="col-sm-6 offset-sm-3">';
335 echo ' <li>' . xlt('Insert your key into a USB port and click the Authenticate button below.') . '</li>';
336 echo ' <li>' . xlt('Then press the flashing button on your key within 1 minute.') . '</li>';
340 echo ' <div class="form-group clearfix">';
341 echo ' <div class="col-sm-12 text-left position-override">';
342 echo ' <button type="button" id="authutf" class="btn btn-secondary btn-save" onclick="doAuth()">' . xlt('Authenticate U2F') . '</button>';
343 echo ' <input type="hidden" name="form_requests" value="' . attr($requests) . '" />';
344 echo ' <input type="hidden" name="form_response" value="" />';
345 generate_html_middle();
352 exit(generate_html_end());
355 ///////////////////////////////////////////////////////////////////////
356 // End of U2F and APP Based TOTP logic.
357 ///////////////////////////////////////////////////////////////////////
360 // Creates a new session id when load this outer frame
361 // (allows creations of separate OpenEMR frames to view patients concurrently
362 // on different browser frame/windows)
363 // This session id is used below in the restoreSession.php include to create a
364 // session cookie for this specific OpenEMR instance that is then maintained
365 // within the OpenEMR instance by calling top.restoreSession() whenever
366 // refreshing or starting a new script.
368 // This is a new login, so create a new session id and remove the old session
369 session_regenerate_id(true);
370 // Also need to delete clearPass from memory
371 if (function_exists('sodium_memzero')) {
372 sodium_memzero($_POST["clearPass"]);
374 $_POST["clearPass"] = '';
377 // This is not a new login, so check csrf and then create a new session id and do NOT remove the old session
378 if (!CsrfUtils
::verifyCsrfToken($_POST["csrf_token_form"])) {
379 CsrfUtils
::csrfNotVerified();
381 session_regenerate_id(false);
383 // Set up the csrf private_key
384 // Note this key always remains private and never leaves server session. It is used to create
386 CsrfUtils
::setupCsrfKey();
387 // Set up the session uuid. This will be used for mapping session setting to database.
388 // At this time only used for lastupdate tracking
389 SessionTracker
::setupSessionDatabaseTracker();
391 $_SESSION["encounter"] = '';
393 if ($GLOBALS['login_into_facility']) {
394 $facility_id = $_POST['facility'];
395 if ($facility_id === 'user_default') {
396 //get the default facility of login user from users table
397 $facilityService = new FacilityService();
398 $facility = $facilityService->getFacilityForUser($_SESSION['authUserID']);
399 $facility_id = $facility['id'];
401 $_SESSION['facilityId'] = $facility_id;
402 if ($GLOBALS['set_facility_cookie']) {
403 // set cookie with facility for the calender screens
404 setcookie("pc_facility", $_SESSION['facilityId'], time() +
(3600 * 365), $GLOBALS['webroot']);
408 // Fetch the password expiration date (note LDAP skips this)
410 if ((!AuthUtils
::useActiveDirectory()) && ($GLOBALS['password_expiration_days'] != 0) && (check_integer($GLOBALS['password_expiration_days']))) {
411 $result = privQuery("select `last_update_password` from `users_secure` where `id` = ?", [$_SESSION['authUserID']]);
412 $current_date = date('Y-m-d');
413 if (!empty($result['last_update_password'])) {
414 $pwd_last_update = $result['last_update_password'];
416 error_log("OpenEMR ERROR: there is a problem with recording of last_update_password entry in users_secure table");
417 $pwd_last_update = $current_date;
420 // Display the password expiration message (will show during the grace time)
421 $pwd_alert_date = date('Y-m-d', strtotime($pwd_last_update . '+' . $GLOBALS['password_expiration_days'] . ' days'));
423 if (empty(strtotime($pwd_alert_date))) {
424 error_log("OpenEMR ERROR: there is a problem when trying to check if user's password is expired");
425 } elseif (strtotime($current_date) >= strtotime($pwd_alert_date)) {
430 $listSvc = new ListService();
431 $_tabs = $listSvc->getOptionsByListName('default_open_tabs', ['activity' => 1]);
434 //display the php file containing the password expiration message.
435 array_unshift($_tabs, [
436 'notes' => "pwd_expires_alert.php?csrf_token_form=" . attr_url(CsrfUtils
::collectCsrfToken()),
438 "label" => xl("Password Reset"),
440 } elseif (!empty($_POST['patientID'])) {
441 // Patient is open, so add this to the list of tabs, at the end
442 $patientID = (int) $_POST['patientID'];
443 $_notes = "../patient_file/summary/demographics.php?set_pid=" . attr_url($patientID);
444 if (!empty($_POST['encounterID'])) {
445 $encounterID = (int) $_POST['encounterID'];
446 $_notes = $_notes . "&set_encounterid=" . attr_url($encounterID);
451 'label' => xl("Dashboard"),
453 } elseif (isset($_GET['mode']) && $_GET['mode'] == "loadcalendar") {
454 // Load the calendar, at the end
455 $_notes = "calendar/index.php?pid=" . attr_url($_GET['pid']);
456 $_notes = (isset($_GET['date'])) ?
$_notes . "&date=" . attr_url($_GET['date']) : $_notes;
460 "label" => xl("Calendar"),
464 // Will set Session variables to communicate settings to tab layout
465 $_SESSION['default_open_tabs'] = $_tabs;
466 // mdsupport - Apps processing invoked for valid app selections from list
467 if ((isset($_POST['appChoice'])) && ($_POST['appChoice'] !== '*OpenEMR')) {
468 $_SESSION['app1'] = $_POST['appChoice'];
471 // Pass a unique token, so main.php script can not be run on its own
472 $_SESSION['token_main_php'] = RandomGenUtils
::createUniqueToken();
473 header('Location: ' . $web_root . "/interface/main/tabs/main.php?token_main=" . urlencode($_SESSION['token_main_php']));