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>
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...
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']) {
52 $logoarea .= " order-md-2";
53 $formarea .= " order-md-1";
57 $logoarea .= " order-md-1";
58 $formarea .= " order-md-2";
62 $logoarea .= " order-1";
63 $formarea .= " col-12";
64 $loginrow .= " login-row-center";
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();
79 <?php Header
::setupHeader(); ?
>
81 <title
><?php
echo xlt('OpenEMR App Registration'); ?
></title
>
91 (function(window
, fhirRegistrationURL
) {
92 function registerApp() {
93 let form
= document
.querySelector('form[name="app_form]');
95 "application_type": "private"
97 ,"initiate_login_uri": ""
98 ,"post_logout_redirect_uris": []
100 ,"token_endpoint_auth_method": "client_secret_post"
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() != "") {
119 appRegister
.jwks
= JSON
.parse(appRegister
.jwks
);
122 console
.error(error
);
123 alert(<?php
echo xlj("Your JWKS is invalid"); ?
>);
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\
//)) {
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.
146 'Content-Type': 'application/json'
148 body
: JSON
.stringify(appRegister
) // body data type must match "Content-Type" header
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
;
162 console
.error(error
);
163 let msgText
= error
.message
;
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
;
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
);
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
);
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
)
213 if (node
.parentNode
.classList
.contains('d-none')) {
214 node
.parentNode
.classList
.remove("d-none");
217 function togglePatientTypeFields(event
) {
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
)
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
) {
274 document
.getElementById('systemSetup').classList
.remove("d-none");
275 document
.querySelectorAll("input[value^='system/']").forEach(showNodeFunction
);
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); ?
>);
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"); ?
>
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; ?
>
321 <div
class="mx-auto m-4 w-75">
322 <?php
echo file_get_contents($GLOBALS['images_static_absolute'] . "/login-logo.svg"); ?
>
325 <?php
if ($GLOBALS['show_label_login']) { ?
>
326 <div
class="text-center login-title-label">
327 <?php
echo text($openemr_name); ?
>
331 // Figure out how to display the tiny logos
332 $t1 = $GLOBALS['tiny_logo_1'];
333 $t2 = $GLOBALS['tiny_logo_2'];
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
>
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
>
347 <div
class="<?php echo $formarea; ?>">
348 <h3
class="card-title text-center"><?php
echo xlt("App Registration Form"); ?
></h3
>
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
>
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
>
365 <h2
><?php
echo xlt("Application Context"); ?
></h2
>
368 <div
class="row pl-3 pr-3">
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
>
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
>
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
>
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
>
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
>
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'); ?>" />
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'); ?>" />
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'); ?>" />
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'); ?>" />
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'); ?>" />
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); ?
>
429 <div
class="row" id
="systemSetup">
431 <h3
class="text-center"><?php
echo xlt("The following items are required for System Scopes"); ?
></h3
>
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'); ?>" />
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
>
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();" />
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
>
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
>
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); ?>" />
462 <div
class="form-group errorResponse hidden">
463 <div id
="errorResponseContainer">