7 * @link http://www.open-emr.org
8 * @author Matthew Vita <matthewvita48@gmail.com>
9 * @author Jerry Padgett <sjpadgett@gmail.com>
10 * @author Brady Miller <brady.g.miller@gmail.com>
11 * @copyright Copyright (c) 2018 Matthew Vita <matthewvita48@gmail.com>
12 * @copyright Copyright (c) 2020 Jerry Padgett <sjpadgett@gmail.com>
13 * @copyright Copyright (c) 2019-2020 Brady Miller <brady.g.miller@gmail.com>
14 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
17 // below brings in autoloader
18 require_once("./../_rest_config.php");
20 use OpenEMR\Common\Auth\UuidUserAccount
;
21 use OpenEMR\Common\Csrf\CsrfUtils
;
22 use OpenEMR\Common\Http\HttpRestRouteHandler
;
23 use OpenEMR\Common\Http\HttpRestRequest
;
24 use OpenEMR\Common\Logging\SystemLogger
;
25 use OpenEMR\Common\Session\SessionUtil
;
26 use OpenEMR\Common\Uuid\UuidRegistry
;
27 use OpenEMR\FHIR\SMART\SmartLaunchController
;
28 use OpenEMR\Events\RestApiExtend\RestApiCreateEvent
;
29 use Psr\Http\Message\ResponseInterface
;
31 $gbl = RestConfig
::GetInstance();
32 $restRequest = new HttpRestRequest($gbl, $_SERVER);
35 // Parse needed information from Redirect or REQUEST_URI
36 $resource = $gbl::getRequestEndPoint();
37 $logger = new SystemLogger();
38 $logger->debug("dispatch.php requested", ["resource" => $resource, "method" => $_SERVER['REQUEST_METHOD']]);
41 if (!empty($_SERVER['HTTP_APICSRFTOKEN'])) {
42 // Calling api from within the same session (ie. isLocalApi) since a apicsrftoken header was passed
47 } elseif ($gbl::skipApiAuth($resource)) {
48 // For rest api endpoints that do not require auth, such as the capability statement
49 // note that the site is validated in the skipApiAuth() function
51 $restRequest->setRequestSite($gbl::$SITE);
53 $_GET['site'] = $gbl::$SITE;
58 // Calling api via rest
59 // ensure token is valid
60 $tokenRaw = $gbl::verifyAccessToken();
61 if ($tokenRaw instanceof ResponseInterface
) {
62 $logger->error("dispatch.php failed token verify for resource", ["resource" => $resource]);
63 // failed token verify
64 // not a request object so send the error as response obj
65 $gbl::emitResponse($tokenRaw);
69 // collect token attributes
70 $attributes = $tokenRaw->getAttributes();
74 $scopes = $attributes['oauth_scopes'];
75 $logger->debug("Parsed oauth_scopes in AccessToken", ["scopes" => $scopes]);
76 foreach ($scopes as $attr) {
77 if (stripos($attr, 'site:') !== false) {
78 $site = str_replace('site:', '', $attr);
79 $restRequest->setRequestSite($site);
82 // set our scopes and updated resources as needed
83 $restRequest->setAccessTokenScopes($scopes);
85 // ensure 1) sane site 2) site from gbl and access token are the same and 3) ensure the site exists on filesystem
87 empty($restRequest->getRequestSite()) ||
empty($gbl::$SITE) ||
preg_match('/[^A-Za-z0-9\\-.]/', $gbl::$SITE)
88 ||
($restRequest->getRequestSite() !== $gbl::$SITE) ||
!file_exists(__DIR__
. '/../sites/' . $gbl::$SITE)
90 $logger->error("OpenEMR Error - api site error, so forced exit");
91 http_response_code(400);
95 $_GET['site'] = $site;
97 // set the scopes globals for endpoint permission checking
98 $GLOBALS['oauth_scopes'] = $scopes;
100 // collect openemr user uuid
101 $userId = $attributes['oauth_user_id'];
102 // collect client id (will be empty for PKCE)
103 $clientId = $attributes['oauth_client_id'] ??
null;
105 $tokenId = $attributes['oauth_access_token_id'];
106 // ensure user uuid and token id are populated
107 if (empty($userId) ||
empty($tokenId)) {
108 $logger->error("OpenEMR Error - userid or tokenid not available, so forced exit", ['attributes' => $attributes]);
109 http_response_code(400);
112 $restRequest->setClientId($clientId);
113 $restRequest->setAccessTokenId($tokenId);
115 // Get a site id from initial login authentication.
117 $skipApiAuth = false;
121 // set the route as well as the resource information. Note $resource is actually the route and not the resource name.
122 //$restRequest->setRequestPath($resource);
123 $resource = $restRequest->getRequestPath();
126 // Will start the api OpenEMR session/cookie.
127 SessionUtil
::apiSessionStart($gbl::$web_root);
130 $GLOBALS['is_local_api'] = $isLocalApi;
131 $restRequest->setIsLocalApi($isLocalApi);
133 // Set $sessionAllowWrite to true here for following reasons:
134 // 1. !$isLocalApi - not applicable since use the SessionUtil::apiSessionStart session, which was set above
135 // 2. $isLocalApi - in this case, basically setting this to true downstream after some session sets via session_write_close() call
136 $sessionAllowWrite = true;
137 require_once("./../interface/globals.php");
139 // we now can check the database to see if the token is revoked
140 // Note despite League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator.php:L117 already checking for revoked
141 // access token we have to do this logic here as we use the access token SCOPE parameter to determine our multi-site setting
142 // and load up the correct database, our earlier access token logic returns false for revoked as we don't have db access
143 // for that reason we have this double check on validating the access token.
144 if (!empty($tokenId)) {
145 $result = $gbl::validateAccessTokenRevoked($tokenId);
146 if ($result instanceof ResponseInterface
) {
147 $logger->error("dispatch.php access token was revoked", ["resource" => $resource]);
148 // failed token verify
149 // not a request object so send the error as response obj
150 $gbl::emitResponse($result);
156 // recollect this so the DEBUG global can be used if set
157 $logger = new SystemLogger();
159 $gbl::$apisBaseFullUrl = $GLOBALS['site_addr_oath'] . $GLOBALS['webroot'] . "/apis/" . $gbl::$SITE;
160 $restRequest->setApiBaseFullUrl($gbl::$apisBaseFullUrl);
163 // need to check for csrf match when using api locally
166 if (empty($_SERVER['HTTP_APICSRFTOKEN'])) {
167 $logger->error("OpenEMR Error: internal api failed because csrf token not received");
171 if ((!$csrfFail) && (!CsrfUtils
::verifyCsrfToken($_SERVER['HTTP_APICSRFTOKEN'], 'api'))) {
172 $logger->error("OpenEMR Error: internal api failed because csrf token did not match");
177 $logger->error("dispatch.php CSRF failed", ["resource" => $resource]);
178 http_response_code(401);
181 } elseif ($skipApiAuth) {
182 $logger->debug("dispatch.php skipping api auth");
183 // For endpoints that do not require auth, such as the capability statement
185 $logger->debug("dispatch.php authenticating user");
186 // verify that user tokens haven't been revoked.
187 // this is done by verifying the user is trusted with active auth session.
188 $isTrusted = $gbl::isTrustedUser($attributes["oauth_client_id"], $attributes["oauth_user_id"]);
189 if ($isTrusted instanceof ResponseInterface
) {
190 $logger->debug("dispatch.php oauth2 inactive user api attempt");
191 // user is not logged on to server with an active session.
192 // too me this is easier than revoking tokens or using phantom tokens.
193 // give a 400(unsure here, could be a 401) so client can redirect to server.
194 $gbl::destroySession();
195 $gbl::emitResponse($isTrusted);
198 // $isTrusted can be used for further validations using session_cache
199 // which is a json. json_decode($isTrusted['session_cache'])
201 // authenticate the token
202 if (!$gbl->authenticateUserToken($tokenId, $clientId, $userId)) {
203 $logger->error("dispatch.php api call with invalid token");
204 $gbl::destroySession();
205 http_response_code(401);
209 // collect user information and user role
210 $uuidToUser = new UuidUserAccount($userId);
211 $user = $uuidToUser->getUserAccount();
212 $userRole = $uuidToUser->getUserRole();
214 // unable to identify the users user role
215 $logger->error("OpenEMR Error - api user account could not be identified, so forced exit", [
217 'userRole' => $uuidToUser->getUserRole()]);
218 $gbl::destroySession();
219 http_response_code(400);
222 if (empty($userRole)) {
223 // unable to identify the users user role
224 $logger->error("OpenEMR Error - api user role for user could not be identified, so forced exit");
225 $gbl::destroySession();
226 http_response_code(400);
230 $restRequest->setAccessTokenId($tokenId);
231 $restRequest->setRequestUserRole($userRole);
232 $restRequest->setRequestUser($userId, $user);
234 // verify that the scope covers the route
236 // fhir routes are the default and can send openid/fhirUser w/ authorization_code, or no scopes at all
237 // with Client Credentials, so we only reject requests for standard or portal if the correct scope is not
239 ($gbl::is_api_request($resource) && !in_array('api:oemr', $GLOBALS['oauth_scopes'])) ||
240 ($gbl::is_portal_request($resource) && !in_array('api:port', $GLOBALS['oauth_scopes']))
242 $logger->error("dispatch.php api call with token that does not cover the requested route");
243 $gbl::destroySession();
244 http_response_code(401);
247 // ensure user role has access to the resource
249 // users has access to oemr and fhir
250 // patient has access to port and fhir
251 if ($userRole == 'users' && ($gbl::is_api_request($resource) ||
$gbl::is_fhir_request($resource))) {
252 $logger->debug("dispatch.php valid role and user has access to api/fhir resource", ['resource' => $resource]);
254 } elseif ($userRole == 'patient' && ($gbl::is_portal_request($resource) ||
$gbl::is_fhir_request($resource))) {
255 $logger->debug("dispatch.php valid role and patient has access portal resource", ['resource' => $resource]);
257 } elseif ($userRole === 'system' && ($gbl::is_fhir_request($resource))) {
258 $logger->debug("dispatch.php valid role and system has access to api/fhir resource", ['resource' => $resource]);
260 $logger->error("OpenEMR Error: api failed because user role does not have access to the resource", ['resource' => $resource, 'userRole' => $userRole]);
261 $gbl::destroySession();
262 http_response_code(401);
265 // set pertinent session variables
266 if ($userRole == 'users') {
267 $_SESSION['authUser'] = $user["username"] ??
null;
268 $_SESSION['authUserID'] = $user["id"] ??
null;
269 $_SESSION['authProvider'] = sqlQueryNoLog("SELECT `name` FROM `groups` WHERE `user` = ?", [$_SESSION['authUser']])['name'] ??
null;
270 if (empty($_SESSION['authUser']) ||
empty($_SESSION['authUserID']) ||
empty($_SESSION['authProvider'])) {
271 // this should never happen
272 $logger->error("OpenEMR Error: api failed because unable to set critical users session variables");
273 $gbl::destroySession();
274 http_response_code(401);
277 $logger->debug("dispatch.php request setup for user role", ['authUserID' => $user['id'], 'authUser' => $user['username']]);
279 $restRequest->requestHasScope(SmartLaunchController
::CLIENT_APP_STANDALONE_LAUNCH_SCOPE
)
280 ||
$restRequest->requestHasScope(SmartLaunchController
::CLIENT_APP_REQUIRED_LAUNCH_SCOPE
)
282 $logger->debug("dispatch.php api is userRole populating token context for request due to smart launch scope");
283 $restRequest = $gbl->populateTokenContextForRequest($restRequest);
285 } elseif ($userRole == 'patient') {
286 $_SESSION['pid'] = $user['pid'] ??
null;
287 $puuidCheck = $user['uuid'] ??
null;
288 $puuidStringCheck = UuidRegistry
::uuidToString($puuidCheck) ??
null;
289 if (empty($_SESSION['pid']) ||
empty($puuidCheck) ||
empty($puuidStringCheck)) {
290 // this should never happen
291 $logger->error("OpenEMR Error: api failed because unable to set critical patient session variables");
292 $gbl::destroySession();
293 http_response_code(401);
296 $restRequest->setPatientRequest(true);
297 $restRequest->setPatientUuidString($puuidStringCheck);
298 $logger->debug("dispatch.php request setup for patient role", ['patient' => $puuidStringCheck]);
299 } else if ($userRole === 'system') {
300 $_SESSION['authUser'] = $user["username"] ??
null;
301 $_SESSION['authUserID'] = $user["id"] ??
null;
303 empty($_SESSION['authUser'])
304 // this should never happen as the system role depends on the system username... but we safety check it anyways
305 ||
$_SESSION['authUser'] != \OpenEMR\Services\UserService
::SYSTEM_USER_USERNAME
306 ||
empty($_SESSION['authUserID'])
308 $logger->error("OpenEMR Error: api failed because unable to set critical users session variables");
309 $gbl::destroySession();
310 http_response_code(401);
314 // this user role is not supported
315 $logger->error("OpenEMR Error - api user role that was provided is not supported, so forced exit");
316 $gbl::destroySession();
317 http_response_code(400);
322 //Extend API using RestApiCreateEvent
323 $restApiCreateEvent = new RestApiCreateEvent($gbl::$ROUTE_MAP, $gbl::$FHIR_ROUTE_MAP, $gbl::$PORTAL_ROUTE_MAP, $restRequest);
324 $restApiCreateEvent = $GLOBALS["kernel"]->getEventDispatcher()->dispatch($restApiCreateEvent, RestApiCreateEvent
::EVENT_HANDLE
, 10);
325 $gbl::$ROUTE_MAP = $restApiCreateEvent->getRouteMap();
326 $gbl::$FHIR_ROUTE_MAP = $restApiCreateEvent->getFHIRRouteMap();
327 $gbl::$PORTAL_ROUTE_MAP = $restApiCreateEvent->getPortalRouteMap();
328 $restRequest = $restApiCreateEvent->getRestRequest();
330 // api flag must be four chars
331 // Pass only routes for current api.
332 // Also check to ensure route is turned on in globals
333 if ($gbl::is_fhir_request($resource)) {
334 if (!$GLOBALS['rest_fhir_api'] && !$isLocalApi) {
335 // if the external fhir api is turned off and this is not a local api call, then exit
336 $logger->error("dispatch.php attempted to access resource with FHIR api turned off ", ['resource' => $resource]);
337 $gbl::destroySession();
338 http_response_code(501);
341 $_SESSION['api'] = 'fhir';
342 $routes = $gbl::$FHIR_ROUTE_MAP;
343 } elseif ($gbl::is_portal_request($resource)) {
344 if (!$GLOBALS['rest_portal_api'] && !$isLocalApi) {
345 $logger->error("dispatch.php attempted to access resource with portal api turned off ", ['resource' => $resource]);
346 // if the external portal api is turned off and this is not a local api call, then exit
347 $gbl::destroySession();
348 http_response_code(501);
351 $_SESSION['api'] = 'port';
352 $routes = $gbl::$PORTAL_ROUTE_MAP;
353 } elseif ($gbl::is_api_request($resource)) {
354 if (!$GLOBALS['rest_api'] && !$isLocalApi) {
356 "dispatch.php attempted to access resource with REST api turned off ",
357 ['resource' => $resource]
359 // if the external api is turned off and this is not a local api call, then exit
360 $gbl::destroySession();
361 http_response_code(501);
364 $_SESSION['api'] = 'oemr';
365 $routes = $gbl::$ROUTE_MAP;
367 $logger->error("dispatch.php invalid access to resource", ['resource' => $resource]);
369 // somebody is up to no good
371 $gbl::destroySession();
373 http_response_code(501);
377 $restRequest->setApiType($_SESSION['api']);
380 // Ensure that a local process does not hold up other processes
381 // Note can not do this for !$isLocalApi since need to be able to set
382 // session variables and it won't help performance anyways.
383 session_write_close();
386 // dispatch $routes called by ref (note storing the output in a variable to allow option
387 // to destroy the session/cookie before sending the output back)
389 $dispatchResult = HttpRestRouteHandler
::dispatch($routes, $restRequest);
390 $apiCallOutput = ob_get_clean();
391 // Tear down session for security.
393 $gbl::destroySession();
395 // TODO: @adunsulag we should consider rearranging the order of this code. We would rather return the response interface
396 // then something that was collected in the buffer... There are things internally that just dump to the screen which
397 // we really don't want to just spit out to the screen such as prepared statement error failures.
398 // Send the output if not empty
399 if (!empty($apiCallOutput)) {
401 } else if ($dispatchResult instanceof ResponseInterface
) {
402 RestConfig
::emitResponse($dispatchResult);
405 // prevent 200 if route doesn't exist
406 if ($dispatchResult === false) {
407 $logger->debug("dispatch.php no route found for resource", ['resource' => $resource]);
408 http_response_code(404);