4 * Authorization Server Member
7 * @link http://www.open-emr.org
8 * @author Jerry Padgett <sjpadgett@gmail.com>
9 * @author Brady Miller <brady.g.miller@gmail.com>
10 * @copyright Copyright (c) 2020 Jerry Padgett <sjpadgett@gmail.com>
11 * @copyright Copyright (c) 2020 Brady Miller <brady.g.miller@gmail.com>
12 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 namespace OpenEMR\RestControllers
;
17 require_once(__DIR__
. "/../Common/Session/SessionUtil.php");
20 use DateTimeImmutable
;
22 use Google\Service\CloudHealthcare\FhirConfig
;
23 use GuzzleHttp\Client
;
24 use Lcobucci\JWT\Parser
;
25 use Lcobucci\JWT\Signer\Rsa\Sha256
;
26 use Lcobucci\JWT\ValidationData
;
27 use League\OAuth2\Server\AuthorizationServer
;
28 use League\OAuth2\Server\CryptKey
;
29 use League\OAuth2\Server\CryptTrait
;
30 use League\OAuth2\Server\Entities\ScopeEntityInterface
;
31 use League\OAuth2\Server\Exception\OAuthServerException
;
32 use League\OAuth2\Server\Grant\AuthCodeGrant
;
33 use League\OAuth2\Server\Grant\ClientCredentialsGrant
;
34 use League\OAuth2\Server\RequestTypes\AuthorizationRequest
;
35 use Nyholm\Psr7Server\ServerRequestCreator
;
36 use OpenEMR\Common\Auth\AuthUtils
;
37 use OpenEMR\Common\Auth\MfaUtils
;
38 use OpenEMR\Common\Auth\OAuth2KeyConfig
;
39 use OpenEMR\Common\Auth\OAuth2KeyException
;
40 use OpenEMR\Common\Auth\OpenIDConnect\Entities\ClientEntity
;
41 use OpenEMR\Common\Auth\OpenIDConnect\Entities\ScopeEntity
;
42 use OpenEMR\Common\Auth\OpenIDConnect\Entities\UserEntity
;
43 use OpenEMR\Common\Auth\OpenIDConnect\Grant\CustomAuthCodeGrant
;
44 use OpenEMR\Common\Auth\OpenIDConnect\Grant\CustomClientCredentialsGrant
;
45 use OpenEMR\Common\Auth\OpenIDConnect\Grant\CustomPasswordGrant
;
46 use OpenEMR\Common\Auth\OpenIDConnect\Grant\CustomRefreshTokenGrant
;
47 use OpenEMR\Common\Auth\OpenIDConnect\IdTokenSMARTResponse
;
48 use OpenEMR\Common\Auth\OpenIDConnect\JWT\JsonWebKeyParser
;
49 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\AccessTokenRepository
;
50 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\AuthCodeRepository
;
51 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\ClientRepository
;
52 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\IdentityRepository
;
53 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\RefreshTokenRepository
;
54 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\ScopeRepository
;
55 use OpenEMR\Common\Auth\OpenIDConnect\Repositories\UserRepository
;
56 use OpenEMR\Common\Auth\UuidUserAccount
;
57 use OpenEMR\Common\Crypto\CryptoGen
;
58 use OpenEMR\Common\Csrf\CsrfUtils
;
59 use OpenEMR\Common\Http\Psr17Factory
;
60 use OpenEMR\Common\Logging\SystemLogger
;
61 use OpenEMR\Common\Session\SessionUtil
;
62 use OpenEMR\Common\Utils\RandomGenUtils
;
63 use OpenEMR\Common\Uuid\UuidRegistry
;
64 use OpenEMR\FHIR\Config\ServerConfig
;
65 use OpenEMR\FHIR\SMART\SmartLaunchController
;
66 use OpenEMR\RestControllers\SMART\SMARTAuthorizationController
;
67 use OpenEMR\Services\TrustedUserService
;
68 use OpenIDConnectServer\ClaimExtractor
;
69 use OpenIDConnectServer\Entities\ClaimSetEntity
;
70 use Psr\Http\Message\ResponseInterface
;
71 use Psr\Http\Message\ServerRequestInterface
;
72 use Psr\Log\LoggerInterface
;
75 class AuthorizationController
79 public const ENDPOINT_SCOPE_AUTHORIZE_CONFIRM
= "/scope-authorize-confirm";
81 public const GRANT_TYPE_PASSWORD
= 'password';
82 public const GRANT_TYPE_CLIENT_CREDENTIALS
= 'client_credentials';
83 public const OFFLINE_ACCESS_SCOPE
= 'offline_access';
86 public $authBaseFullUrl;
91 private $oaEncryptionKey;
93 private $providerForm;
94 private $authRequestSerial;
99 * @var SMARTAuthorizationController
101 private $smartAuthController;
104 * @var LoggerInterface
109 * Handles CRUD operations for OAUTH2 Trusted Users
110 * @var TrustedUserService
112 private $trustedUserService;
119 public function __construct($providerForm = true)
121 $gbl = \RestConfig
::GetInstance();
122 $this->restConfig
= $gbl;
123 $this->logger
= new SystemLogger();
125 $this->siteId
= $_SESSION['site_id'] ??
$gbl::$SITE;
126 $this->authBaseUrl
= $GLOBALS['webroot'] . '/oauth2/' . $this->siteId
;
127 $this->authBaseFullUrl
= self
::getAuthBaseFullURL();
128 // used for session stash
129 $this->authRequestSerial
= $_SESSION['authRequestSerial'] ??
'';
130 // Create a crypto object that will be used for for encryption/decryption
131 $this->cryptoGen
= new CryptoGen();
132 // verify and/or setup our key pairs.
133 $this->configKeyPairs();
135 // true will display client/user server sign in. false, not.
136 $this->providerForm
= $providerForm;
138 $this->smartAuthController
= new SMARTAuthorizationController(
140 $this->authBaseFullUrl
,
141 $this->authBaseFullUrl
. self
::ENDPOINT_SCOPE_AUTHORIZE_CONFIRM
,
142 __DIR__
. "/../../oauth2/"
145 $this->trustedUserService
= new TrustedUserService();
148 private function configKeyPairs(): void
150 $response = $this->createServerResponse();
152 $oauth2KeyConfig = new OAuth2KeyConfig($GLOBALS['OE_SITE_DIR']);
153 $oauth2KeyConfig->configKeyPairs();
154 $this->privateKey
= $oauth2KeyConfig->getPrivateKeyLocation();
155 $this->publicKey
= $oauth2KeyConfig->getPublicKeyLocation();
156 $this->oaEncryptionKey
= $oauth2KeyConfig->getEncryptionKey();
157 $this->passphrase
= $oauth2KeyConfig->getPassPhrase();
158 } catch (OAuth2KeyException
$exception) {
159 $this->logger
->error("OpenEMR error - " . $exception->getMessage() . ", so forced exit");
160 $serverException = OAuthServerException
::serverError(
161 "Security error - problem with authorization server keys.",
164 SessionUtil
::oauthSessionCookieDestroy();
165 $this->emitResponse($serverException->generateHttpResponse($response));
170 public function clientRegistration(): void
172 $response = $this->createServerResponse();
173 $request = $this->createServerRequest();
176 $request_headers = $request->getHeaders();
177 foreach ($request_headers as $header => $value) {
178 $headers[strtolower($header)] = $value[0];
180 if (!$headers['content-type'] ||
strpos($headers['content-type'], 'application/json') !== 0) {
181 throw new OAuthServerException('Unexpected content type', 0, 'invalid_client_metadata');
183 $json = file_get_contents('php://input');
185 throw new OAuthServerException('No JSON body', 0, 'invalid_client_metadata');
187 $data = json_decode($json, true);
189 throw new OAuthServerException('Invalid JSON', 0, 'invalid_client_metadata');
191 // many of these are optional and are here if we want to implement
192 $keys = array('contacts' => null,
193 'application_type' => null,
194 'client_name' => null,
196 'redirect_uris' => null,
197 'post_logout_redirect_uris' => null,
198 'token_endpoint_auth_method' => array('client_secret_basic', 'client_secret_post'),
199 'policy_uri' => null,
203 'sector_identifier_uri' => null,
204 'subject_type' => array('pairwise', 'public'),
205 'default_max_age' => null,
206 'require_auth_time' => null,
207 'default_acr_values' => null,
208 'initiate_login_uri' => null, // for anything with a SMART 'launch/ehr' context we need to know how to initiate the login
209 'request_uris' => null,
210 'response_types' => null,
211 'grant_types' => null,
212 // info on scope can be seen at
213 // OAUTH2 Dynamic Client Registration RFC 7591 Section 2 Page 9
214 // @see https://tools.ietf.org/html/rfc7591#section-2
217 $client_id = $this->base64url_encode(RandomGenUtils
::produceRandomBytes(32));
218 $reg_token = $this->base64url_encode(RandomGenUtils
::produceRandomBytes(32));
219 $reg_client_uri_path = $this->base64url_encode(RandomGenUtils
::produceRandomBytes(16));
221 'client_id' => $client_id,
222 'client_id_issued_at' => time(),
223 'registration_access_token' => $reg_token,
224 'registration_client_uri_path' => $reg_client_uri_path
227 $params['client_role'] = 'patient';
228 // only include secret if a confidential app else force PKCE for native and web apps.
230 if ($data['application_type'] === 'private') {
231 $client_secret = $this->base64url_encode(RandomGenUtils
::produceRandomBytes(64));
232 $params['client_secret'] = $client_secret;
233 $params['client_role'] = 'user';
235 // don't allow system scopes without a jwk or jwks_uri value
237 strpos($data['scope'], 'system/') !== false
238 && empty($data['jwks']) && empty($data['jwks_uri'])
240 throw new OAuthServerException('jwks is invalid', 0, 'invalid_client_metadata');
242 // don't allow user, system scopes, and offline_access for public apps
244 strpos($data['scope'], 'system/') !== false
245 ||
strpos($data['scope'], 'user/') !== false
247 throw new OAuthServerException("system and user scopes are only allowed for confidential clients", 0, 'invalid_client_metadata');
249 $this->validateScopesAgainstServerApprovedScopes($data['scope']);
251 foreach ($keys as $key => $supported_values) {
252 if (isset($data[$key])) {
253 if (in_array($key, array('contacts', 'redirect_uris', 'request_uris', 'post_logout_redirect_uris', 'grant_types', 'response_types', 'default_acr_values'))) {
254 $params[$key] = implode('|', $data[$key]);
255 } elseif ($key === 'jwks') {
256 $params[$key] = json_encode($data[$key]);
258 $params[$key] = $data[$key];
260 if (!empty($supported_values)) {
261 if (!in_array($params[$key], $supported_values)) {
262 throw new OAuthServerException("Unsupported $key value : $params[$key]", 0, 'invalid_client_metadata');
267 if (!isset($data['redirect_uris'])) {
268 throw new OAuthServerException('redirect_uris is invalid', 0, 'invalid_redirect_uri');
270 if (isset($data['post_logout_redirect_uris']) && empty($data['post_logout_redirect_uris'])) {
271 throw new OAuthServerException('post_logout_redirect_uris is invalid', 0, 'invalid_client_metadata');
273 // save to oauth client table
274 $badSave = $this->newClientSave($client_id, $params);
275 if (!empty($badSave)) {
276 throw OAuthServerException
::serverError("Try again. Unable to create account");
278 $reg_uri = $this->authBaseFullUrl
. '/client/' . $reg_client_uri_path;
279 unset($params['registration_client_uri_path']);
280 $client_json = array(
281 'client_id' => $client_id,
282 'client_secret' => $client_secret,
283 'registration_access_token' => $reg_token,
284 'registration_client_uri' => $reg_uri,
285 'client_id_issued_at' => time(),
286 'client_secret_expires_at' => 0
288 $array_params = array('contacts', 'redirect_uris', 'request_uris', 'post_logout_redirect_uris', 'response_types', 'grant_types', 'default_acr_values');
289 foreach ($array_params as $aparam) {
290 if (isset($params[$aparam])) {
291 $params[$aparam] = explode('|', $params[$aparam]);
294 if (!empty($params['jwks'])) {
295 $params['jwks'] = json_decode($params['jwks'], true);
297 if (isset($params['require_auth_time'])) {
298 $params['require_auth_time'] = ($params['require_auth_time'] === 1);
301 $response->withHeader("Cache-Control", "no-store");
302 $response->withHeader("Pragma", "no-cache");
303 $response->withHeader('Content-Type', 'application/json');
304 $body = $response->getBody();
305 $body->write(json_encode(array_merge($client_json, $params)));
307 SessionUtil
::oauthSessionCookieDestroy();
308 $this->emitResponse($response->withStatus(200)->withBody($body));
309 } catch (OAuthServerException
$exception) {
310 SessionUtil
::oauthSessionCookieDestroy();
311 $this->emitResponse($exception->generateHttpResponse($response));
316 * Verifies that the scope string only has approved scopes for the system.
317 * @param $scopeString the space separated scope string
319 private function validateScopesAgainstServerApprovedScopes($scopeString)
321 $requestScopes = explode(" ", $scopeString);
322 if (empty($requestScopes)) {
326 $scopeRepo = new ScopeRepository($this->restConfig
);
327 $scopeRepo->setRequestScopes($scopeString);
328 foreach ($requestScopes as $scope) {
329 $validScope = $scopeRepo->getScopeEntityByIdentifier($scope);
330 if (empty($validScope)) {
331 throw OAuthServerException
::invalidScope($scope);
336 private function createServerResponse(): ResponseInterface
338 return (new Psr17Factory())->createResponse();
341 private function createServerRequest(): ServerRequestInterface
343 $psr17Factory = new Psr17Factory();
345 return (new ServerRequestCreator(
346 $psr17Factory, // ServerRequestFactory
347 $psr17Factory, // UriFactory
348 $psr17Factory, // UploadedFileFactory
349 $psr17Factory // StreamFactory
353 public function base64url_encode($data): string
355 return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
358 public function base64url_decode($token)
360 $b64 = strtr($token, '-_', '+/');
361 return base64_decode($b64);
364 public function newClientSave($clientId, $info): bool
366 $user = $_SESSION['authUserID'] ??
null; // future use for provider client.
367 $site = $this->siteId
;
368 $is_confidential_client = empty($info['client_secret']) ?
0 : 1;
370 $contacts = $info['contacts'];
371 $redirects = $info['redirect_uris'];
372 $logout_redirect_uris = $info['post_logout_redirect_uris'] ??
null;
373 $info['client_secret'] = $info['client_secret'] ??
null; // just to be sure empty is null;
374 // set our list of default scopes for the registration if our scope is empty
375 // This is how a client can set if they support SMART apps and other stuff by passing in the 'launch'
376 // scope to the dynamic client registration.
377 // per RFC 7591 @see https://tools.ietf.org/html/rfc7591#section-2
378 // TODO: adunsulag do we need to reject the registration if there are certain scopes here we do not support
379 // TODO: adunsulag should we check these scopes against our '$this->supportedScopes'?
380 $info['scope'] = $info['scope'] ??
'openid email phone address api:oemr api:fhir api:port';
382 $scopes = explode(" ", $info['scope']);
383 $scopeRepo = new ScopeRepository();
385 if ($scopeRepo->hasScopesThatRequireManualApproval($is_confidential_client == 1, $scopes)) {
386 $is_client_enabled = 0; // disabled
388 $is_client_enabled = 1; // enabled
391 // encrypt the client secret
392 if (!empty($info['client_secret'])) {
393 $info['client_secret'] = $this->cryptoGen
->encryptStandard($info['client_secret']);
398 // TODO: @adunsulag why do we skip over request_uris when we have it in the outer function?
399 $sql = "INSERT INTO `oauth_clients` (`client_id`, `client_role`, `client_name`, `client_secret`, `registration_token`, `registration_uri_path`, `register_date`, `revoke_date`, `contacts`, `redirect_uri`, `grant_types`, `scope`, `user_id`, `site_id`, `is_confidential`, `logout_redirect_uris`, `jwks_uri`, `jwks`, `initiate_login_uri`, `endorsements`, `policy_uri`, `tos_uri`, `is_enabled`) VALUES (?, ?, ?, ?, ?, ?, NOW(), NULL, ?, ?, 'authorization_code', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
402 $info['client_role'],
403 $info['client_name'],
404 $info['client_secret'],
405 $info['registration_access_token'],
406 $info['registration_client_uri_path'],
412 $is_confidential_client,
413 $logout_redirect_uris,
414 ($info['jwks_uri'] ??
null),
415 ($info['jwks'] ??
null),
416 ($info['initiate_login_uri'] ??
null),
417 ($info['endorsements'] ??
null),
418 ($info['policy_uri'] ??
null),
419 ($info['tos_uri'] ??
null),
423 return sqlQueryNoLog($sql, $i_vals);
424 } catch (\RuntimeException
$e) {
429 public function emitResponse($response): void
431 if (headers_sent()) {
432 throw new RuntimeException('Headers already sent.');
434 $statusLine = sprintf(
436 $response->getProtocolVersion(),
437 $response->getStatusCode(),
438 $response->getReasonPhrase()
440 header($statusLine, true);
441 foreach ($response->getHeaders() as $name => $values) {
442 $responseHeader = sprintf('%s: %s', $name, $response->getHeaderLine($name));
443 header($responseHeader, false);
446 echo $response->getBody();
449 public function clientRegisteredDetails(): void
451 $response = $this->createServerResponse();
454 $token = $_REQUEST['access_token'];
456 $token = $this->getBearerToken();
458 throw new OAuthServerException('No Access Code', 0, 'invalid_request', 403);
461 $pos = strpos($_SERVER['PATH_INFO'], '/client/');
462 if ($pos === false) {
463 throw new OAuthServerException('Invalid path', 0, 'invalid_request', 403);
465 $uri_path = substr($_SERVER['PATH_INFO'], $pos +
8);
466 $client = sqlQuery("SELECT * FROM `oauth_clients` WHERE `registration_uri_path` = ?", array($uri_path));
468 throw new OAuthServerException('Invalid client', 0, 'invalid_request', 403);
470 if ($client['registration_access_token'] !== $token) {
471 throw new OAuthServerException('Invalid registration token', 0, 'invalid _request', 403);
473 $params['client_id'] = $client['client_id'];
474 $params['client_secret'] = $this->cryptoGen
->decryptStandard($client['client_secret']);
475 $params['contacts'] = explode('|', $client['contacts']);
476 $params['application_type'] = $client['client_role'];
477 $params['client_name'] = $client['client_name'];
478 $params['redirect_uris'] = explode('|', $client['redirect_uri']);
480 $response->withHeader("Cache-Control", "no-store");
481 $response->withHeader("Pragma", "no-cache");
482 $response->withHeader('Content-Type', 'application/json');
483 $body = $response->getBody();
484 $body->write(json_encode($params));
486 SessionUtil
::oauthSessionCookieDestroy();
487 $this->emitResponse($response->withStatus(200)->withBody($body));
488 } catch (OAuthServerException
$exception) {
489 SessionUtil
::oauthSessionCookieDestroy();
490 $this->emitResponse($exception->generateHttpResponse($response));
494 public function getBearerToken(): string
496 $request = $this->createServerRequest();
497 $request_headers = $request->getHeaders();
499 foreach ($request_headers as $header => $value) {
500 $headers[strtolower($header)] = $value[0];
502 $authorization = $headers['authorization'];
503 if ($authorization) {
504 $pieces = explode(' ', $authorization);
505 if (strcasecmp($pieces[0], 'bearer') !== 0) {
509 return rtrim($pieces[1]);
514 public function oauthAuthorizationFlow(): void
516 $this->logger
->debug("AuthorizationController->oauthAuthorizationFlow() starting authorization flow");
517 $response = $this->createServerResponse();
518 $request = $this->createServerRequest();
520 if ($nonce = $request->getQueryParams()['nonce'] ??
null) {
521 $_SESSION['nonce'] = $request->getQueryParams()['nonce'];
524 $this->logger
->debug("AuthorizationController->oauthAuthorizationFlow() request query params ", ["queryParams" => $request->getQueryParams()]);
526 $this->grantType
= 'authorization_code';
527 $server = $this->getAuthorizationServer();
529 // Validate the HTTP request and return an AuthorizationRequest object.
530 $this->logger
->debug("AuthorizationController->oauthAuthorizationFlow() attempting to validate auth request");
531 $authRequest = $server->validateAuthorizationRequest($request);
532 $this->logger
->debug("AuthorizationController->oauthAuthorizationFlow() auth request validated, csrf,scopes,client_id setup");
533 $_SESSION['csrf'] = $authRequest->getState();
534 $_SESSION['scopes'] = $request->getQueryParams()['scope'];
535 $_SESSION['client_id'] = $request->getQueryParams()['client_id'];
536 $_SESSION['client_role'] = $authRequest->getClient()->getClientRole();
537 $_SESSION['launch'] = $request->getQueryParams()['launch'] ??
null;
538 $_SESSION['redirect_uri'] = $authRequest->getRedirectUri() ??
null;
539 $this->logger
->debug("AuthorizationController->oauthAuthorizationFlow() session updated", ['session' => $_SESSION]);
540 // If needed, serialize into a users session
541 if ($this->providerForm
) {
542 $this->serializeUserSession($authRequest, $request);
543 $this->logger
->debug("AuthorizationController->oauthAuthorizationFlow() redirecting to provider form");
544 // call our login then login calls authorize if approved by user
545 header("Location: " . $this->authBaseUrl
. "/provider/login", true, 301);
548 } catch (OAuthServerException
$exception) {
549 $this->logger
->error(
550 "AuthorizationController->oauthAuthorizationFlow() OAuthServerException",
551 ["hint" => $exception->getHint(), "message" => $exception->getMessage()
552 , 'payload' => $exception->getPayload()
553 , 'trace' => $exception->getTraceAsString()
554 , 'redirectUri' => $exception->getRedirectUri()
555 , 'errorType' => $exception->getErrorType()]
557 SessionUtil
::oauthSessionCookieDestroy();
558 $this->emitResponse($exception->generateHttpResponse($response));
559 } catch (Exception
$exception) {
560 $this->logger
->error("AuthorizationController->oauthAuthorizationFlow() Exception message: " . $exception->getMessage());
561 SessionUtil
::oauthSessionCookieDestroy();
562 $body = $response->getBody();
563 $body->write($exception->getMessage());
564 $this->emitResponse($response->withStatus(500)->withBody($body));
569 * Retrieve the authorization server with all of the grants configured
570 * TODO: @adunsulag is there a better way to handle skipping the refresh token on the authorization grant?
571 * Due to the way the server is created and the fact we have to skip the refresh token when an offline_scope is passed
572 * for authorization_grant/password_grant. We ignore offline_scope for custom_credentials
573 * @param bool $includeAuthGrantRefreshToken Whether the authorization server should issue a refresh token for an authorization grant.
574 * @return AuthorizationServer
577 public function getAuthorizationServer($includeAuthGrantRefreshToken = true): AuthorizationServer
579 $protectedClaims = ['profile', 'email', 'address', 'phone'];
580 $scopeRepository = new ScopeRepository($this->restConfig
);
581 $claims = $scopeRepository->getSupportedClaims();
583 foreach ($claims as $claim) {
584 if (in_array($claim, $protectedClaims, true)) {
587 $customClaim[] = new ClaimSetEntity($claim, [$claim]);
589 if (!empty($_SESSION['nonce'])) {
590 // nonce scope added later. this is for id token nonce claim.
591 $customClaim[] = new ClaimSetEntity('nonce', ['nonce']);
594 // OpenID Connect Response Type
595 $this->logger
->debug("AuthorizationController->getAuthorizationServer() creating server");
596 $responseType = new IdTokenSMARTResponse(new IdentityRepository(), new ClaimExtractor($customClaim));
598 if (empty($this->grantType
)) {
599 $this->grantType
= 'authorization_code';
602 // responseType is cloned inside the league auth server so we have to handle changes here before we send
603 // into the $authServer the $responseType
604 if ($this->grantType
=== 'authorization_code') {
605 $responseType->markIsAuthorizationGrant(); // we have specific SMART responses for an authorization grant.
608 $authServer = new AuthorizationServer(
609 new ClientRepository(),
610 new AccessTokenRepository(),
611 new ScopeRepository($this->restConfig
),
612 new CryptKey($this->privateKey
, $this->passphrase
),
613 $this->oaEncryptionKey
,
617 $this->logger
->debug("AuthorizationController->getAuthorizationServer() grantType is " . $this->grantType
);
618 if ($this->grantType
=== 'authorization_code') {
619 $this->logger
->debug(
620 "logging global params",
621 ['site_addr_oath' => $GLOBALS['site_addr_oath'], 'web_root' => $GLOBALS['web_root'], 'site_id' => $_SESSION['site_id']]
623 $fhirServiceConfig = new ServerConfig();
624 $expectedAudience = [
625 $fhirServiceConfig->getFhirUrl(),
626 $GLOBALS['site_addr_oath'] . $GLOBALS['web_root'] . '/apis/' . $_SESSION['site_id'] . "/api",
627 $GLOBALS['site_addr_oath'] . $GLOBALS['web_root'] . '/apis/' . $_SESSION['site_id'] . "/portal",
629 $grant = new CustomAuthCodeGrant(
630 new AuthCodeRepository(),
631 new RefreshTokenRepository($includeAuthGrantRefreshToken),
632 new \
DateInterval('PT1M'), // auth code. should be short turn around.
636 $grant->setRefreshTokenTTL(new \
DateInterval('P3M')); // minimum per ONC
637 $authServer->enableGrantType(
639 new \
DateInterval('PT1H') // access token
642 if ($this->grantType
=== 'refresh_token') {
643 $grant = new CustomRefreshTokenGrant(new RefreshTokenRepository());
644 $grant->setRefreshTokenTTL(new \
DateInterval('P3M'));
645 $authServer->enableGrantType(
647 new \
DateInterval('PT1H') // The new access token will expire after 1 hour
650 // TODO: break this up - throw exception for not turned on.
651 if (!empty($GLOBALS['oauth_password_grant']) && ($this->grantType
=== self
::GRANT_TYPE_PASSWORD
)) {
652 $grant = new CustomPasswordGrant(
653 new UserRepository(),
654 new RefreshTokenRepository($includeAuthGrantRefreshToken)
656 $grant->setRefreshTokenTTL(new DateInterval('P3M'));
657 $authServer->enableGrantType(
659 new \
DateInterval('PT1H') // access token
662 if ($this->grantType
=== self
::GRANT_TYPE_CLIENT_CREDENTIALS
) {
663 // Enable the client credentials grant on the server
664 $client_credentials = new CustomClientCredentialsGrant(AuthorizationController
::getAuthBaseFullURL() . AuthorizationController
::getTokenPath());
665 $client_credentials->setLogger($this->logger
);
666 $client_credentials->setHttpClient(new Client()); // set our guzzle client here
667 $authServer->enableGrantType(
669 // https://hl7.org/fhir/uv/bulkdata/authorization/index.html#issuing-access-tokens Spec states 5 min max
670 new \
DateInterval('PT300S')
674 $this->logger
->debug("AuthorizationController->getAuthorizationServer() authServer created");
678 private function serializeUserSession($authRequest, ServerRequestInterface
$httpRequest): void
680 $launchParam = isset($httpRequest->getQueryParams()['launch']) ?
$httpRequest->getQueryParams()['launch'] : null;
681 // keeping somewhat granular
683 $scopes = $authRequest->getScopes();
685 foreach ($scopes as $scope) {
686 $scoped[] = $scope->getIdentifier();
688 $client['name'] = $authRequest->getClient()->getName();
689 $client['redirectUri'] = $authRequest->getClient()->getRedirectUri();
690 $client['identifier'] = $authRequest->getClient()->getIdentifier();
691 $client['isConfidential'] = $authRequest->getClient()->isConfidential();
693 'grantTypeId' => $authRequest->getGrantTypeId(),
694 'authorizationApproved' => false,
695 'redirectUri' => $authRequest->getRedirectUri(),
696 'state' => $authRequest->getState(),
697 'codeChallenge' => $authRequest->getCodeChallenge(),
698 'codeChallengeMethod' => $authRequest->getCodeChallengeMethod(),
700 $result = array('outer' => $outer, 'scopes' => $scoped, 'client' => $client);
701 $this->authRequestSerial
= json_encode($result, JSON_THROW_ON_ERROR
);
702 $_SESSION['authRequestSerial'] = $this->authRequestSerial
;
703 } catch (Exception
$e) {
708 public function userLogin(): void
710 $response = $this->createServerResponse();
712 $patientRoleSupport = (!empty($GLOBALS['rest_portal_api']) ||
!empty($GLOBALS['rest_fhir_api']));
714 if (empty($_POST['username']) && empty($_POST['password'])) {
715 $this->logger
->debug("AuthorizationController->userLogin() presenting blank login form");
717 $redirect = $this->authBaseUrl
. "/login";
718 require_once(__DIR__
. "/../../oauth2/provider/login.php");
721 $continueLogin = false;
722 if (isset($_POST['user_role'])) {
723 if (!CsrfUtils
::verifyCsrfToken($_POST["csrf_token_form"], 'oauth2')) {
724 $this->logger
->error("AuthorizationController->userLogin() Invalid CSRF token");
725 CsrfUtils
::csrfNotVerified(false, true, false);
726 unset($_POST['username'], $_POST['password']);
727 $invalid = "Sorry. Invalid CSRF!"; // todo: display error
729 $redirect = $this->authBaseUrl
. "/login";
730 require_once(__DIR__
. "/../../oauth2/provider/login.php");
733 $this->logger
->debug("AuthorizationController->userLogin() verifying login information");
734 $continueLogin = $this->verifyLogin($_POST['username'], $_POST['password'], ($_POST['email'] ??
''), $_POST['user_role']);
735 $this->logger
->debug("AuthorizationController->userLogin() verifyLogin result", ["continueLogin" => $continueLogin]);
739 if (!$continueLogin) {
740 $this->logger
->debug("AuthorizationController->userLogin() login invalid, presenting login form");
741 $invalid = "Sorry, Invalid!"; // todo: display error
743 $redirect = $this->authBaseUrl
. "/login";
744 require_once(__DIR__
. "/../../oauth2/provider/login.php");
747 $this->logger
->debug("AuthorizationController->userLogin() login valid, continuing oauth process");
750 //Require MFA if turned on
751 $mfa = new MfaUtils($this->userId
);
752 $mfaToken = $mfa->tokenFromRequest($_POST['mfa_type'] ??
null);
753 $mfaType = $mfa->getType();
754 $TOTP = MfaUtils
::TOTP
;
755 $U2F = MfaUtils
::U2F
;
756 if ($_POST['user_role'] === 'api' && $mfa->isMfaRequired() && is_null($mfaToken)) {
759 $redirect = $this->authBaseUrl
. "/login";
760 if (in_array(MfaUtils
::U2F
, $mfaType)) {
761 $appId = $mfa->getAppId();
762 $requests = $mfa->getU2fRequests();
764 require_once(__DIR__
. "/../../oauth2/provider/login.php");
767 //Check the validity of the authentication token
768 if ($_POST['user_role'] === 'api' && $mfa->isMfaRequired() && !is_null($mfaToken)) {
769 if (!$mfaToken ||
!$mfa->check($mfaToken, $_POST['mfa_type'])) {
770 $invalid = "Sorry, Invalid code!";
773 $mfaType = $mfa->getType();
774 $redirect = $this->authBaseUrl
. "/login";
775 require_once(__DIR__
. "/../../oauth2/provider/login.php");
780 unset($_POST['username'], $_POST['password']);
781 $_SESSION['persist_login'] = isset($_POST['persist_login']) ?
1 : 0;
782 $user = new UserEntity();
783 $user->setIdentifier($_SESSION['user_id']);
784 $_SESSION['claims'] = $user->getClaims();
786 // need to redirect to patient select if we have a launch context && this isn't a patient login
787 $authorize = 'authorize';
789 // if we need to authorize any smart context as part of our OAUTH handler we do that here
790 // otherwise we send on to our scope authorization confirm.
791 if ($this->smartAuthController
->needSmartAuthorization()) {
792 $redirect = $this->authBaseFullUrl
. $this->smartAuthController
->getSmartAuthorizationPath();
794 $redirect = $this->authBaseFullUrl
. self
::ENDPOINT_SCOPE_AUTHORIZE_CONFIRM
;
796 $this->logger
->debug("AuthorizationController->userLogin() complete redirecting", ["scopes" => $_SESSION['scopes']
797 , 'claims' => $_SESSION['claims'], 'redirect' => $redirect]);
799 header("Location: $redirect");
803 public function scopeAuthorizeConfirm()
805 // TODO: @adunsulag if there are no scopes or claims here we probably want to show an error...
807 // TODO: @adunsulag this is also where we want to show a special message if the offline scope is present.
809 // show our scope auth piece
811 $redirect = $this->authBaseUrl
. "/device/code";
812 $scopeString = $_SESSION['scopes'] ??
'';
813 // check for offline_access
815 $scopesList = explode(' ', $scopeString);
816 $offline_requested = false;
818 foreach ($scopesList as $scope) {
819 if ($scope !== self
::OFFLINE_ACCESS_SCOPE
) {
822 $offline_requested = true;
825 $offline_access_date = (new DateTimeImmutable())->add(new \
DateInterval("P3M"))->format("Y-m-d");
828 $claims = $_SESSION['claims'] ??
[];
830 $clientRepository = new ClientRepository();
831 $client = $clientRepository->getClientEntity($_SESSION['client_id']);
832 $clientName = "<" . xl("Client Name Not Found") . ">";
833 if (!empty($client)) {
834 $clientName = $client->getName();
837 $uuidToUser = new UuidUserAccount($_SESSION['user_id']);
838 $userRole = $uuidToUser->getUserRole();
839 $userAccount = $uuidToUser->getUserAccount();
840 require_once(__DIR__
. "/../../oauth2/provider/scope-authorize.php");
844 * Checks if we are in a SMART authorization endpoint
848 public function isSMARTAuthorizationEndPoint($end_point)
850 return $this->smartAuthController
->isValidRoute($end_point);
854 * Route handler for any SMART authorization contexts that we need for OpenEMR
857 public function dispatchSMARTAuthorizationEndpoint($end_point)
859 return $this->smartAuthController
->dispatchRoute($end_point);
862 private function verifyLogin($username, $password, $email = '', $type = 'api'): bool
864 $auth = new AuthUtils($type);
865 $is_true = $auth->confirmPassword($username, $password, $email);
867 $this->logger
->debug("AuthorizationController->verifyLogin() login attempt failed", ['username' => $username, 'email' => $email, 'type' => $type]);
870 // TODO: should user_id be set to be a uuid here?
871 if ($this->userId
= $auth->getUserId()) {
872 $_SESSION['user_id'] = $this->getUserUuid($this->userId
, 'users');
873 $this->logger
->debug("AuthorizationController->verifyLogin() user login", ['user_id' => $_SESSION['user_id'],
874 'username' => $username, 'email' => $email, 'type' => $type]);
877 if ($id = $auth->getPatientId()) {
878 $puuid = $this->getUserUuid($id, 'patient');
879 // TODO: @adunsulag check with @sjpadgett on where this user_id is even used as we are assigning it to be a uuid
880 $_SESSION['user_id'] = $puuid;
881 $this->logger
->debug("AuthorizationController->verifyLogin() patient login", ['pid' => $_SESSION['user_id']
882 , 'username' => $username, 'email' => $email, 'type' => $type]);
883 $_SESSION['pid'] = $id;
884 $_SESSION['puuid'] = $puuid;
891 protected function getUserUuid($userId, $userRole): string
895 UuidRegistry
::createMissingUuidsForTables(['users']);
896 $account_sql = "SELECT `uuid` FROM `users` WHERE `id` = ?";
899 UuidRegistry
::createMissingUuidsForTables(['patient_data']);
900 $account_sql = "SELECT `uuid` FROM `patient_data` WHERE `pid` = ?";
905 $id = sqlQueryNoLog($account_sql, array($userId))['uuid'];
907 return UuidRegistry
::uuidToString($id);
911 * Note this corresponds with the /auth/code endpoint
913 public function authorizeUser(): void
915 $this->logger
->debug("AuthorizationController->authorizeUser() starting authorization");
916 $response = $this->createServerResponse();
917 $authRequest = $this->deserializeUserSession();
919 $authRequest = $this->updateAuthRequestWithUserApprovedScopes($authRequest, $_POST['scope']);
920 $include_refresh_token = $this->shouldIncludeRefreshTokenForScopes($authRequest->getScopes());
921 $server = $this->getAuthorizationServer($include_refresh_token);
922 $user = new UserEntity();
923 $user->setIdentifier($_SESSION['user_id']);
924 $authRequest->setUser($user);
925 $authRequest->setAuthorizationApproved(true);
926 $result = $server->completeAuthorizationRequest($authRequest, $response);
927 $redirect = $result->getHeader('Location')[0];
928 $authorization = parse_url($redirect, PHP_URL_QUERY
);
929 // stash appropriate session for token endpoint.
930 unset($_SESSION['authRequestSerial']);
931 unset($_SESSION['claims']);
932 $csrf_private_key = $_SESSION['csrf_private_key']; // switcheroo so this does not end up in the session cache
933 unset($_SESSION['csrf_private_key']);
934 $session_cache = json_encode($_SESSION, JSON_THROW_ON_ERROR
);
935 $_SESSION['csrf_private_key'] = $csrf_private_key;
936 unset($csrf_private_key);
938 // parse scope as also a query param if needed
939 parse_str($authorization, $code);
940 $code = $code["code"];
941 if (isset($_POST['proceed']) && !empty($code) && !empty($session_cache)) {
942 if (!CsrfUtils
::verifyCsrfToken($_POST["csrf_token_form"], 'oauth2')) {
943 CsrfUtils
::csrfNotVerified(false, true, false);
944 throw OAuthServerException
::serverError("Failed authorization due to failed CSRF check.");
946 $this->saveTrustedUser($_SESSION['client_id'], $_SESSION['user_id'], $_SESSION['scopes'], $_SESSION['persist_login'], $code, $session_cache);
949 if (empty($_SESSION['csrf'])) {
950 throw OAuthServerException
::serverError("Failed authorization due to missing data.");
953 // Return the HTTP redirect response. Redirect is to client callback.
954 $this->logger
->debug("AuthorizationController->authorizeUser() sending server response");
955 SessionUtil
::oauthSessionCookieDestroy();
956 $this->emitResponse($result);
958 } catch (Exception
$exception) {
959 $this->logger
->error("AuthorizationController->authorizeUser() Exception thrown", ["message" => $exception->getMessage()]);
960 SessionUtil
::oauthSessionCookieDestroy();
961 $body = $response->getBody();
962 $body->write($exception->getMessage());
963 $this->emitResponse($response->withStatus(500)->withBody($body));
968 * @param ScopeEntityInterface[] $scopes
970 private function shouldIncludeRefreshTokenForScopes(array $scopes)
972 foreach ($scopes as $scope) {
973 if ($scope->getIdentifier() == self
::OFFLINE_ACCESS_SCOPE
) {
980 private function updateAuthRequestWithUserApprovedScopes(AuthorizationRequest
$request, $approvedScopes)
982 $this->logger
->debug(
983 "AuthorizationController->updateAuthRequestWithUserApprovedScopes() attempting to update auth request with user approved scopes",
984 ['userApprovedScopes' => $approvedScopes ]
986 $requestScopes = $request->getScopes();
988 // we only allow scopes from the original session request, if user approved scope it will show up here.
989 foreach ($requestScopes as $scope) {
990 if (isset($approvedScopes[$scope->getIdentifier()])) {
991 $scopeUpdates[] = $scope;
994 $this->logger
->debug(
995 "AuthorizationController->updateAuthRequestWithUserApprovedScopes() replaced request scopes with user approved scopes",
996 ['updatedScopes' => $scopeUpdates]
999 $request->setScopes($scopeUpdates);
1003 private function deserializeUserSession(): AuthorizationRequest
1005 $authRequest = new AuthorizationRequest();
1007 $requestData = $_SESSION['authRequestSerial'] ??
$this->authRequestSerial
;
1008 $restore = json_decode($requestData, true, 512);
1009 $outer = $restore['outer'];
1010 $client = $restore['client'];
1011 $scoped = $restore['scopes'];
1012 $authRequest->setGrantTypeId($outer['grantTypeId']);
1013 $e = new ClientEntity();
1014 $e->setName($client['name']);
1015 $e->setRedirectUri($client['redirectUri']);
1016 $e->setIdentifier($client['identifier']);
1017 $e->setIsConfidential($client['isConfidential']);
1018 $authRequest->setClient($e);
1020 foreach ($scoped as $scope) {
1021 $s = new ScopeEntity();
1022 $s->setIdentifier($scope);
1025 $authRequest->setScopes($scopes);
1026 $authRequest->setAuthorizationApproved($outer['authorizationApproved']);
1027 $authRequest->setRedirectUri($outer['redirectUri']);
1028 $authRequest->setState($outer['state']);
1029 $authRequest->setCodeChallenge($outer['codeChallenge']);
1030 $authRequest->setCodeChallengeMethod($outer['codeChallengeMethod']);
1031 } catch (Exception
$e) {
1035 return $authRequest;
1039 * Note this corresponds with the /token endpoint
1041 public function oauthAuthorizeToken(): void
1043 $this->logger
->debug("AuthorizationController->oauthAuthorizeToken() starting request");
1044 $response = $this->createServerResponse();
1045 $request = $this->createServerRequest();
1047 // authorization code which is normally only sent for new tokens
1048 // by the authorization grant flow.
1049 $code = $request->getParsedBody()['code'] ??
null;
1050 // grantType could be authorization_code, password or refresh_token.
1051 $this->grantType
= $request->getParsedBody()['grant_type'];
1052 $this->logger
->debug("AuthorizationController->oauthAuthorizeToken() grant type received", ['grant_type' => $this->grantType
]);
1053 if ($this->grantType
=== 'authorization_code') {
1054 // re-populate from saved session cache populated in authorizeUser().
1055 $ssbc = $this->sessionUserByCode($code);
1056 $_SESSION = json_decode($ssbc['session_cache'], true);
1057 $this->logger
->debug("AuthorizationController->oauthAuthorizeToken() restored session user from code ", ['session' => $_SESSION]);
1059 // TODO: explore why we create the request again...
1060 if ($this->grantType
=== 'refresh_token') {
1061 $request = $this->createServerRequest();
1063 // Finally time to init the server.
1064 $server = $this->getAuthorizationServer();
1066 if (($this->grantType
=== 'authorization_code') && empty($_SESSION['csrf'])) {
1067 // the saved session was not populated as expected
1068 $this->logger
->error("AuthorizationController->oauthAuthorizeToken() CSRF check failed");
1069 throw new OAuthServerException('Bad request', 0, 'invalid_request', 400);
1071 $result = $server->respondToAccessTokenRequest($request, $response);
1072 // save a password trusted user
1073 if ($this->grantType
=== self
::GRANT_TYPE_PASSWORD
) {
1074 $this->saveTrustedUserForPasswordGrant($result);
1076 SessionUtil
::oauthSessionCookieDestroy();
1077 $this->emitResponse($result);
1078 } catch (OAuthServerException
$exception) {
1079 $this->logger
->debug(
1080 "AuthorizationController->oauthAuthorizeToken() OAuthServerException occurred",
1081 ["hint" => $exception->getHint(), "message" => $exception->getMessage(), "stack" => $exception->getTraceAsString()]
1083 SessionUtil
::oauthSessionCookieDestroy();
1084 $this->emitResponse($exception->generateHttpResponse($response));
1085 } catch (Exception
$exception) {
1086 $this->logger
->error(
1087 "AuthorizationController->oauthAuthorizeToken() Exception occurred",
1088 ["message" => $exception->getMessage(), 'trace' => $exception->getTraceAsString()]
1090 SessionUtil
::oauthSessionCookieDestroy();
1091 $body = $response->getBody();
1092 $body->write($exception->getMessage());
1093 $this->emitResponse($response->withStatus(500)->withBody($body));
1097 public function trustedUser($clientId, $userId)
1099 return $this->trustedUserService
->getTrustedUser($clientId, $userId);
1102 public function sessionUserByCode($code)
1104 return $this->trustedUserService
->getTrustedUserByCode($code);
1107 public function saveTrustedUser($clientId, $userId, $scope, $persist, $code = '', $session = '', $grant = 'authorization_code')
1109 return $this->trustedUserService
->saveTrustedUser($clientId, $userId, $scope, $persist, $code, $session, $grant);
1112 public function decodeToken($token)
1114 return json_decode($this->base64url_decode($token), true);
1117 public function userSessionLogout(): void
1120 $response = $this->createServerResponse();
1122 $id_token = $_REQUEST['id_token_hint'] ??
'';
1123 if (empty($id_token)) {
1124 throw new OAuthServerException('Id token missing from request', 0, 'invalid _request', 400);
1126 $post_logout_url = $_REQUEST['post_logout_redirect_uri'] ??
'';
1127 $state = $_REQUEST['state'] ??
'';
1128 $token_parts = explode('.', $id_token);
1129 $id_payload = $this->decodeToken($token_parts[1]);
1131 $client_id = $id_payload['aud'];
1132 $user = $id_payload['sub'];
1133 $id_nonce = $id_payload['nonce'] ??
'';
1134 $trustedUser = $this->trustedUser($client_id, $user);
1135 if (empty($trustedUser['id'])) {
1136 // not logged in so just continue as if were.
1137 $message = xlt("You are currently not signed in.");
1138 if (!empty($post_logout_url)) {
1139 SessionUtil
::oauthSessionCookieDestroy();
1140 header('Location:' . $post_logout_url . "?state=$state");
1142 SessionUtil
::oauthSessionCookieDestroy();
1147 $session_nonce = json_decode($trustedUser['session_cache'], true)['nonce'] ??
'';
1148 // this should be enough to confirm valid id
1149 if ($session_nonce !== $id_nonce) {
1150 throw new OAuthServerException('Id token not issued from this server', 0, 'invalid _request', 400);
1152 // clear the users session
1153 $rtn = $this->trustedUserService
->deleteTrustedUserById($trustedUser['id']);
1154 $client = sqlQueryNoLog("SELECT logout_redirect_uris as valid FROM `oauth_clients` WHERE `client_id` = ? AND `logout_redirect_uris` = ?", array($client_id, $post_logout_url));
1155 if (!empty($post_logout_url) && !empty($client['valid'])) {
1156 SessionUtil
::oauthSessionCookieDestroy();
1157 header('Location:' . $post_logout_url . "?state=$state");
1159 $message = xlt("You have been signed out. Thank you.");
1160 SessionUtil
::oauthSessionCookieDestroy();
1163 } catch (OAuthServerException
$exception) {
1164 SessionUtil
::oauthSessionCookieDestroy();
1165 $this->emitResponse($exception->generateHttpResponse($response));
1169 public function tokenIntrospection(): void
1171 $response = $this->createServerResponse();
1172 $response->withHeader("Cache-Control", "no-store");
1173 $response->withHeader("Pragma", "no-cache");
1174 $response->withHeader('Content-Type', 'application/json');
1176 $rawToken = $_REQUEST['token'] ??
null;
1177 $token_hint = $_REQUEST['token_type_hint'] ??
null;
1178 $clientId = $_REQUEST['client_id'] ??
null;
1179 // not required for public apps but mandatory for confidential
1180 $clientSecret = $_REQUEST['client_secret'] ??
null;
1182 $this->logger
->debug(
1183 self
::class . "->tokenIntrospection() start",
1184 ['token_type_hint' => $token_hint, 'client_id' => $clientId]
1187 // the ride starts. had to use a try because PHP doesn't support tryhard yet!
1189 // so regardless of client type(private/public) we need client for client app type and secret.
1190 $client = sqlQueryNoLog("SELECT * FROM `oauth_clients` WHERE `client_id` = ?", array($clientId));
1191 if (empty($client)) {
1192 throw new OAuthServerException('Not a registered client', 0, 'invalid_request', 401);
1194 // a no no. if private we need a secret.
1195 if (empty($clientSecret) && !empty($client['is_confidential'])) {
1196 throw new OAuthServerException('Invalid client app type', 0, 'invalid_request', 400);
1198 // lets verify secret to prevent bad guys.
1199 if (intval($client['is_enabled'] !== 1)) {
1200 // client is disabled and we don't allow introspection of tokens for disabled clients.
1201 throw new OAuthServerException('Client failed security', 0, 'invalid_request', 401);
1203 // lets verify secret to prevent bad guys.
1204 if (!empty($client['client_secret'])) {
1205 $decryptedSecret = $this->cryptoGen
->decryptStandard($client['client_secret']);
1206 if ($decryptedSecret !== $clientSecret) {
1207 throw new OAuthServerException('Client failed security', 0, 'invalid_request', 401);
1210 $jsonWebKeyParser = new JsonWebKeyParser($this->oaEncryptionKey
, $this->publicKey
);
1211 // will try hard to go on if missing token hint. this is to help with universal conformance.
1212 if (empty($token_hint)) {
1213 $token_hint = $jsonWebKeyParser->getTokenHintFromToken($rawToken);
1214 } elseif (($token_hint !== 'access_token' && $token_hint !== 'refresh_token') ||
empty($rawToken)) {
1215 throw new OAuthServerException('Missing token or unsupported hint.', 0, 'invalid_request', 400);
1218 // are we there yet! client's okay but, is token?
1219 if ($token_hint === 'access_token') {
1221 $result = $jsonWebKeyParser->parseAccessToken($rawToken);
1222 $result['client_id'] = $clientId;
1223 $trusted = $this->trustedUser($result['client_id'], $result['sub']);
1224 if (empty($trusted['id'])) {
1225 $result['active'] = false;
1226 $result['status'] = 'revoked';
1228 $tokenRepository = new AccessTokenRepository();
1229 if ($tokenRepository->isAccessTokenRevokedInDatabase($result['jti'])) {
1230 $result['active'] = false;
1231 $result['status'] = 'revoked';
1233 $audience = $result['aud'];
1234 if (!empty($audience)) {
1235 // audience is an array... we will only validate against the first item
1236 $audience = current($audience);
1238 if ($audience !== $clientId) {
1239 // return no info in this case. possible Phishing
1240 $result = array('active' => false);
1242 } catch (Exception
$exception) {
1243 // JWT couldn't be parsed
1244 $body = $response->getBody();
1245 $body->write($exception->getMessage());
1246 SessionUtil
::oauthSessionCookieDestroy();
1247 $this->emitResponse($response->withStatus(400)->withBody($body));
1251 if ($token_hint === 'refresh_token') {
1253 // client_id comes back from the parsed refresh token
1254 $result = $jsonWebKeyParser->parseRefreshToken($rawToken);
1255 } catch (Exception
$exception) {
1256 $body = $response->getBody();
1257 $body->write($exception->getMessage());
1258 SessionUtil
::oauthSessionCookieDestroy();
1259 $this->emitResponse($response->withStatus(400)->withBody($body));
1262 $trusted = $this->trustedUser($result['client_id'], $result['sub']);
1263 if (empty($trusted['id'])) {
1264 $result['active'] = false;
1265 $result['status'] = 'revoked';
1267 $tokenRepository = new RefreshTokenRepository();
1268 if ($tokenRepository->isRefreshTokenRevoked($result['jti'])) {
1269 $result['active'] = false;
1270 $result['status'] = 'revoked';
1272 if ($result['client_id'] !== $clientId) {
1273 // return no info in this case. possible Phishing
1274 $result = array('active' => false);
1277 } catch (OAuthServerException
$exception) {
1278 // JWT couldn't be parsed
1279 SessionUtil
::oauthSessionCookieDestroy();
1280 $this->logger
->errorLogCaller($exception->getMessage(), ['trace' => $exception->getTraceAsString()]);
1281 $this->emitResponse($exception->generateHttpResponse($response));
1284 // we're here so emit results to interface thank you very much.
1285 $body = $response->getBody();
1286 $body->write(json_encode($result));
1287 SessionUtil
::oauthSessionCookieDestroy();
1288 $this->emitResponse($response->withStatus(200)->withBody($body));
1293 * Returns the authentication server token Url endpoint
1296 public function getTokenUrl()
1298 return $this->authBaseFullUrl
. self
::getTokenPath();
1302 * Returns the path prefix that the token authorization endpoint is on.
1305 public static function getTokenPath()
1311 * Returns the authentication server manage url
1314 public function getManageUrl()
1316 return $this->authBaseFullUrl
. self
::getManagePath();
1320 * Returns the path prefix that the manage token authorization endpoint is on.
1323 public static function getManagePath()
1329 * Returns the authentication server authorization url to use for oauth authentication
1332 public function getAuthorizeUrl()
1334 return $this->authBaseFullUrl
. self
::getAuthorizePath();
1338 * Returns the path prefix that the authorization endpoint is on.
1341 public static function getAuthorizePath()
1343 return "/authorize";
1347 * Returns the authentication server registration url to use for client app / api registration
1350 public function getRegistrationUrl()
1352 return $this->authBaseFullUrl
. self
::getRegistrationPath();
1356 * Returns the path prefix that the registration endpoint is on.
1359 public static function getRegistrationPath()
1361 return "/registration";
1365 * Returns the authentication server introspection url to use for checking tokens
1368 public function getIntrospectionUrl()
1370 return $this->authBaseFullUrl
. self
::getIntrospectionPath();
1374 * Returns the path prefix that the introspection endpoint is on.
1377 public static function getIntrospectionPath()
1379 return "/introspect";
1383 public static function getAuthBaseFullURL()
1385 $baseUrl = $GLOBALS['webroot'] . '/oauth2/' . $_SESSION['site_id'];
1386 // collect full url and issuing url by using 'site_addr_oath' global
1387 $authBaseFullURL = $GLOBALS['site_addr_oath'] . $baseUrl;
1388 return $authBaseFullURL;
1392 * Given a password grant response, save the trusted user information to the database so password grant users
1394 * @param ServerResponseInterface $result
1396 private function saveTrustedUserForPasswordGrant(ResponseInterface
$result)
1398 $body = $result->getBody();
1400 // yep, even password grant gets one. could be useful.
1401 $code = json_decode($body->getContents(), true, 512, JSON_THROW_ON_ERROR
)['id_token'];
1402 unset($_SESSION['csrf_private_key']); // gotta remove since binary and will break json_encode (not used for password granttype, so ok to remove)
1403 $session_cache = json_encode($_SESSION, JSON_THROW_ON_ERROR
);
1404 $this->saveTrustedUser($_REQUEST['client_id'], $_SESSION['pass_user_id'], $_REQUEST['scope'], 0, $code, $session_cache, self
::GRANT_TYPE_PASSWORD
);