3 * The outside frame that holds all of the OpenEMR User Interface.
6 * @link http://www.open-emr.org
7 * @author Rod Roark <rod@sunsetsystems.com>
8 * @author Brady Miller <brady.g.miller@gmail.com>
9 * @author Ranganath Pathak <pathak@scrs1.org>
10 * @copyright Copyright (c) 2018 Rod Roark <rod@sunsetsystems.com>
11 * @copyright Copyright (c) 2018-2019 Brady Miller <brady.g.miller@gmail.com>
12 * @copyright Copyright (c) 2019 Ranganath Pathak <pathak@scrs1.org>
13 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
16 /* Include our required headers */
17 require_once('../globals.php');
19 use OpenEMR\Common\Crypto\CryptoGen
;
20 use OpenEMR\Common\Utils\RandomGenUtils
;
21 use OpenEMR\Core\Header
;
22 use OpenEMR\Services\FacilityService
;
23 use u2flib_server\U2F
;
25 ///////////////////////////////////////////////////////////////////////
26 // Functions to support MFA.
27 ///////////////////////////////////////////////////////////////////////
29 function posted_to_hidden($name)
31 if (isset($_POST[$name])) {
32 echo "<input type='hidden' name='" . attr($name) . "' value='" . attr($_POST[$name]) . "' />\r\n";
36 function generate_html_start()
41 <?php Header
::setupHeader(); ?
>
42 <title
><?php
echo xlt("MFA Authorization"); ?
></title
>
52 function generate_html_u2f()
56 <script src
="<?php echo $GLOBALS['webroot'] ?>/library/js/u2f-api.js"></script
>
59 var f
= document
.getElementById("u2fform");
60 var requests
= JSON
.parse(f
.form_requests
.value
);
61 // The server's getAuthenticateData() repeats the same challenge in all requests.
62 var challenge
= requests
[0].challenge
;
63 var registeredKeys
= new Array();
64 for (var i
= 0; i
< requests
.length
; ++i
) {
65 registeredKeys
[i
] = {"version": requests
[i
].version
, "keyHandle": requests
[i
].keyHandle
};
68 <?php
echo js_escape($appId); ?
>,
72 if (data
.errorCode
&& data
.errorCode
!= 0) {
73 alert(<?php
echo xlj("Key access failed with error"); ?
> +
' ' + data
.errorCode
);
76 f
.form_response
.value
= JSON
.stringify(data
);
86 function input_focus()
98 function generate_html_top()
104 function generate_html_middle()
106 posted_to_hidden('new_login_session_management');
107 posted_to_hidden('authProvider');
108 posted_to_hidden('languageChoice');
109 posted_to_hidden('authUser');
110 posted_to_hidden('clearPass');
113 function generate_html_end()
115 echo "</div></body></html>\n";
118 unset($_COOKIE[session_name()]);
122 ///////////////////////////////////////////////////////////////////////
123 // Begin code to support U2F and APP Based TOTP logic.
124 ///////////////////////////////////////////////////////////////////////
126 $regs = array(); // for mapping device handles to their names
127 $registrations = array(); // the array of stored registration objects
128 $res1 = sqlStatement(
129 "SELECT a.name, a.method, a.var1 FROM login_mfa_registrations AS a " .
130 "WHERE a.user_id = ? AND (a.method = 'TOTP' OR a.method = 'U2F') ORDER BY a.name",
131 array($_SESSION['authId'])
134 $registrationAttempt = false;
137 while ($row1 = sqlFetchArray($res1)) {
138 $registrationAttempt = true;
139 if ($row1['method'] == 'U2F') {
141 $regobj = json_decode($row1['var1']);
142 $regs[json_encode($regobj->keyHandle
)] = $row1['name'];
143 $registrations[] = $regobj;
144 } else { // $row1['method'] == 'TOTP'
149 if ($registrationAttempt) {
153 // There is at least one U2F key registered so we have to request or verify key data.
154 // https is required, and with a proxy the server might not see it.
155 $scheme = "https://"; // isset($_SERVER['HTTPS']) ? "https://" : "http://";
156 $appId = $scheme . $_SERVER['HTTP_HOST'];
157 $u2f = new u2flib_server\
U2F($appId);
159 $userid = $_SESSION['authId'];
160 $form_response = empty($_POST['form_response']) ?
'' : $_POST['form_response'];
161 if ($form_response) {
162 // TOTP METHOD enabled if TOTP is visible in post request
163 if (isset($_POST['totp']) && trim($_POST['totp']) != "" && $isTOTP) {
169 "SELECT a.var1 FROM login_mfa_registrations AS a WHERE a.user_id = ? AND a.method = 'TOTP'",
170 array($_SESSION['authId'])
172 $registrationSecret = false;
173 if (!empty($res1['var1'])) {
174 $registrationSecret = $res1['var1'];
177 // Decrypt the secret
178 // First, try standard method that uses standard key
179 $cryptoGen = new CryptoGen();
180 $secret = $cryptoGen->decryptStandard($registrationSecret);
181 if (empty($secret)) {
182 // Second, try the password hash, which was setup during install and is temporary
183 $passwordResults = privQuery(
184 "SELECT password FROM users_secure WHERE username = ?",
185 array($_POST["authUser"])
187 if (!empty($passwordResults["password"])) {
188 $secret = $cryptoGen->decryptStandard($registrationSecret, $passwordResults["password"]);
189 if (!empty($secret)) {
190 error_log("Disregard the decryption failed authentication error reported above this line; it is not an error.");
191 // Re-encrypt with the more secure standard key
192 $secretEncrypt = $cryptoGen->encryptStandard($secret);
194 "UPDATE login_mfa_registrations SET var1 = ? where user_id = ? AND method = 'TOTP'",
195 array($secretEncrypt, $userid)
201 if (!empty($secret)) {
202 $googleAuth = new Totp($secret);
203 $form_response = $googleAuth->validateCode($_POST['totp']);
206 if ($form_response) {
207 // Keep track of when challenges were last answered correctly.
209 "UPDATE users_secure SET last_challenge_response = NOW() WHERE id = ?",
210 array($_SESSION['authId'])
213 $errormsg = xl("The code you entered was not valid");
216 } elseif ($isU2F) { // Otherwise use U2F METHOD
217 // We have key data, check if it matches what was registered.
218 $tmprow = sqlQuery("SELECT login_work_area FROM users_secure WHERE id = ?", array($userid));
220 $registration = $u2f->doAuthenticate(
221 json_decode($tmprow['login_work_area']), // these are the original challenge requests
223 json_decode($_POST['form_response'])
225 // Stored registration data needs to be updated because the usage count has changed.
226 // We have to use the matching registered key.
227 $strhandle = json_encode($registration->keyHandle
);
228 if (isset($regs[$strhandle])) {
230 "UPDATE login_mfa_registrations SET `var1` = ? WHERE " .
231 "`user_id` = ? AND `method` = 'U2F' AND `name` = ?",
232 array(json_encode($registration), $userid, $regs[$strhandle])
235 error_log("Unexpected keyHandle returned from doAuthenticate(): '$strhandle'");
237 // Keep track of when challenges were last answered correctly.
239 "UPDATE users_secure SET last_challenge_response = NOW() WHERE id = ?",
240 array($_SESSION['authId'])
242 } catch (u2flib_server\Error
$e) {
243 // Authentication failed so we will build the U2F form again.
245 $errormsg = xl('U2F Key Authentication error') . ": " . $e->getMessage();
253 if (!$form_response) {
254 generate_html_start();
263 echo '<div class="container">';
264 echo '<div class="row">';
265 echo ' <div class="col-sm-12">';
266 echo ' <div class="page-header">';
267 echo ' <h2>' . xlt('TOTP Verification') . '</h2>';
271 if ($errormsg && $errortype == "TOTP") {
272 echo '<div class="row"><div class="col-sm-12"><div class="alert alert-danger alert-msg">' . text($errormsg) . '</div></div></div>';
275 echo '<div class="row">';
276 echo ' <div class="col-sm-12">';
277 echo ' <form method="post" action="main_screen.php?auth=login&site=' . attr_url($_GET['site']) . '" target="_top" name="challenge_form" id=="challenge_form">';
279 echo ' <legend>'. xlt('Provide TOTP code') .'</legend>';
280 echo ' <div class="form-group">';
281 echo ' <div class="col-sm-6 col-sm-offset-3">';
282 echo ' <label for="totp">' . xlt('Enter the code from your authentication application on your device') . ':</label>';
283 echo ' <input type="text" name="totp" class="form-control input-lg" id="totp" maxlength="12" required>';
284 echo ' <input type="hidden" name="form_response" value="true" />';
285 generate_html_middle();
288 echo ' <div class="form-group clearfix">';
289 echo ' <div class="col-sm-12 text-left position-override">';
290 echo ' <button type="submit" class="btn btn-default btn-save">' . xlt('Authenticate TOTP') . '</button>';
299 // There is no key data yet or authentication failed, so we need to solicit it.
300 $requests = json_encode($u2f->getAuthenticateData($registrations));
301 // Persist the challenge also in the database because the browser is untrusted.
303 "UPDATE users_secure SET login_work_area = ? WHERE id = ?",
304 array($requests, $userid)
307 echo '<div class="container">';
308 echo '<div class="row">';
309 echo ' <div class="col-sm-12">';
310 echo ' <div class="page-header">';
311 echo ' <h2>' . xlt('U2F Key Verification') . '</h2>';
315 if ($errormsg && $errortype == "U2F") {
316 echo '<div class="row"><div class="col-sm-12"><div class="alert alert-danger alert-msg">' . text($errormsg) . '</div></div></div>';
318 echo '<div class="row">';
319 echo ' <div class="col-sm-12">';
320 echo ' <form method="post" name="u2fform" id="u2fform" action="main_screen.php?auth=login&site=' . attr_url($_GET['site']) . '" target="_top">';
322 echo ' <legend>'. xlt('Insert U2F Key') .'</legend>';
323 echo ' <div class="form-group">';
324 echo ' <div class="col-sm-6 col-sm-offset-3">';
326 echo ' <li>' . xlt('Insert your key into a USB port and click the Authenticate button below.') . '</li>';
327 echo ' <li>' . xlt('Then press the flashing button on your key within 1 minute.') . '</li>';
331 echo ' <div class="form-group clearfix">';
332 echo ' <div class="col-sm-12 text-left position-override">';
333 echo ' <button type="button" id="authutf" class="btn btn-default btn-save" onclick="doAuth()">' . xlt('Authenticate U2F') . '</button>';
334 echo ' <input type="hidden" name="form_requests" value="' . attr($requests) . '" />';
335 echo ' <input type="hidden" name="form_response" value="" />';
336 generate_html_middle();
343 exit(generate_html_end());
347 ///////////////////////////////////////////////////////////////////////
348 // End of U2F and APP Based TOTP logic.
349 ///////////////////////////////////////////////////////////////////////
351 // Creates a new session id when load this outer frame
352 // (allows creations of separate OpenEMR frames to view patients concurrently
353 // on different browser frame/windows)
354 // This session id is used below in the restoreSession.php include to create a
355 // session cookie for this specific OpenEMR instance that is then maintained
356 // within the OpenEMR instance by calling top.restoreSession() whenever
357 // refreshing or starting a new script.
358 if (isset($_POST['new_login_session_management'])) {
359 // This is a new login, so create a new session id and remove the old session
360 session_regenerate_id(true);
362 // This is not a new login, so check csrf and then create a new session id and do NOT remove the old session
363 if (!verifyCsrfToken($_POST["csrf_token_form"])) {
366 session_regenerate_id(false);
368 // Create the csrf_token
369 $_SESSION['csrf_token'] = createCsrfToken();
370 // Also create a api_csrf_token that is only used for the api
371 $_SESSION['api_csrf_token'] = createCsrfToken();
373 $_SESSION["encounter"] = '';
375 if ($GLOBALS['login_into_facility']) {
376 $facility_id = $_POST['facility'];
377 if ($facility_id === 'user_default') {
378 //get the default facility of login user from users table
379 $facilityService = new FacilityService();
380 $facility = $facilityService->getFacilityForUser($_SESSION['authUserID']);
381 $facility_id = $facility['id'];
383 $_SESSION['facilityId'] = $facility_id;
384 if ($GLOBALS['set_facility_cookie']) {
385 // set cookie with facility for the calender screens
386 setcookie("pc_facility", $_SESSION['facilityId'], time() +
(3600 * 365), $GLOBALS['webroot']);
390 // Fetch the password expiration date
392 if ($GLOBALS['password_expiration_days'] != 0) {
394 $q= (isset($_POST['authUser'])) ?
$_POST['authUser'] : '';
395 $result = sqlStatement("select pwd_expiration_date from users where username = ?", array($q));
396 $current_date = date('Y-m-d');
397 $pwd_expires_date = $current_date;
398 if ($row = sqlFetchArray($result)) {
399 $pwd_expires_date = $row['pwd_expiration_date'];
402 // Display the password expiration message (starting from 7 days before the password gets expired)
403 $pwd_alert_date = date('Y-m-d', strtotime($pwd_expires_date . '-7 days'));
405 if (strtotime($pwd_alert_date) != '' &&
406 strtotime($current_date) >= strtotime($pwd_alert_date) &&
407 (!isset($_SESSION['expiration_msg'])
408 or $_SESSION['expiration_msg'] == 0)) {
410 $_SESSION['expiration_msg'] = 1; // only show the expired message once
415 //display the php file containing the password expiration message.
416 $frame1url = "pwd_expires_alert.php?csrf_token_form=" . attr_url(collectCsrfToken());
417 $frame1target = "adm";
418 } elseif (!empty($_POST['patientID'])) {
419 $patientID = 0 +
$_POST['patientID'];
420 if (empty($_POST['encounterID'])) {
421 // Open patient summary screen (without a specific encounter)
422 $frame1url = "../patient_file/summary/demographics.php?set_pid=" . attr_url($patientID);
423 $frame1target = "pat";
425 // Open patient summary screen with a specific encounter
426 $encounterID = 0 +
$_POST['encounterID'];
427 $frame1url = "../patient_file/summary/demographics.php?set_pid=" . attr_url($patientID) . "&set_encounterid=" . attr_url($encounterID);
428 $frame1target = "pat";
430 } elseif (isset($_GET['mode']) && $_GET['mode'] == "loadcalendar") {
431 $frame1url = "calendar/index.php?pid=" . attr_url($_GET['pid']);
432 if (isset($_GET['date'])) {
433 $frame1url .= "&date=" . attr_url($_GET['date']);
436 $frame1target = "cal";
439 $map_paths_to_targets = array(
440 'main_info.php' => ('cal'),
441 '../new/new.php' => ('pat'),
442 '../../interface/main/finder/dynamic_finder.php' => ('fin'),
443 '../../interface/patient_tracker/patient_tracker.php?skip_timeout_reset=1' => ('flb'),
444 '../../interface/main/messages/messages.php?form_active=1' => ('msg')
446 if ($GLOBALS['default_top_pane']) {
447 $frame1url=attr($GLOBALS['default_top_pane']);
448 $frame1target = $map_paths_to_targets[$GLOBALS['default_top_pane']];
449 if (empty($frame1target)) {
450 $frame1target = "msc";
453 $frame1url = "main_info.php";
454 $frame1target = "cal";
456 if ($GLOBALS['default_second_tab']) {
457 $frame2url=attr($GLOBALS['default_second_tab']);
458 $frame2target = $map_paths_to_targets[$GLOBALS['default_second_tab']];
459 if (empty($frame2target)) {
460 $frame2target = "msc";
463 $frame2url = "../../interface/main/messages/messages.php?form_active=1";
464 $frame2target = "msg";
468 $nav_area_width = '130';
469 if (!empty($GLOBALS['gbl_nav_area_width'])) {
470 $nav_area_width = $GLOBALS['gbl_nav_area_width'];
473 // This is where will decide whether to use tabs layout or non-tabs layout
474 // Will also set Session variables to communicate settings to tab layout
475 if ($GLOBALS['new_tabs_layout']) {
476 $_SESSION['frame1url'] = $frame1url;
477 $_SESSION['frame1target'] = $frame1target;
478 $_SESSION['frame2url'] = $frame2url;
479 $_SESSION['frame2target'] = $frame2target;
480 // mdsupport - Apps processing invoked for valid app selections from list
481 if ((isset($_POST['appChoice'])) && ($_POST['appChoice'] !== '*OpenEMR')) {
482 $_SESSION['app1'] = $_POST['appChoice'];
485 // Pass a unique token, so main.php script can not be run on its own
486 $_SESSION['token_main_php'] = RandomGenUtils
::createUniqueToken();
487 header('Location: ' . $web_root . "/interface/main/tabs/main.php?token_main=" . urlencode($_SESSION['token_main_php']));
495 <?php
echo text($openemr_name) ?
>
497 <script type
="text/javascript" src
="<?php echo $GLOBALS['assets_static_relative']; ?>/jquery-1-9-1/jquery.min.js"></script
>
498 <script type
="text/javascript" src
="../../library/topdialog.js"></script
>
499 <script type
="text/javascript" src
="tabs/js/dialog_utils.js?v=<?php echo $v_js_includes; ?>"></script
>
501 <link rel
="shortcut icon" href
="<?php echo $GLOBALS['images_static_relative']; ?>/favicon.ico" />
503 <script language
='JavaScript'>
505 // Flag that tab mode is off
507 // some globals to access using top.variable
508 var userDebug
= <?php
echo js_escape($GLOBALS['user_debug']); ?
>;
509 var webroot_url
= <?php
echo js_escape($web_root); ?
>;
510 var jsLanguageDirection
= <?php
echo js_escape($_SESSION['language_direction']); ?
>;
512 <?php
require($GLOBALS['srcdir'] . "/restoreSession.php"); ?
>
514 // Since this should be the parent window, this is to prevent calls to the
515 // window that opened this window. For example when a new window is opened
516 // from the Patient Flow Board or the Patient Finder.
517 window
.opener
= null;
519 // This flag indicates if another window or frame is trying to reload the login
520 // page to this top-level window. It is set by javascript returned by auth.inc
521 // and is checked by handlers of beforeunload events.
522 var timed_out
= false;
524 // This counts the number of frames that have reported themselves as loaded.
525 // Currently only left_nav and Title do this, so the maximum will be 2.
526 // This is used to determine when those frames are all loaded.
527 var loadedFrameCount
= 0;
529 function allFramesLoaded() {
530 // Change this number if more frames participate in reporting.
531 return loadedFrameCount
>= 2;
539 * for RTL layout we need to change order of frames in framesets
541 $lang_dir = $_SESSION['language_direction'];
543 $sidebar_tpl = "<frameset rows='*,0' frameborder='0' border='0' framespacing='0'>
544 <frame src='left_nav.php' name='left_nav' />
545 <frame src='daemon_frame.php' name='Daemon' scrolling='no' frameborder='0'
546 border='0' framespacing='0' />
549 $main_tpl = "<frameset rows='60%,*' id='fsright' bordercolor='#999999' frameborder='1'>" ;
550 $main_tpl .= "<frame src='". $frame1url ."' name='RTop' scrolling='auto' />
551 <frame src='messages/messages.php?form_active=1' name='RBot' scrolling='auto' /></frameset>";
553 // Please keep in mind that border (mozilla) and framespacing (ie) are the
554 // same thing. use both.
555 // frameborder specifies a 3d look, not whether there are borders.
557 if (empty($GLOBALS['gbl_tall_nav_area'])) {
558 // not tall nav area ?>
559 <frameset rows
='<?php echo attr($GLOBALS['titleBarHeight
']) + 5 ?>,*' frameborder
='1' border
='1' framespacing
='1' onunload
='imclosing()'>
560 <frame src
='main_title.php' name
='Title' scrolling
='no' frameborder
='1' noresize
/>
561 <?php
if ($lang_dir != 'rtl') { ?
>
562 <frameset cols
='<?php echo attr($nav_area_width) . ',*'; ?>' id
='fsbody' frameborder
='1' border
='4' framespacing
='4'>
563 <?php
echo $sidebar_tpl ?
>
564 <?php
echo $main_tpl ?
>
568 <frameset cols
='<?php echo '*,' . attr($nav_area_width); ?>' id
='fsbody' frameborder
='1' border
='4' framespacing
='4'>
569 <?php
echo $main_tpl ?
>
570 <?php
echo $sidebar_tpl ?
>
578 <?php
} else { // use tall nav area ?>
579 <frameset cols
='<?php echo attr($nav_area_width); ?>,*' id
='fsbody' frameborder
='1' border
='4' framespacing
='4' onunload
='imclosing()'>
580 <frameset rows
='*,0' frameborder
='0' border
='0' framespacing
='0'>
581 <frame src
='left_nav.php' name
='left_nav' />
582 <frame src
='daemon_frame.php' name
='Daemon' scrolling
='no' frameborder
='0'
583 border
='0' framespacing
='0' />
585 <frameset rows
='<?php echo attr($GLOBALS['titleBarHeight
']) + 5 ?>,*' frameborder
='1' border
='1' framespacing
='1'>
586 <frame src
='main_title.php' name
='Title' scrolling
='no' frameborder
='1' />
587 <frameset rows
='60%,*' id
='fsright' bordercolor
='#999999' frameborder
='1' border
='4' framespacing
='4'>
588 <frame src
='<?php echo $frame1url ?>' name
='RTop' scrolling
='auto' />
589 <frame src
='messages/messages.php?form_active=1' name
='RBot' scrolling
='auto' />
594 <?php
} // end tall nav area ?>