Bug fixes while testing (#6607)
[openemr.git] / src / RestControllers / AuthorizationController.php
bloba1e8aa9d2e77f0b861ad249a82cec9f7c91f51e0
1 <?php
3 /**
4 * Authorization Server Member
6 * @package OpenEMR
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");
19 use DateInterval;
20 use DateTimeImmutable;
21 use Exception;
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\HttpUtils;
63 use OpenEMR\Common\Utils\RandomGenUtils;
64 use OpenEMR\Common\Uuid\UuidRegistry;
65 use OpenEMR\FHIR\Config\ServerConfig;
66 use OpenEMR\FHIR\SMART\SmartLaunchController;
67 use OpenEMR\RestControllers\SMART\SMARTAuthorizationController;
68 use OpenEMR\Services\TrustedUserService;
69 use OpenIDConnectServer\ClaimExtractor;
70 use OpenIDConnectServer\Entities\ClaimSetEntity;
71 use Psr\Http\Message\ResponseInterface;
72 use Psr\Http\Message\ServerRequestInterface;
73 use Psr\Log\LoggerInterface;
74 use RestConfig;
75 use RuntimeException;
77 class AuthorizationController
79 use CryptTrait;
81 public const ENDPOINT_SCOPE_AUTHORIZE_CONFIRM = "/scope-authorize-confirm";
83 public const GRANT_TYPE_PASSWORD = 'password';
84 public const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials';
85 public const OFFLINE_ACCESS_SCOPE = 'offline_access';
87 public $authBaseUrl;
88 public $authBaseFullUrl;
89 public $siteId;
90 private $privateKey;
91 private $passphrase;
92 private $publicKey;
93 private $oaEncryptionKey;
94 private $grantType;
95 private $providerForm;
96 private $authRequestSerial;
97 private $cryptoGen;
98 private $userId;
101 * @var SMARTAuthorizationController
103 private $smartAuthController;
106 * @var LoggerInterface
108 private $logger;
111 * Handles CRUD operations for OAUTH2 Trusted Users
112 * @var TrustedUserService
114 private $trustedUserService;
117 * @var RestConfig
119 private $restConfig;
121 public function __construct($providerForm = true)
123 if (is_callable([RestConfig::class, 'GetInstance'])) {
124 $gbl = RestConfig::GetInstance();
125 } else {
126 $gbl = \RestConfig::GetInstance();
128 $this->restConfig = $gbl;
129 $this->logger = new SystemLogger();
131 $this->siteId = $_SESSION['site_id'] ?? $gbl::$SITE;
132 $this->authBaseUrl = $GLOBALS['webroot'] . '/oauth2/' . $this->siteId;
133 $this->authBaseFullUrl = self::getAuthBaseFullURL();
134 // used for session stash
135 $this->authRequestSerial = $_SESSION['authRequestSerial'] ?? '';
136 // Create a crypto object that will be used for for encryption/decryption
137 $this->cryptoGen = new CryptoGen();
138 // verify and/or setup our key pairs.
139 $this->configKeyPairs();
141 // true will display client/user server sign in. false, not.
142 $this->providerForm = $providerForm;
144 $this->smartAuthController = new SMARTAuthorizationController(
145 $this->logger,
146 $this->authBaseFullUrl,
147 $this->authBaseFullUrl . self::ENDPOINT_SCOPE_AUTHORIZE_CONFIRM,
148 __DIR__ . "/../../oauth2/"
151 $this->trustedUserService = new TrustedUserService();
154 private function configKeyPairs(): void
156 $response = $this->createServerResponse();
157 try {
158 $oauth2KeyConfig = new OAuth2KeyConfig($GLOBALS['OE_SITE_DIR']);
159 $oauth2KeyConfig->configKeyPairs();
160 $this->privateKey = $oauth2KeyConfig->getPrivateKeyLocation();
161 $this->publicKey = $oauth2KeyConfig->getPublicKeyLocation();
162 $this->oaEncryptionKey = $oauth2KeyConfig->getEncryptionKey();
163 $this->passphrase = $oauth2KeyConfig->getPassPhrase();
164 } catch (OAuth2KeyException $exception) {
165 $this->logger->error("OpenEMR error - " . $exception->getMessage() . ", so forced exit");
166 $serverException = OAuthServerException::serverError(
167 "Security error - problem with authorization server keys.",
168 $exception
170 SessionUtil::oauthSessionCookieDestroy();
171 $this->emitResponse($serverException->generateHttpResponse($response));
172 exit;
176 public function clientRegistration(): void
178 $response = $this->createServerResponse();
179 $request = $this->createServerRequest();
180 $headers = array();
181 try {
182 $request_headers = $request->getHeaders();
183 foreach ($request_headers as $header => $value) {
184 $headers[strtolower($header)] = $value[0];
186 if (!$headers['content-type'] || strpos($headers['content-type'], 'application/json') !== 0) {
187 throw new OAuthServerException('Unexpected content type', 0, 'invalid_client_metadata');
189 $json = file_get_contents('php://input');
190 if (!$json) {
191 throw new OAuthServerException('No JSON body', 0, 'invalid_client_metadata');
193 $data = json_decode($json, true);
194 if (!$data) {
195 throw new OAuthServerException('Invalid JSON', 0, 'invalid_client_metadata');
197 // many of these are optional and are here if we want to implement
198 $keys = array('contacts' => null,
199 'application_type' => null,
200 'client_name' => null,
201 'logo_uri' => null,
202 'redirect_uris' => null,
203 'post_logout_redirect_uris' => null,
204 'token_endpoint_auth_method' => array('client_secret_basic', 'client_secret_post'),
205 'policy_uri' => null,
206 'tos_uri' => null,
207 'jwks_uri' => null,
208 'jwks' => null,
209 'sector_identifier_uri' => null,
210 'subject_type' => array('pairwise', 'public'),
211 'default_max_age' => null,
212 'require_auth_time' => null,
213 'default_acr_values' => null,
214 'initiate_login_uri' => null, // for anything with a SMART 'launch/ehr' context we need to know how to initiate the login
215 'request_uris' => null,
216 'response_types' => null,
217 'grant_types' => null,
218 // info on scope can be seen at
219 // OAUTH2 Dynamic Client Registration RFC 7591 Section 2 Page 9
220 // @see https://tools.ietf.org/html/rfc7591#section-2
221 'scope' => null
223 $clientRepository = new ClientRepository();
224 $client_id = $clientRepository->generateClientId();
225 $reg_token = $clientRepository->generateRegistrationAccessToken();
226 $reg_client_uri_path = $clientRepository->generateRegistrationClientUriPath();
227 $params = array(
228 'client_id' => $client_id,
229 'client_id_issued_at' => time(),
230 'registration_access_token' => $reg_token,
231 'registration_client_uri_path' => $reg_client_uri_path
234 $params['client_role'] = 'patient';
235 // only include secret if a confidential app else force PKCE for native and web apps.
236 $client_secret = '';
237 if ($data['application_type'] === 'private') {
238 $client_secret = $clientRepository->generateClientSecret();
239 $params['client_secret'] = $client_secret;
240 $params['client_role'] = 'user';
242 // don't allow system scopes without a jwk or jwks_uri value
243 if (
244 strpos($data['scope'], 'system/') !== false
245 && empty($data['jwks']) && empty($data['jwks_uri'])
247 throw new OAuthServerException('jwks is invalid', 0, 'invalid_client_metadata');
249 // don't allow user, system scopes, and offline_access for public apps
250 } elseif (
251 strpos($data['scope'], 'system/') !== false
252 || strpos($data['scope'], 'user/') !== false
254 throw new OAuthServerException("system and user scopes are only allowed for confidential clients", 0, 'invalid_client_metadata');
256 $this->validateScopesAgainstServerApprovedScopes($data['scope']);
258 foreach ($keys as $key => $supported_values) {
259 if (isset($data[$key])) {
260 if (in_array($key, array('contacts', 'redirect_uris', 'request_uris', 'post_logout_redirect_uris', 'grant_types', 'response_types', 'default_acr_values'))) {
261 $params[$key] = implode('|', $data[$key]);
262 } elseif ($key === 'jwks') {
263 $params[$key] = json_encode($data[$key]);
264 } else {
265 $params[$key] = $data[$key];
267 if (!empty($supported_values)) {
268 if (!in_array($params[$key], $supported_values)) {
269 throw new OAuthServerException("Unsupported $key value : $params[$key]", 0, 'invalid_client_metadata');
274 if (!isset($data['redirect_uris'])) {
275 throw new OAuthServerException('redirect_uris is invalid', 0, 'invalid_redirect_uri');
277 if (isset($data['post_logout_redirect_uris']) && empty($data['post_logout_redirect_uris'])) {
278 throw new OAuthServerException('post_logout_redirect_uris is invalid', 0, 'invalid_client_metadata');
280 // save to oauth client table
281 try {
282 $clientRepository->insertNewClient($client_id, $params, $this->siteId);
283 } catch (\Exception $exception) {
284 throw OAuthServerException::serverError("Try again. Unable to create account", $exception);
286 $reg_uri = $this->authBaseFullUrl . '/client/' . $reg_client_uri_path;
287 unset($params['registration_client_uri_path']);
288 $client_json = array(
289 'client_id' => $client_id,
290 'client_secret' => $client_secret,
291 'registration_access_token' => $reg_token,
292 'registration_client_uri' => $reg_uri,
293 'client_id_issued_at' => time(),
294 'client_secret_expires_at' => 0
296 $array_params = array('contacts', 'redirect_uris', 'request_uris', 'post_logout_redirect_uris', 'response_types', 'grant_types', 'default_acr_values');
297 foreach ($array_params as $aparam) {
298 if (isset($params[$aparam])) {
299 $params[$aparam] = explode('|', $params[$aparam]);
302 if (!empty($params['jwks'])) {
303 $params['jwks'] = json_decode($params['jwks'], true);
305 if (isset($params['require_auth_time'])) {
306 $params['require_auth_time'] = ($params['require_auth_time'] === 1);
308 // send response
309 $response->withHeader("Cache-Control", "no-store");
310 $response->withHeader("Pragma", "no-cache");
311 $response->withHeader('Content-Type', 'application/json');
312 $body = $response->getBody();
313 $body->write(json_encode(array_merge($client_json, $params)));
315 SessionUtil::oauthSessionCookieDestroy();
316 $this->emitResponse($response->withStatus(200)->withBody($body));
317 } catch (OAuthServerException $exception) {
318 SessionUtil::oauthSessionCookieDestroy();
319 $this->emitResponse($exception->generateHttpResponse($response));
324 * Verifies that the scope string only has approved scopes for the system.
325 * @param $scopeString the space separated scope string
327 private function validateScopesAgainstServerApprovedScopes($scopeString)
329 $requestScopes = explode(" ", $scopeString);
330 if (empty($requestScopes)) {
331 return;
334 $scopeRepo = new ScopeRepository($this->restConfig);
335 $scopeRepo->setRequestScopes($scopeString);
336 foreach ($requestScopes as $scope) {
337 $validScope = $scopeRepo->getScopeEntityByIdentifier($scope);
338 if (empty($validScope)) {
339 throw OAuthServerException::invalidScope($scope);
344 private function createServerResponse(): ResponseInterface
346 return (new Psr17Factory())->createResponse();
349 private function createServerRequest(): ServerRequestInterface
351 $psr17Factory = new Psr17Factory();
353 return (new ServerRequestCreator(
354 $psr17Factory, // ServerRequestFactory
355 $psr17Factory, // UriFactory
356 $psr17Factory, // UploadedFileFactory
357 $psr17Factory // StreamFactory
358 ))->fromGlobals();
361 public function base64url_encode($data): string
363 return HttpUtils::base64url_encode($data);
366 public function base64url_decode($token)
368 $b64 = strtr($token, '-_', '+/');
369 return base64_decode($b64);
372 public function emitResponse($response): void
374 if (headers_sent()) {
375 throw new RuntimeException('Headers already sent.');
377 $statusLine = sprintf(
378 'HTTP/%s %s %s',
379 $response->getProtocolVersion(),
380 $response->getStatusCode(),
381 $response->getReasonPhrase()
383 header($statusLine, true);
384 foreach ($response->getHeaders() as $name => $values) {
385 $responseHeader = sprintf('%s: %s', $name, $response->getHeaderLine($name));
386 header($responseHeader, false);
388 // send it along.
389 echo $response->getBody();
392 public function clientRegisteredDetails(): void
394 $response = $this->createServerResponse();
396 try {
397 $token = $_REQUEST['access_token'];
398 if (!$token) {
399 $token = $this->getBearerToken();
400 if (!$token) {
401 throw new OAuthServerException('No Access Code', 0, 'invalid_request', 403);
404 $pos = strpos($_SERVER['PATH_INFO'], '/client/');
405 if ($pos === false) {
406 throw new OAuthServerException('Invalid path', 0, 'invalid_request', 403);
408 $uri_path = substr($_SERVER['PATH_INFO'], $pos + 8);
409 $client = sqlQuery("SELECT * FROM `oauth_clients` WHERE `registration_uri_path` = ?", array($uri_path));
410 if (!$client) {
411 throw new OAuthServerException('Invalid client', 0, 'invalid_request', 403);
413 if ($client['registration_access_token'] !== $token) {
414 throw new OAuthServerException('Invalid registration token', 0, 'invalid _request', 403);
416 $params['client_id'] = $client['client_id'];
417 $params['client_secret'] = $this->cryptoGen->decryptStandard($client['client_secret']);
418 $params['contacts'] = explode('|', $client['contacts']);
419 $params['application_type'] = $client['client_role'];
420 $params['client_name'] = $client['client_name'];
421 $params['redirect_uris'] = explode('|', $client['redirect_uri']);
423 $response->withHeader("Cache-Control", "no-store");
424 $response->withHeader("Pragma", "no-cache");
425 $response->withHeader('Content-Type', 'application/json');
426 $body = $response->getBody();
427 $body->write(json_encode($params));
429 SessionUtil::oauthSessionCookieDestroy();
430 $this->emitResponse($response->withStatus(200)->withBody($body));
431 } catch (OAuthServerException $exception) {
432 SessionUtil::oauthSessionCookieDestroy();
433 $this->emitResponse($exception->generateHttpResponse($response));
437 public function getBearerToken(): string
439 $request = $this->createServerRequest();
440 $request_headers = $request->getHeaders();
441 $headers = [];
442 foreach ($request_headers as $header => $value) {
443 $headers[strtolower($header)] = $value[0];
445 $authorization = $headers['authorization'];
446 if ($authorization) {
447 $pieces = explode(' ', $authorization);
448 if (strcasecmp($pieces[0], 'bearer') !== 0) {
449 return "";
452 return rtrim($pieces[1]);
454 return "";
457 public function oauthAuthorizationFlow(): void
459 $this->logger->debug("AuthorizationController->oauthAuthorizationFlow() starting authorization flow");
460 $response = $this->createServerResponse();
461 $request = $this->createServerRequest();
463 if ($nonce = $request->getQueryParams()['nonce'] ?? null) {
464 $_SESSION['nonce'] = $request->getQueryParams()['nonce'];
467 $this->logger->debug("AuthorizationController->oauthAuthorizationFlow() request query params ", ["queryParams" => $request->getQueryParams()]);
469 $this->grantType = 'authorization_code';
470 $server = $this->getAuthorizationServer();
471 try {
472 // Validate the HTTP request and return an AuthorizationRequest object.
473 $this->logger->debug("AuthorizationController->oauthAuthorizationFlow() attempting to validate auth request");
474 $authRequest = $server->validateAuthorizationRequest($request);
475 $this->logger->debug("AuthorizationController->oauthAuthorizationFlow() auth request validated, csrf,scopes,client_id setup");
476 $_SESSION['csrf'] = $authRequest->getState();
477 $_SESSION['scopes'] = $request->getQueryParams()['scope'];
478 $_SESSION['client_id'] = $request->getQueryParams()['client_id'];
479 $_SESSION['client_role'] = $authRequest->getClient()->getClientRole();
480 $_SESSION['launch'] = $request->getQueryParams()['launch'] ?? null;
481 $_SESSION['redirect_uri'] = $authRequest->getRedirectUri() ?? null;
482 $this->logger->debug("AuthorizationController->oauthAuthorizationFlow() session updated", ['session' => $_SESSION]);
483 // If needed, serialize into a users session
484 if ($this->providerForm) {
485 $this->serializeUserSession($authRequest, $request);
486 $this->logger->debug("AuthorizationController->oauthAuthorizationFlow() redirecting to provider form");
487 // call our login then login calls authorize if approved by user
488 header("Location: " . $this->authBaseUrl . "/provider/login", true, 301);
489 exit;
491 } catch (OAuthServerException $exception) {
492 $this->logger->error(
493 "AuthorizationController->oauthAuthorizationFlow() OAuthServerException",
494 ["hint" => $exception->getHint(), "message" => $exception->getMessage()
495 , 'payload' => $exception->getPayload()
496 , 'trace' => $exception->getTraceAsString()
497 , 'redirectUri' => $exception->getRedirectUri()
498 , 'errorType' => $exception->getErrorType()]
500 SessionUtil::oauthSessionCookieDestroy();
501 $this->emitResponse($exception->generateHttpResponse($response));
502 } catch (Exception $exception) {
503 $this->logger->error("AuthorizationController->oauthAuthorizationFlow() Exception message: " . $exception->getMessage());
504 SessionUtil::oauthSessionCookieDestroy();
505 $body = $response->getBody();
506 $body->write($exception->getMessage());
507 $this->emitResponse($response->withStatus(500)->withBody($body));
512 * Retrieve the authorization server with all of the grants configured
513 * TODO: @adunsulag is there a better way to handle skipping the refresh token on the authorization grant?
514 * Due to the way the server is created and the fact we have to skip the refresh token when an offline_scope is passed
515 * for authorization_grant/password_grant. We ignore offline_scope for custom_credentials
516 * @param bool $includeAuthGrantRefreshToken Whether the authorization server should issue a refresh token for an authorization grant.
517 * @return AuthorizationServer
518 * @throws Exception
520 public function getAuthorizationServer($includeAuthGrantRefreshToken = true): AuthorizationServer
522 $protectedClaims = ['profile', 'email', 'address', 'phone'];
523 $scopeRepository = new ScopeRepository($this->restConfig);
524 $claims = $scopeRepository->getSupportedClaims();
525 $customClaim = [];
526 foreach ($claims as $claim) {
527 if (in_array($claim, $protectedClaims, true)) {
528 continue;
530 $customClaim[] = new ClaimSetEntity($claim, [$claim]);
532 if (!empty($_SESSION['nonce'])) {
533 // nonce scope added later. this is for id token nonce claim.
534 $customClaim[] = new ClaimSetEntity('nonce', ['nonce']);
537 // OpenID Connect Response Type
538 $this->logger->debug("AuthorizationController->getAuthorizationServer() creating server");
539 $responseType = new IdTokenSMARTResponse(new IdentityRepository(), new ClaimExtractor($customClaim));
541 if (empty($this->grantType)) {
542 $this->grantType = 'authorization_code';
545 // responseType is cloned inside the league auth server so we have to handle changes here before we send
546 // into the $authServer the $responseType
547 if ($this->grantType === 'authorization_code') {
548 $responseType->markIsAuthorizationGrant(); // we have specific SMART responses for an authorization grant.
551 $authServer = new AuthorizationServer(
552 new ClientRepository(),
553 new AccessTokenRepository(),
554 new ScopeRepository($this->restConfig),
555 new CryptKey($this->privateKey, $this->passphrase),
556 $this->oaEncryptionKey,
557 $responseType
560 $this->logger->debug("AuthorizationController->getAuthorizationServer() grantType is " . $this->grantType);
561 if ($this->grantType === 'authorization_code') {
562 $this->logger->debug(
563 "logging global params",
564 ['site_addr_oath' => $GLOBALS['site_addr_oath'], 'web_root' => $GLOBALS['web_root'], 'site_id' => $_SESSION['site_id']]
566 $fhirServiceConfig = new ServerConfig();
567 $expectedAudience = [
568 $fhirServiceConfig->getFhirUrl(),
569 $GLOBALS['site_addr_oath'] . $GLOBALS['web_root'] . '/apis/' . $_SESSION['site_id'] . "/api",
570 $GLOBALS['site_addr_oath'] . $GLOBALS['web_root'] . '/apis/' . $_SESSION['site_id'] . "/portal",
572 $grant = new CustomAuthCodeGrant(
573 new AuthCodeRepository(),
574 new RefreshTokenRepository($includeAuthGrantRefreshToken),
575 new \DateInterval('PT1M'), // auth code. should be short turn around.
576 $expectedAudience
579 $grant->setRefreshTokenTTL(new \DateInterval('P3M')); // minimum per ONC
580 $authServer->enableGrantType(
581 $grant,
582 new \DateInterval('PT1H') // access token
585 if ($this->grantType === 'refresh_token') {
586 $grant = new CustomRefreshTokenGrant(new RefreshTokenRepository());
587 $grant->setRefreshTokenTTL(new \DateInterval('P3M'));
588 $authServer->enableGrantType(
589 $grant,
590 new \DateInterval('PT1H') // The new access token will expire after 1 hour
593 // TODO: break this up - throw exception for not turned on.
594 if (!empty($GLOBALS['oauth_password_grant']) && ($this->grantType === self::GRANT_TYPE_PASSWORD)) {
595 $grant = new CustomPasswordGrant(
596 new UserRepository(),
597 new RefreshTokenRepository($includeAuthGrantRefreshToken)
599 $grant->setRefreshTokenTTL(new DateInterval('P3M'));
600 $authServer->enableGrantType(
601 $grant,
602 new \DateInterval('PT1H') // access token
605 if ($this->grantType === self::GRANT_TYPE_CLIENT_CREDENTIALS) {
606 // Enable the client credentials grant on the server
607 $client_credentials = new CustomClientCredentialsGrant(AuthorizationController::getAuthBaseFullURL() . AuthorizationController::getTokenPath());
608 $client_credentials->setLogger($this->logger);
609 $client_credentials->setHttpClient(new Client()); // set our guzzle client here
610 $authServer->enableGrantType(
611 $client_credentials,
612 // https://hl7.org/fhir/uv/bulkdata/authorization/index.html#issuing-access-tokens Spec states 5 min max
613 new \DateInterval('PT300S')
617 $this->logger->debug("AuthorizationController->getAuthorizationServer() authServer created");
618 return $authServer;
621 private function serializeUserSession($authRequest, ServerRequestInterface $httpRequest): void
623 $launchParam = isset($httpRequest->getQueryParams()['launch']) ? $httpRequest->getQueryParams()['launch'] : null;
624 // keeping somewhat granular
625 try {
626 $scopes = $authRequest->getScopes();
627 $scoped = [];
628 foreach ($scopes as $scope) {
629 $scoped[] = $scope->getIdentifier();
631 $client['name'] = $authRequest->getClient()->getName();
632 $client['redirectUri'] = $authRequest->getClient()->getRedirectUri();
633 $client['identifier'] = $authRequest->getClient()->getIdentifier();
634 $client['isConfidential'] = $authRequest->getClient()->isConfidential();
635 $outer = array(
636 'grantTypeId' => $authRequest->getGrantTypeId(),
637 'authorizationApproved' => false,
638 'redirectUri' => $authRequest->getRedirectUri(),
639 'state' => $authRequest->getState(),
640 'codeChallenge' => $authRequest->getCodeChallenge(),
641 'codeChallengeMethod' => $authRequest->getCodeChallengeMethod(),
643 $result = array('outer' => $outer, 'scopes' => $scoped, 'client' => $client);
644 $this->authRequestSerial = json_encode($result, JSON_THROW_ON_ERROR);
645 $_SESSION['authRequestSerial'] = $this->authRequestSerial;
646 } catch (Exception $e) {
647 echo $e;
651 public function userLogin(): void
653 $response = $this->createServerResponse();
655 $patientRoleSupport = (!empty($GLOBALS['rest_portal_api']) || !empty($GLOBALS['rest_fhir_api']));
657 if (empty($_POST['username']) && empty($_POST['password'])) {
658 $this->logger->debug("AuthorizationController->userLogin() presenting blank login form");
659 $oauthLogin = true;
660 $redirect = $this->authBaseUrl . "/login";
661 require_once(__DIR__ . "/../../oauth2/provider/login.php");
662 exit();
664 $continueLogin = false;
665 if (isset($_POST['user_role'])) {
666 if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"], 'oauth2')) {
667 $this->logger->error("AuthorizationController->userLogin() Invalid CSRF token");
668 CsrfUtils::csrfNotVerified(false, true, false);
669 unset($_POST['username'], $_POST['password']);
670 $invalid = "Sorry. Invalid CSRF!"; // todo: display error
671 $oauthLogin = true;
672 $redirect = $this->authBaseUrl . "/login";
673 require_once(__DIR__ . "/../../oauth2/provider/login.php");
674 exit();
675 } else {
676 $this->logger->debug("AuthorizationController->userLogin() verifying login information");
677 $continueLogin = $this->verifyLogin($_POST['username'], $_POST['password'], ($_POST['email'] ?? ''), $_POST['user_role']);
678 $this->logger->debug("AuthorizationController->userLogin() verifyLogin result", ["continueLogin" => $continueLogin]);
682 if (!$continueLogin) {
683 $this->logger->debug("AuthorizationController->userLogin() login invalid, presenting login form");
684 $invalid = "Sorry, Invalid!"; // todo: display error
685 $oauthLogin = true;
686 $redirect = $this->authBaseUrl . "/login";
687 require_once(__DIR__ . "/../../oauth2/provider/login.php");
688 exit();
689 } else {
690 $this->logger->debug("AuthorizationController->userLogin() login valid, continuing oauth process");
693 //Require MFA if turned on
694 $mfa = new MfaUtils($this->userId);
695 $mfaToken = $mfa->tokenFromRequest($_POST['mfa_type'] ?? null);
696 $mfaType = $mfa->getType();
697 $TOTP = MfaUtils::TOTP;
698 $U2F = MfaUtils::U2F;
699 if ($_POST['user_role'] === 'api' && $mfa->isMfaRequired() && is_null($mfaToken)) {
700 $oauthLogin = true;
701 $mfaRequired = true;
702 $redirect = $this->authBaseUrl . "/login";
703 if (in_array(MfaUtils::U2F, $mfaType)) {
704 $appId = $mfa->getAppId();
705 $requests = $mfa->getU2fRequests();
707 require_once(__DIR__ . "/../../oauth2/provider/login.php");
708 exit();
710 //Check the validity of the authentication token
711 if ($_POST['user_role'] === 'api' && $mfa->isMfaRequired() && !is_null($mfaToken)) {
712 if (!$mfaToken || !$mfa->check($mfaToken, $_POST['mfa_type'])) {
713 $invalid = "Sorry, Invalid code!";
714 $oauthLogin = true;
715 $mfaRequired = true;
716 $mfaType = $mfa->getType();
717 $redirect = $this->authBaseUrl . "/login";
718 require_once(__DIR__ . "/../../oauth2/provider/login.php");
719 exit();
723 unset($_POST['username'], $_POST['password']);
724 $_SESSION['persist_login'] = isset($_POST['persist_login']) ? 1 : 0;
725 $user = new UserEntity();
726 $user->setIdentifier($_SESSION['user_id']);
727 $_SESSION['claims'] = $user->getClaims();
728 $oauthLogin = true;
729 // need to redirect to patient select if we have a launch context && this isn't a patient login
730 $authorize = 'authorize';
732 // if we need to authorize any smart context as part of our OAUTH handler we do that here
733 // otherwise we send on to our scope authorization confirm.
734 if ($this->smartAuthController->needSmartAuthorization()) {
735 $redirect = $this->authBaseFullUrl . $this->smartAuthController->getSmartAuthorizationPath();
736 } else {
737 $redirect = $this->authBaseFullUrl . self::ENDPOINT_SCOPE_AUTHORIZE_CONFIRM;
739 $this->logger->debug("AuthorizationController->userLogin() complete redirecting", ["scopes" => $_SESSION['scopes']
740 , 'claims' => $_SESSION['claims'], 'redirect' => $redirect]);
742 header("Location: $redirect");
743 exit;
746 public function scopeAuthorizeConfirm()
748 // TODO: @adunsulag if there are no scopes or claims here we probably want to show an error...
750 // TODO: @adunsulag this is also where we want to show a special message if the offline scope is present.
752 // show our scope auth piece
753 $oauthLogin = true;
754 $redirect = $this->authBaseUrl . "/device/code";
755 $scopeString = $_SESSION['scopes'] ?? '';
756 // check for offline_access
758 $scopesList = explode(' ', $scopeString);
759 $offline_requested = false;
760 $scopes = [];
761 foreach ($scopesList as $scope) {
762 if ($scope !== self::OFFLINE_ACCESS_SCOPE) {
763 $scopes[] = $scope;
764 } else {
765 $offline_requested = true;
768 $offline_access_date = (new DateTimeImmutable())->add(new \DateInterval("P3M"))->format("Y-m-d");
771 $claims = $_SESSION['claims'] ?? [];
773 $clientRepository = new ClientRepository();
774 $client = $clientRepository->getClientEntity($_SESSION['client_id']);
775 $clientName = "<" . xl("Client Name Not Found") . ">";
776 if (!empty($client)) {
777 $clientName = $client->getName();
780 $uuidToUser = new UuidUserAccount($_SESSION['user_id']);
781 $userRole = $uuidToUser->getUserRole();
782 $userAccount = $uuidToUser->getUserAccount();
783 require_once(__DIR__ . "/../../oauth2/provider/scope-authorize.php");
787 * Checks if we are in a SMART authorization endpoint
788 * @param $end_point
789 * @return bool
791 public function isSMARTAuthorizationEndPoint($end_point)
793 return $this->smartAuthController->isValidRoute($end_point);
797 * Route handler for any SMART authorization contexts that we need for OpenEMR
798 * @param $end_point
800 public function dispatchSMARTAuthorizationEndpoint($end_point)
802 return $this->smartAuthController->dispatchRoute($end_point);
805 private function verifyLogin($username, $password, $email = '', $type = 'api'): bool
807 $auth = new AuthUtils($type);
808 $is_true = $auth->confirmPassword($username, $password, $email);
809 if (!$is_true) {
810 $this->logger->debug("AuthorizationController->verifyLogin() login attempt failed", ['username' => $username, 'email' => $email, 'type' => $type]);
811 return false;
813 // TODO: should user_id be set to be a uuid here?
814 if ($this->userId = $auth->getUserId()) {
815 $_SESSION['user_id'] = $this->getUserUuid($this->userId, 'users');
816 $this->logger->debug("AuthorizationController->verifyLogin() user login", ['user_id' => $_SESSION['user_id'],
817 'username' => $username, 'email' => $email, 'type' => $type]);
818 return true;
820 if ($id = $auth->getPatientId()) {
821 $puuid = $this->getUserUuid($id, 'patient');
822 // TODO: @adunsulag check with @sjpadgett on where this user_id is even used as we are assigning it to be a uuid
823 $_SESSION['user_id'] = $puuid;
824 $this->logger->debug("AuthorizationController->verifyLogin() patient login", ['pid' => $_SESSION['user_id']
825 , 'username' => $username, 'email' => $email, 'type' => $type]);
826 $_SESSION['pid'] = $id;
827 $_SESSION['puuid'] = $puuid;
828 return true;
831 return false;
834 protected function getUserUuid($userId, $userRole): string
836 switch ($userRole) {
837 case 'users':
838 UuidRegistry::createMissingUuidsForTables(['users']);
839 $account_sql = "SELECT `uuid` FROM `users` WHERE `id` = ?";
840 break;
841 case 'patient':
842 UuidRegistry::createMissingUuidsForTables(['patient_data']);
843 $account_sql = "SELECT `uuid` FROM `patient_data` WHERE `pid` = ?";
844 break;
845 default:
846 return '';
848 $id = sqlQueryNoLog($account_sql, array($userId))['uuid'];
850 return UuidRegistry::uuidToString($id);
854 * Note this corresponds with the /auth/code endpoint
856 public function authorizeUser(): void
858 $this->logger->debug("AuthorizationController->authorizeUser() starting authorization");
859 $response = $this->createServerResponse();
860 $authRequest = $this->deserializeUserSession();
861 try {
862 $authRequest = $this->updateAuthRequestWithUserApprovedScopes($authRequest, $_POST['scope']);
863 $include_refresh_token = $this->shouldIncludeRefreshTokenForScopes($authRequest->getScopes());
864 $server = $this->getAuthorizationServer($include_refresh_token);
865 $user = new UserEntity();
866 $user->setIdentifier($_SESSION['user_id']);
867 $authRequest->setUser($user);
868 $authRequest->setAuthorizationApproved(true);
869 $result = $server->completeAuthorizationRequest($authRequest, $response);
870 $redirect = $result->getHeader('Location')[0];
871 $authorization = parse_url($redirect, PHP_URL_QUERY);
872 // stash appropriate session for token endpoint.
873 unset($_SESSION['authRequestSerial']);
874 unset($_SESSION['claims']);
875 $csrf_private_key = $_SESSION['csrf_private_key']; // switcheroo so this does not end up in the session cache
876 unset($_SESSION['csrf_private_key']);
877 $session_cache = json_encode($_SESSION, JSON_THROW_ON_ERROR);
878 $_SESSION['csrf_private_key'] = $csrf_private_key;
879 unset($csrf_private_key);
880 $code = [];
881 // parse scope as also a query param if needed
882 parse_str($authorization, $code);
883 $code = $code["code"];
884 if (isset($_POST['proceed']) && !empty($code) && !empty($session_cache)) {
885 if (!CsrfUtils::verifyCsrfToken($_POST["csrf_token_form"], 'oauth2')) {
886 CsrfUtils::csrfNotVerified(false, true, false);
887 throw OAuthServerException::serverError("Failed authorization due to failed CSRF check.");
888 } else {
889 $this->saveTrustedUser($_SESSION['client_id'], $_SESSION['user_id'], $_SESSION['scopes'], $_SESSION['persist_login'], $code, $session_cache);
891 } else {
892 if (empty($_SESSION['csrf'])) {
893 throw OAuthServerException::serverError("Failed authorization due to missing data.");
896 // Return the HTTP redirect response. Redirect is to client callback.
897 $this->logger->debug("AuthorizationController->authorizeUser() sending server response");
898 SessionUtil::oauthSessionCookieDestroy();
899 $this->emitResponse($result);
900 exit;
901 } catch (Exception $exception) {
902 $this->logger->error("AuthorizationController->authorizeUser() Exception thrown", ["message" => $exception->getMessage()]);
903 SessionUtil::oauthSessionCookieDestroy();
904 $body = $response->getBody();
905 $body->write($exception->getMessage());
906 $this->emitResponse($response->withStatus(500)->withBody($body));
911 * @param ScopeEntityInterface[] $scopes
913 private function shouldIncludeRefreshTokenForScopes(array $scopes)
915 foreach ($scopes as $scope) {
916 if ($scope->getIdentifier() == self::OFFLINE_ACCESS_SCOPE) {
917 return true;
920 return false;
923 private function updateAuthRequestWithUserApprovedScopes(AuthorizationRequest $request, $approvedScopes)
925 $this->logger->debug(
926 "AuthorizationController->updateAuthRequestWithUserApprovedScopes() attempting to update auth request with user approved scopes",
927 ['userApprovedScopes' => $approvedScopes ]
929 $requestScopes = $request->getScopes();
930 $scopeUpdates = [];
931 // we only allow scopes from the original session request, if user approved scope it will show up here.
932 foreach ($requestScopes as $scope) {
933 if (isset($approvedScopes[$scope->getIdentifier()])) {
934 $scopeUpdates[] = $scope;
937 $this->logger->debug(
938 "AuthorizationController->updateAuthRequestWithUserApprovedScopes() replaced request scopes with user approved scopes",
939 ['updatedScopes' => $scopeUpdates]
942 $request->setScopes($scopeUpdates);
943 return $request;
946 private function deserializeUserSession(): AuthorizationRequest
948 $authRequest = new AuthorizationRequest();
949 try {
950 $requestData = $_SESSION['authRequestSerial'] ?? $this->authRequestSerial;
951 $restore = json_decode($requestData, true, 512);
952 $outer = $restore['outer'];
953 $client = $restore['client'];
954 $scoped = $restore['scopes'];
955 $authRequest->setGrantTypeId($outer['grantTypeId']);
956 $e = new ClientEntity();
957 $e->setName($client['name']);
958 $e->setRedirectUri($client['redirectUri']);
959 $e->setIdentifier($client['identifier']);
960 $e->setIsConfidential($client['isConfidential']);
961 $authRequest->setClient($e);
962 $scopes = [];
963 foreach ($scoped as $scope) {
964 $s = new ScopeEntity();
965 $s->setIdentifier($scope);
966 $scopes[] = $s;
968 $authRequest->setScopes($scopes);
969 $authRequest->setAuthorizationApproved($outer['authorizationApproved']);
970 $authRequest->setRedirectUri($outer['redirectUri']);
971 $authRequest->setState($outer['state']);
972 $authRequest->setCodeChallenge($outer['codeChallenge']);
973 $authRequest->setCodeChallengeMethod($outer['codeChallengeMethod']);
974 } catch (Exception $e) {
975 echo $e;
978 return $authRequest;
982 * Note this corresponds with the /token endpoint
984 public function oauthAuthorizeToken(): void
986 $this->logger->debug("AuthorizationController->oauthAuthorizeToken() starting request");
987 $response = $this->createServerResponse();
988 $request = $this->createServerRequest();
990 if ($request->getMethod() == 'OPTIONS') {
991 // nothing to do here, just return
992 $this->emitResponse($response->withStatus(200));
993 return;
996 // authorization code which is normally only sent for new tokens
997 // by the authorization grant flow.
998 $code = $request->getParsedBody()['code'] ?? null;
999 // grantType could be authorization_code, password or refresh_token.
1000 $this->grantType = $request->getParsedBody()['grant_type'];
1001 $this->logger->debug("AuthorizationController->oauthAuthorizeToken() grant type received", ['grant_type' => $this->grantType]);
1002 if ($this->grantType === 'authorization_code') {
1003 // re-populate from saved session cache populated in authorizeUser().
1004 $ssbc = $this->sessionUserByCode($code);
1005 $_SESSION = json_decode($ssbc['session_cache'], true);
1006 $this->logger->debug("AuthorizationController->oauthAuthorizeToken() restored session user from code ", ['session' => $_SESSION]);
1008 // TODO: explore why we create the request again...
1009 if ($this->grantType === 'refresh_token') {
1010 $request = $this->createServerRequest();
1012 // Finally time to init the server.
1013 $server = $this->getAuthorizationServer();
1014 try {
1015 if (($this->grantType === 'authorization_code') && empty($_SESSION['csrf'])) {
1016 // the saved session was not populated as expected
1017 $this->logger->error("AuthorizationController->oauthAuthorizeToken() CSRF check failed");
1018 throw new OAuthServerException('Bad request', 0, 'invalid_request', 400);
1020 $result = $server->respondToAccessTokenRequest($request, $response);
1021 // save a password trusted user
1022 if ($this->grantType === self::GRANT_TYPE_PASSWORD) {
1023 $this->saveTrustedUserForPasswordGrant($result);
1025 SessionUtil::oauthSessionCookieDestroy();
1026 $this->emitResponse($result);
1027 } catch (OAuthServerException $exception) {
1028 $this->logger->debug(
1029 "AuthorizationController->oauthAuthorizeToken() OAuthServerException occurred",
1030 ["hint" => $exception->getHint(), "message" => $exception->getMessage(), "stack" => $exception->getTraceAsString()]
1032 SessionUtil::oauthSessionCookieDestroy();
1033 $this->emitResponse($exception->generateHttpResponse($response));
1034 } catch (Exception $exception) {
1035 $this->logger->error(
1036 "AuthorizationController->oauthAuthorizeToken() Exception occurred",
1037 ["message" => $exception->getMessage(), 'trace' => $exception->getTraceAsString()]
1039 SessionUtil::oauthSessionCookieDestroy();
1040 $body = $response->getBody();
1041 $body->write($exception->getMessage());
1042 $this->emitResponse($response->withStatus(500)->withBody($body));
1046 public function trustedUser($clientId, $userId)
1048 return $this->trustedUserService->getTrustedUser($clientId, $userId);
1051 public function sessionUserByCode($code)
1053 return $this->trustedUserService->getTrustedUserByCode($code);
1056 public function saveTrustedUser($clientId, $userId, $scope, $persist, $code = '', $session = '', $grant = 'authorization_code')
1058 return $this->trustedUserService->saveTrustedUser($clientId, $userId, $scope, $persist, $code, $session, $grant);
1061 public function decodeToken($token)
1063 return json_decode($this->base64url_decode($token), true);
1066 public function userSessionLogout(): void
1068 $message = '';
1069 $response = $this->createServerResponse();
1070 try {
1071 $id_token = $_REQUEST['id_token_hint'] ?? '';
1072 if (empty($id_token)) {
1073 throw new OAuthServerException('Id token missing from request', 0, 'invalid _request', 400);
1075 $post_logout_url = $_REQUEST['post_logout_redirect_uri'] ?? '';
1076 $state = $_REQUEST['state'] ?? '';
1077 $token_parts = explode('.', $id_token);
1078 $id_payload = $this->decodeToken($token_parts[1]);
1080 $client_id = $id_payload['aud'];
1081 $user = $id_payload['sub'];
1082 $id_nonce = $id_payload['nonce'] ?? '';
1083 $trustedUser = $this->trustedUser($client_id, $user);
1084 if (empty($trustedUser['id'])) {
1085 // not logged in so just continue as if were.
1086 $message = xlt("You are currently not signed in.");
1087 if (!empty($post_logout_url)) {
1088 SessionUtil::oauthSessionCookieDestroy();
1089 header('Location:' . $post_logout_url . "?state=$state");
1090 } else {
1091 SessionUtil::oauthSessionCookieDestroy();
1092 die($message);
1094 exit;
1096 $session_nonce = json_decode($trustedUser['session_cache'], true)['nonce'] ?? '';
1097 // this should be enough to confirm valid id
1098 if ($session_nonce !== $id_nonce) {
1099 throw new OAuthServerException('Id token not issued from this server', 0, 'invalid _request', 400);
1101 // clear the users session
1102 $rtn = $this->trustedUserService->deleteTrustedUserById($trustedUser['id']);
1103 $client = sqlQueryNoLog("SELECT logout_redirect_uris as valid FROM `oauth_clients` WHERE `client_id` = ? AND `logout_redirect_uris` = ?", array($client_id, $post_logout_url));
1104 if (!empty($post_logout_url) && !empty($client['valid'])) {
1105 SessionUtil::oauthSessionCookieDestroy();
1106 header('Location:' . $post_logout_url . "?state=$state");
1107 } else {
1108 $message = xlt("You have been signed out. Thank you.");
1109 SessionUtil::oauthSessionCookieDestroy();
1110 die($message);
1112 } catch (OAuthServerException $exception) {
1113 SessionUtil::oauthSessionCookieDestroy();
1114 $this->emitResponse($exception->generateHttpResponse($response));
1118 public function tokenIntrospection(): void
1120 $response = $this->createServerResponse();
1121 $response->withHeader("Cache-Control", "no-store");
1122 $response->withHeader("Pragma", "no-cache");
1123 $response->withHeader('Content-Type', 'application/json');
1125 $rawToken = $_REQUEST['token'] ?? null;
1126 $token_hint = $_REQUEST['token_type_hint'] ?? null;
1127 $clientId = $_REQUEST['client_id'] ?? null;
1128 // not required for public apps but mandatory for confidential
1129 $clientSecret = $_REQUEST['client_secret'] ?? null;
1131 $this->logger->debug(
1132 self::class . "->tokenIntrospection() start",
1133 ['token_type_hint' => $token_hint, 'client_id' => $clientId]
1136 // the ride starts. had to use a try because PHP doesn't support tryhard yet!
1137 try {
1138 // so regardless of client type(private/public) we need client for client app type and secret.
1139 $client = sqlQueryNoLog("SELECT * FROM `oauth_clients` WHERE `client_id` = ?", array($clientId));
1140 if (empty($client)) {
1141 throw new OAuthServerException('Not a registered client', 0, 'invalid_request', 401);
1143 // a no no. if private we need a secret.
1144 if (empty($clientSecret) && !empty($client['is_confidential'])) {
1145 throw new OAuthServerException('Invalid client app type', 0, 'invalid_request', 400);
1147 // lets verify secret to prevent bad guys.
1148 if (intval($client['is_enabled'] !== 1)) {
1149 // client is disabled and we don't allow introspection of tokens for disabled clients.
1150 throw new OAuthServerException('Client failed security', 0, 'invalid_request', 401);
1152 // lets verify secret to prevent bad guys.
1153 if (!empty($client['client_secret'])) {
1154 $decryptedSecret = $this->cryptoGen->decryptStandard($client['client_secret']);
1155 if ($decryptedSecret !== $clientSecret) {
1156 throw new OAuthServerException('Client failed security', 0, 'invalid_request', 401);
1159 $jsonWebKeyParser = new JsonWebKeyParser($this->oaEncryptionKey, $this->publicKey);
1160 // will try hard to go on if missing token hint. this is to help with universal conformance.
1161 if (empty($token_hint)) {
1162 $token_hint = $jsonWebKeyParser->getTokenHintFromToken($rawToken);
1163 } elseif (($token_hint !== 'access_token' && $token_hint !== 'refresh_token') || empty($rawToken)) {
1164 throw new OAuthServerException('Missing token or unsupported hint.', 0, 'invalid_request', 400);
1167 // are we there yet! client's okay but, is token?
1168 if ($token_hint === 'access_token') {
1169 try {
1170 $result = $jsonWebKeyParser->parseAccessToken($rawToken);
1171 $result['client_id'] = $clientId;
1172 $trusted = $this->trustedUser($result['client_id'], $result['sub']);
1173 if (empty($trusted['id'])) {
1174 $result['active'] = false;
1175 $result['status'] = 'revoked';
1177 $tokenRepository = new AccessTokenRepository();
1178 if ($tokenRepository->isAccessTokenRevokedInDatabase($result['jti'])) {
1179 $result['active'] = false;
1180 $result['status'] = 'revoked';
1182 $audience = $result['aud'];
1183 if (!empty($audience)) {
1184 // audience is an array... we will only validate against the first item
1185 $audience = current($audience);
1187 if ($audience !== $clientId) {
1188 // return no info in this case. possible Phishing
1189 $result = array('active' => false);
1191 } catch (Exception $exception) {
1192 // JWT couldn't be parsed
1193 $body = $response->getBody();
1194 $body->write($exception->getMessage());
1195 SessionUtil::oauthSessionCookieDestroy();
1196 $this->emitResponse($response->withStatus(400)->withBody($body));
1197 exit();
1200 if ($token_hint === 'refresh_token') {
1201 try {
1202 // client_id comes back from the parsed refresh token
1203 $result = $jsonWebKeyParser->parseRefreshToken($rawToken);
1204 } catch (Exception $exception) {
1205 $body = $response->getBody();
1206 $body->write($exception->getMessage());
1207 SessionUtil::oauthSessionCookieDestroy();
1208 $this->emitResponse($response->withStatus(400)->withBody($body));
1209 exit();
1211 $trusted = $this->trustedUser($result['client_id'], $result['sub']);
1212 if (empty($trusted['id'])) {
1213 $result['active'] = false;
1214 $result['status'] = 'revoked';
1216 $tokenRepository = new RefreshTokenRepository();
1217 if ($tokenRepository->isRefreshTokenRevoked($result['jti'])) {
1218 $result['active'] = false;
1219 $result['status'] = 'revoked';
1221 if ($result['client_id'] !== $clientId) {
1222 // return no info in this case. possible Phishing
1223 $result = array('active' => false);
1226 } catch (OAuthServerException $exception) {
1227 // JWT couldn't be parsed
1228 SessionUtil::oauthSessionCookieDestroy();
1229 $this->logger->errorLogCaller($exception->getMessage(), ['trace' => $exception->getTraceAsString()]);
1230 $this->emitResponse($exception->generateHttpResponse($response));
1231 exit();
1233 // we're here so emit results to interface thank you very much.
1234 $body = $response->getBody();
1235 $body->write(json_encode($result));
1236 SessionUtil::oauthSessionCookieDestroy();
1237 $this->emitResponse($response->withStatus(200)->withBody($body));
1238 exit();
1242 * Returns the authentication server token Url endpoint
1243 * @return string
1245 public function getTokenUrl()
1247 return $this->authBaseFullUrl . self::getTokenPath();
1251 * Returns the path prefix that the token authorization endpoint is on.
1252 * @return string
1254 public static function getTokenPath()
1256 return "/token";
1260 * Returns the authentication server manage url
1261 * @return string
1263 public function getManageUrl()
1265 return $this->authBaseFullUrl . self::getManagePath();
1269 * Returns the path prefix that the manage token authorization endpoint is on.
1270 * @return string
1272 public static function getManagePath()
1274 return "/manage";
1278 * Returns the authentication server authorization url to use for oauth authentication
1279 * @return string
1281 public function getAuthorizeUrl()
1283 return $this->authBaseFullUrl . self::getAuthorizePath();
1287 * Returns the path prefix that the authorization endpoint is on.
1288 * @return string
1290 public static function getAuthorizePath()
1292 return "/authorize";
1296 * Returns the authentication server registration url to use for client app / api registration
1297 * @return string
1299 public function getRegistrationUrl()
1301 return $this->authBaseFullUrl . self::getRegistrationPath();
1305 * Returns the path prefix that the registration endpoint is on.
1306 * @return string
1308 public static function getRegistrationPath()
1310 return "/registration";
1314 * Returns the authentication server introspection url to use for checking tokens
1315 * @return string
1317 public function getIntrospectionUrl()
1319 return $this->authBaseFullUrl . self::getIntrospectionPath();
1323 * Returns the path prefix that the introspection endpoint is on.
1324 * @return string
1326 public static function getIntrospectionPath()
1328 return "/introspect";
1332 public static function getAuthBaseFullURL()
1334 $baseUrl = $GLOBALS['webroot'] . '/oauth2/' . $_SESSION['site_id'];
1335 // collect full url and issuing url by using 'site_addr_oath' global
1336 $authBaseFullURL = $GLOBALS['site_addr_oath'] . $baseUrl;
1337 return $authBaseFullURL;
1341 * Given a password grant response, save the trusted user information to the database so password grant users
1342 * can proceed.
1343 * @param ServerResponseInterface $result
1345 private function saveTrustedUserForPasswordGrant(ResponseInterface $result)
1347 $body = $result->getBody();
1348 $body->rewind();
1349 // yep, even password grant gets one. could be useful.
1350 $code = json_decode($body->getContents(), true, 512, JSON_THROW_ON_ERROR)['id_token'];
1351 unset($_SESSION['csrf_private_key']); // gotta remove since binary and will break json_encode (not used for password granttype, so ok to remove)
1352 $session_cache = json_encode($_SESSION, JSON_THROW_ON_ERROR);
1353 $this->saveTrustedUser($_REQUEST['client_id'], $_SESSION['pass_user_id'], $_REQUEST['scope'], 0, $code, $session_cache, self::GRANT_TYPE_PASSWORD);