Update mfa_totp.php (#6916)
[openemr.git] / interface / smart / register-app.php
blobd64f43779b78437760000b2c43bb926077c7f3ff
1 <?php
3 /**
4 * Login screen.
6 * @package OpenEMR
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 Kevin Yeh <kevin.y@integralemr.com>
11 * @author Scott Wakefield <scott.wakefield@gmail.com>
12 * @author ViCarePlus <visolve_emr@visolve.com>
13 * @author Julia Longtin <julialongtin@diasp.org>
14 * @author cfapress
15 * @author markleeds
16 * @author Tyler Wrenn <tyler@tylerwrenn.com>
17 * @author Stephen Nielson <stephen@nielson.org>
18 * @copyright Copyright (c) 2019 Brady Miller <brady.g.miller@gmail.com>
19 * @copyright Copyright (c) 2020 Tyler Wrenn <tyler@tylerwrenn.com>
20 * @copyright Copyright (c) 2020 Stephen Nielson <stephen@nielson.org>
21 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
24 use OpenEMR\Core\Header;
25 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\ScopeRepository;
26 use OpenEMR\FHIR\Config\ServerConfig;
27 use OpenEMR\RestControllers\AuthorizationController;
29 // not sure if we need the site id or not...
30 $ignoreAuth = true;
31 require_once("../globals.php");
32 require_once("./../../_rest_config.php");
34 // exit if fhir api is not turned on
35 if (empty($GLOBALS['rest_fhir_api'])) {
36 die(xlt("Not Authorized"));
39 // This code allows configurable positioning in the login page
40 $logoarea = "py-2 px-2 py-md-3 px-md-5 order-1 bg-primary";
41 $formarea = "py-3 px-2 p-sm-5 bg-white order-2";
42 $loginrow = "row login-row bg-white shadow-lg align-items-center my-sm-5";
44 // Apply these classes to the logo area if the login page is left or right
45 $lrArr = ['left', 'right'];
46 $logoarea .= (in_array($GLOBALS['login_page_layout'], $lrArr)) ? " col-md-6" : " col-md-12";
47 $formarea .= (in_array($GLOBALS['login_page_layout'], $lrArr)) ? " col-md-6" : " col-md-12";
49 // More finite control on a per-setting basis
50 switch ($GLOBALS['login_page_layout']) {
51 case 'left':
52 $logoarea .= " order-md-2";
53 $formarea .= " order-md-1";
54 break;
56 case 'right':
57 $logoarea .= " order-md-1";
58 $formarea .= " order-md-2";
59 break;
61 default:
62 $logoarea .= " order-1";
63 $formarea .= " col-12";
64 $loginrow .= " login-row-center";
65 break;
68 // TODO: adunsulag find out where our openemr name comes from
69 $openemr_name = $openemr_name ?? '';
71 $scopeRepo = new ScopeRepository(RestConfig::GetInstance());
72 $scopes = $scopeRepo->getCurrentSmartScopes();
73 // TODO: adunsulag there's gotta be a better way for this url...
74 $fhirRegisterURL = AuthorizationController::getAuthBaseFullURL() . AuthorizationController::getRegistrationPath();
75 $audienceUrl = (new ServerConfig())->getFhirUrl();
77 <html>
78 <head>
79 <?php Header::setupHeader(); ?>
81 <title><?php echo xlt('OpenEMR App Registration'); ?></title>
82 <style>
83 .hidden {
84 display: none;
86 .errorResponse {
87 color: red;
89 </style>
90 <script>
91 (function(window, fhirRegistrationURL) {
92 function registerApp() {
93 let form = document.querySelector('form[name="app_form]');
94 let appRegister = {
95 "application_type": "private"
96 ,"redirect_uris": []
97 ,"initiate_login_uri": ""
98 ,"post_logout_redirect_uris": []
99 ,"client_name": ""
100 ,"token_endpoint_auth_method": "client_secret_post"
101 ,"contacts": []
102 ,"scope": []
103 ,"jwks_uri": ""
104 ,"jwks": ""
106 appRegister.client_name = document.querySelector('#appName').value;
107 let redirect_uri = document.querySelector("#redirectUri").value;
108 appRegister.redirect_uris.push(redirect_uri);
109 // not sure we need logout redirect right now
110 appRegister.post_logout_redirect_uris.push(document.querySelector("#logoutURI").value);
111 appRegister.initiate_login_uri = document.querySelector("#launchUri").value;
112 appRegister.contacts.push(document.querySelector("#contactEmail").value);
113 appRegister.jwks_uri = document.querySelector("#jwksUri").value;
114 appRegister.jwks = document.querySelector("#jwks").value;
115 appRegister.application_type = document.querySelector("input[name='appType']:checked").value || "private";
117 if (appRegister.jwks.trim() != "") {
118 try {
119 appRegister.jwks = JSON.parse(appRegister.jwks);
121 catch (error) {
122 console.error(error);
123 alert(<?php echo xlj("Your JWKS is invalid"); ?>);
124 return;
128 let scopes = [];
129 let scopeInputs = document.querySelectorAll('input.app-scope:checked');
130 for (let scope of scopeInputs) {
131 if (appRegister.application_type != 'private')
133 // if we are not a private app don't let system scopes be granted
134 // NOTE: this is just a convenience as the server prevents it too.
135 if (scope.value.match(/^system\//)) {
136 continue;
139 scopes.push(scope.value);
141 appRegister.scope = scopes.join(" "); // combine the scopes selected.
143 fetch(fhirRegistrationURL, {
144 method: 'POST', // *GET, POST, PUT, DELETE, etc.
145 headers: {
146 'Content-Type': 'application/json'
148 body: JSON.stringify(appRegister) // body data type must match "Content-Type" header
149 }).then(result => {
150 if (!result.ok) {
151 return result.json().then(json => { throw json });
153 return result.json();
154 }).then(resultJSON => {
155 console.log(resultJSON);
156 document.querySelector(".apiResponse").classList.remove("hidden");
157 document.querySelector(".errorResponse").classList.add("hidden");
158 document.querySelector("#clientID").value = resultJSON.client_id;
159 document.querySelector("#clientSecretID").value = resultJSON.client_secret;
161 .catch(error => {
162 console.error(error);
163 let msgText = error.message;
164 if (!msgText) {
165 msgText = JSON.stringify(error);
167 document.querySelector(".apiResponse").classList.add("hidden");
168 document.querySelector(".errorResponse").classList.remove("hidden");
169 document.querySelector("#errorResponseContainer").textContent = msgText;
171 return false;
174 function toggleSelectAll(evt) {
175 let target = evt.target;
176 let hiddenClass = 'd-none';
177 let alternateSelector = target.classList.contains('toggle-on') ? 'toggle-off' : 'toggle-on';
178 let alternate = document.querySelector('.select-all-toggle.' + alternateSelector);
180 if (!alternate) {
181 throw new Error("Alternate dom element missing for id '.select-all-toggle." + alternateSelector + "'");
184 if (target.classList.contains(hiddenClass)) {
185 target.classList.remove(hiddenClass);
186 alternate.classList.add(hiddenClass);
187 } else {
188 target.classList.add(hiddenClass);
189 alternate.classList.remove(hiddenClass);
192 let inputs = document.querySelectorAll('input.app-scope');
193 let isChecked = target.classList.contains('toggle-on') ? true : false;
194 for (let scope of inputs) {
195 scope.checked = isChecked;
198 function hideNodeFunction(node)
200 if (node.checked !== undefined)
202 node.checked = false;
204 node.parentNode.classList.add("d-none");
206 function showNodeFunction(node)
208 if (node.checked !== undefined)
210 node.checked = true;
213 if (node.parentNode.classList.contains('d-none')) {
214 node.parentNode.classList.remove("d-none");
217 function togglePatientTypeFields(event) {
218 if (!event.target)
220 return;
222 let val = event.target.value;
223 if (val == 'single') {
224 document.querySelectorAll("input[value^='user/']").forEach(hideNodeFunction);
225 document.querySelectorAll("input[value^='patient/']").forEach(showNodeFunction);
226 toggleSystemFunctionality(false);
227 } else if (val == 'multiple') {
228 toggleSystemFunctionality(false);
229 document.querySelectorAll("input[value^='user/']").forEach(showNodeFunction);
230 document.querySelectorAll("input[value^='patient/']").forEach(hideNodeFunction);
231 } else if (val == 'client') {
232 document.querySelectorAll("input[value^='user/']").forEach(hideNodeFunction);
233 document.querySelectorAll("input[value^='patient/']").forEach(hideNodeFunction);
234 toggleSystemFunctionality(true);
235 } else if (val == 'all') {
237 let selected =document.querySelector("input[name='appType']:checked");
238 if (selected && selected.value == "private") {
239 toggleSystemFunctionality(true);
241 document.querySelectorAll("input[value^='user/']").forEach(showNodeFunction);
242 document.querySelectorAll("input[value^='patient/']").forEach(showNodeFunction);
245 function toggleAppTypeFields(event)
247 if (!event.target)
249 return;
251 let val = event.target.value;
253 if (val === 'private')
255 toggleSystemFunctionality(true);
256 // document.querySelectorAll("input[value='offline_access']").forEach(showNodeFunction);
257 document.querySelectorAll("#clientSecretID").forEach(showNodeFunction);
258 document.querySelectorAll("#patientTypeClient").forEach(showNodeFunction);
259 document.querySelectorAll("label[for='patientTypeClient']").forEach(showNodeFunction);
262 else if (val == 'public')
264 toggleSystemFunctionality(false);
265 // document.querySelectorAll("input[value='offline_access']").forEach(hideNodeFunction);
266 document.querySelectorAll("#clientSecretID").forEach(hideNodeFunction);
267 document.querySelectorAll("#patientTypeClient").forEach(hideNodeFunction);
268 document.querySelectorAll("label[for='patientTypeClient']").forEach(hideNodeFunction);
272 function toggleSystemFunctionality(enabled) {
273 if (enabled) {
274 document.getElementById('systemSetup').classList.remove("d-none");
275 document.querySelectorAll("input[value^='system/']").forEach(showNodeFunction);
276 } else {
277 document.querySelectorAll("input[value^='system/']").forEach(hideNodeFunction);
278 document.getElementById('systemSetup').classList.add("d-none");
282 window.addEventListener('load', function() {
283 var scopeSelectAll = document.querySelectorAll('.select-all-toggle');
284 for (var element of scopeSelectAll) {
285 element.addEventListener('click', toggleSelectAll);
288 var appTypes = document.querySelectorAll("input[name='appType']");
289 for (var element of appTypes)
291 element.addEventListener('click', toggleAppTypeFields);
294 var patientTypes = document.querySelectorAll("input[name='patientType']");
295 for (var element of patientTypes)
297 element.addEventListener('click', togglePatientTypeFields);
300 document.querySelector('#submit').addEventListener('click', registerApp);
302 })(window, <?php echo js_escape($fhirRegisterURL); ?>);
303 </script>
304 </head>
305 <body class="register-app">
306 <form id="app_form" method="POST" autocomplete="off">
307 <div class="<?php echo $loginrow; ?> card m-5">
308 <div class="<?php echo attr($logoarea); ?>">
309 <?php $extraLogo = $GLOBALS['extra_logo_login']; ?>
310 <?php if ($extraLogo) { ?>
311 <div class="text-center">
312 <span class="d-inline-block w-40">
313 <?php echo file_get_contents($GLOBALS['images_static_absolute'] . "/login-logo.svg"); ?>
314 </span>
315 <span class="d-inline-block w-15 login-bg-text-color"><i class="fas fa-plus fa-2x"></i></span>
316 <span class="d-inline-block w-40">
317 <?php echo $logocode; ?>
318 </span>
319 </div>
320 <?php } else { ?>
321 <div class="mx-auto m-4 w-75">
322 <?php echo file_get_contents($GLOBALS['images_static_absolute'] . "/login-logo.svg"); ?>
323 </div>
324 <?php } ?>
325 <?php if ($GLOBALS['show_label_login']) { ?>
326 <div class="text-center login-title-label">
327 <?php echo text($openemr_name); ?>
328 </div>
329 <?php } ?>
330 <?php
331 // Figure out how to display the tiny logos
332 $t1 = $GLOBALS['tiny_logo_1'];
333 $t2 = $GLOBALS['tiny_logo_2'];
334 if ($t1 && !$t2) {
335 echo $tinylogocode1;
336 } if ($t2 && !$t1) {
337 echo $tinylogocode2;
338 } if ($t1 && $t2) { ?>
339 <div class="row mb-3">
340 <div class="col-sm-6"><?php echo $tinylogocode1;?></div>
341 <div class="col-sm-6"><?php echo $tinylogocode2;?></div>
342 </div>
343 <?php } ?>
344 <p class="text-center lead font-weight-normal login-bg-text-color text-white"><?php echo xlt('The most popular open-source Electronic Health Record and Medical Practice Management solution.'); ?></p>
345 <p class="text-center small"><a href="../../acknowledge_license_cert.html" class="login-bg-text-color text-white" target="main"><?php echo xlt('Acknowledgments, Licensing and Certification'); ?></a></p>
346 </div>
347 <div class="<?php echo $formarea; ?>">
348 <h3 class="card-title text-center"><?php echo xlt("App Registration Form"); ?></h3>
349 <div>
350 <div class="row">
351 <div class="col">
352 <h2><?php echo xlt("Application Type"); ?></h2>
353 <p><?php echo xlt("Confidential clients must be able to securely safeguard a secret."); ?></p>
354 <p><?php echo xlt("If your application cannot keep a secret (such as an application that runs in a web browser) you should use the public application type."); ?></p>
355 </div>
356 </div>
357 <div class="form-check form-check-inline">
358 <input type="radio" class="form-check-input" id="appTypeConfidential" name="appType" value="private" checked="checked"/>
359 <label for="appTypeConfidential" class="form-check-label pr-2"><?php echo xlt('Confidential'); ?></label>
360 <input type="radio" class="form-check-input" id="appTypePublic" name="appType" value="public"/>
361 <label for="appTypePublic" class="form-check-label"><?php echo xlt('Public'); ?></label>
362 </div>
363 <div class="row">
364 <div class="col">
365 <h2><?php echo xlt("Application Context"); ?></h2>
366 </div>
367 </div>
368 <div class="row pl-3 pr-3">
369 <div class="col">
370 <input type="radio" class="form-check-input" id="patientTypeSingle" name="patientType" value="single"/>
371 <label for="patientTypeSingle" class="form-check-label pr-3"><?php echo xlt('Single Patient Application'); ?></label>
372 </div>
373 <div class="col">
374 <input type="radio" class="form-check-input" id="patientTypeMultiple" name="patientType" value="multiple"/>
375 <label for="patientTypeMultiple" class="form-check-label pr-3"><?php echo xlt('Multiple Patients Application'); ?></label>
376 </div>
377 <div class="col">
378 <input type="radio" class="form-check-input" id="patientTypeClient" name="patientType" value="client"/>
379 <label for="patientTypeClient" class="form-check-label pr-3"><?php echo xlt('System Client Application'); ?></label>
380 </div>
381 <div class="col">
382 <input type="radio" class="form-check-input" id="patientTypeAll" name="patientType" value="all" checked="checked"/>
383 <label for="patientTypeAll" class="form-check-label"><?php echo xlt('Multipurpose Application'); ?></label>
384 </div>
385 </div>
386 <div class="row">
387 <div class="col alert alert-info">
388 <p><?php echo xlt("system, user, and offline_access scopes require confidential app permissions."); ?></p>
389 <p><?php echo xlt("Confidential apps are applications that are able to safely and securely store a secret. Browser based and many mobile applications do not satisfy this security constraint"); ?></p>
390 </div>
391 </div>
392 <div class="form-group">
393 <label for="appName" class="text-right"><?php echo xlt('App Name'); ?>:</label>
394 <input type="text" class="form-control" id="appName" name="appName" placeholder="<?php echo xla('App Name'); ?>" />
395 </div>
396 <div class="form-group">
397 <label for="contactEmail" class="text-right"><?php echo xlt('Contact Email'); ?>:</label>
398 <input type="text" class="form-control" id="contactEmail" name="contactEmail" placeholder="<?php echo xla('Email'); ?>" />
399 </div>
400 <div class="form-group">
401 <label for="redirectUri" class="text-right"><?php echo xlt('App Redirect URI'); ?>:</label>
402 <input type="text" class="form-control" id="redirectUri" name="redirectUri" placeholder="<?php echo xla('URI'); ?>" />
403 </div>
404 <div class="form-group">
405 <label for="launchUri" class="text-right"><?php echo xlt('App Launch URI'); ?>:</label>
406 <input type="text" class="form-control" id="launchUri" name="launchUri" placeholder="<?php echo xla('URI'); ?>" />
407 </div>
408 <div class="form-group">
409 <label for="logoutURI" class="text-right"><?php echo xlt('App Logout URI'); ?>:</label>
410 <input type="text" class="form-control" id="logoutURI" name="logoutURI" placeholder="<?php echo xla('URI'); ?>" />
411 </div>
412 <!-- TODO: adunsulag display the list of scopes that can be requested here -->
414 <div class="form-group">
415 <?php echo xlt("Scopes Requested"); ?>:
416 <input type="button" class="select-all-toggle toggle-on btn btn-secondary d-none" value="<?php echo xlt('Select all'); ?>" />
417 <input type="button" class="select-all-toggle toggle-off btn btn-secondary" value="<?php echo xlt('Unselect all'); ?>" />
418 <input type="button" class="select-single-patient btn btn-secondary d-none" value="<?php echo xlt('Single Patient Application'); ?>" />
419 <input type="button" class="select-multi-patient btn btn-secondary d-none" value="<?php echo xlt('Multiple Patients Application'); ?>" />
420 <div class="list-group">
421 <?php foreach ($scopes as $scope) : ?>
422 <label class="list-group-item m-0">
423 <input type="checkbox" class='app-scope' name="scope[<?php echo attr($scope); ?>]" value="<?php echo attr($scope); ?>" checked>
424 <?php echo xlt($scope); ?>
425 </label>
426 <?php endforeach; ?>
427 </div>
428 </div>
429 <div class="row" id="systemSetup">
430 <div class="col">
431 <h3 class="text-center"><?php echo xlt("The following items are required for System Scopes"); ?></h3>
432 <hr />
433 <div class="form-group">
434 <label for="jwksUri" class="text-right"><?php echo xlt('JSON Web Key Set URI'); ?>:</label>
435 <input type="text" class="form-control" id="jwksUri" name="jwksUri" placeholder="<?php echo xla('URI'); ?>" />
436 </div>
437 <div class="form-group">
438 <label for="jwks" class="text-right"><?php echo xlt('JSON Web Key Set (Note a hosted web URI is preferred and this feature may be removed in future SMART versions)'); ?>:</label>
439 <textarea class="form-control" id="jwks" name="jwks" rows="5"></textarea>
440 </div>
441 </div>
442 </div>
444 <div class="form-group">
445 <input type="button" class="form-control btn btn-primary" id="submit" name="submit" value="<?php echo xla('Submit'); ?>" (onClick)="registerApp();" />
446 </div>
448 <div class="apiResponse hidden">
449 <div class="form-group">
450 <label for="clientID" class="text-right"><?php echo xlt('Client APP ID:'); ?></label>
451 <textarea class="form-control" id="clientID" name="clientID"></textarea>
452 </div>
453 <div class="form-group">
454 <label for="clientSecretID" class="text-right"><?php echo xlt('Client Secret APP ID:'); ?></label>
455 <textarea class="form-control" id="clientSecretID" name="clientSecretID"></textarea>
456 </div>
457 <div class="form-group">
458 <label for="audURL" class="text-right"><?php echo xlt('Aud URI (use this in the "aud" claim of your JWT)'); ?></label>
459 <input type="text" disabled class="form-control" id="audURL" name="audURL" value="<?php echo attr($audienceUrl); ?>" />
460 </div>
461 </div>
462 <div class="form-group errorResponse hidden">
463 <div id="errorResponseContainer">
464 </div>
465 </div>
466 </div>
467 </div>
468 </div>
469 </form>
470 </body>
471 </html>