BxCidaasConModule.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup CidaasConnect Cidaas Connect
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. require_once(BX_DIRECTORY_PATH_MODULES . 'boonex/cidaas_connect/vendor/autoload.php');
  12. use Cidaas\OAuth2\Client\Provider\GrantType;
  13. use GuzzleHttp\Exception\ClientException;
  14. /**
  15. * Login via the official Cidaas PHP SDK hosted login page.
  16. * @see https://github.com/cidaas/cidaas-sdk-php
  17. */
  18. class BxCidaasConModule extends BxBaseModConnectModule
  19. {
  20. function __construct(&$aModule)
  21. {
  22. parent::__construct($aModule);
  23. }
  24. public function actionLogout()
  25. {
  26. if (!isLogged())
  27. return;
  28. echo "TODO: logout";
  29. }
  30. /**
  31. * This service need to be called when we call logout from UNA,
  32. * so it will propagate logout to IDP
  33. * Other words: logout in UNA, IDP and all other SP
  34. */
  35. public function serviceLogout()
  36. {
  37. if (!isLogged() || bx_is_api() || !$this->_oConfig->sBaseUrl)
  38. return;
  39. $sAccessToken = BxDolSession::getInstance()->getValue('cidaascon_access_token');
  40. $sIdToken = BxDolSession::getInstance()->getValue('cidaascon_id_token');
  41. if (!$sAccessToken && !$sIdToken)
  42. return;
  43. $aParams = [];
  44. if ($this->_oConfig->sClientID)
  45. $aParams['client_id'] = $this->_oConfig->sClientID;
  46. $sEndSession = $this->_oConfig->sBaseUrl . '/session/end_session';
  47. if ($aParams)
  48. $sEndSession .= '?' . http_build_query($aParams);
  49. bx_logout(false);
  50. require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
  51. $sIframe = '<iframe src="' . bx_html_attribute($sEndSession) . '" width="0" height="0" style="display:none;visibility:hidden"></iframe>';
  52. $sJs = '<script>setTimeout(function () { window.location.href = ' . json_encode(BX_DOL_URL_ROOT) . '; }, 3000);</script>';
  53. $oTemplate = BxDolTemplate::getInstance();
  54. $oTemplate->setPageNameIndex(BX_PAGE_TRANSITION);
  55. $oTemplate->setPageHeader(_t('_Please Wait'));
  56. $oTemplate->setPageContent('page_main_code', MsgBox(_t('_Please Wait')) . $sIframe . $sJs);
  57. // Keep the 1s meta-refresh on this page from navigating away before the iframe request is sent.
  58. $oTemplate->setPageContent('url_relocate', '');
  59. send_headers_page_changed();
  60. $oTemplate->getPageCode();
  61. exit;
  62. /*
  63. $sAccessToken = BxDolSession::getInstance()->getUnsetValue('cidaascon_access_token');
  64. $sIdToken = BxDolSession::getInstance()->getUnsetValue('cidaascon_id_token');
  65. if (!$sAccessToken)
  66. return;
  67. try {
  68. $this->_getProvider()->logout($sAccessToken)->wait();
  69. } catch (Exception $oException) {
  70. bx_log('bx_cidaas', $this->_getExceptionMessage($oException));
  71. }
  72. */
  73. }
  74. /**
  75. * Redirect to Cidaas hosted login page.
  76. *
  77. * @return n/a - redirect or HTML page in case of error
  78. */
  79. function actionStart()
  80. {
  81. if (isset($_GET['error'])) {
  82. $this->_oTemplate->getPage(_t($this->_oConfig->sDefaultTitleLangKey), DesignBoxContent(_t($this->_oConfig->sDefaultTitleLangKey), MsgBox(bx_get('error'))));
  83. exit;
  84. }
  85. if (isLogged()) {
  86. $this->_redirect($this->_oConfig->sDefaultRedirectUrl);
  87. }
  88. if (!$this->_oConfig->sBaseUrl || !$this->_oConfig->sClientID || (!$this->_oConfig->bPkce && !$this->_oConfig->sSecret)) {
  89. require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
  90. bx_import('BxDolLanguages');
  91. $sCode = MsgBox(_t('_bx_cidaascon_profile_error_api_keys'));
  92. $this->_oTemplate->getPage(_t('_bx_cidaascon'), $sCode);
  93. return;
  94. }
  95. try {
  96. $this->_getProvider()->loginWithBrowser($this->_oConfig->sScope, [
  97. 'state' => $this->_genToken(),
  98. ], $this->_oConfig->bPkce);
  99. } catch (Exception $oException) {
  100. require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
  101. $this->_oTemplate->getPage(_t('_Error'), MsgBox($this->_getExceptionMessage($oException)));
  102. return;
  103. }
  104. // SDK stores the PKCE verifier in PHP $_SESSION; persist it in UNA session for the callback.
  105. if ($this->_oConfig->bPkce && !empty($_SESSION['code-verifier']))
  106. BxDolSession::getInstance()->setValue($this->_oConfig->sSessionCodeVerifier, $_SESSION['code-verifier']);
  107. exit;
  108. }
  109. /**
  110. * $aAuthData = [
  111. * [access_token] => eyJhbGciOiJSUz...
  112. * [id_token] => eyJhbGciOiJS...
  113. * [state] => TkkLRS6wp4ukNyEzPhZi
  114. * [expires_in] => 86400
  115. * [id_token_expires_in] => 86400
  116. * [token_type] => Bearer
  117. * [sub] => 21xxxxxx-9da8-4fd9-ad14-e5f3c615f55a
  118. * [sid] => c5xxxxxx-a54a-403e-b237-99c728059cc6
  119. * [identity_id] => 74e3bf36-d1d7-4e4c-a95b-5822abfc6b97
  120. * ]
  121. * @return void
  122. */
  123. function actionHandle()
  124. {
  125. require_once(BX_DIRECTORY_PATH_INC . 'design.inc.php');
  126. try {
  127. $oProvider = $this->_getProvider();
  128. } catch (Exception $oException) {
  129. $this->_oTemplate->getPage(_t('_Error'), MsgBox($this->_getExceptionMessage($oException)));
  130. return;
  131. }
  132. $aParameters = $oProvider->loginCallback();
  133. if ($this->_getToken() != ($aParameters['state'] ?? '')) {
  134. $this->_oTemplate->getPage(_t('_Error'), MsgBox(_t('_sys_connect_state_invalid')));
  135. return;
  136. }
  137. $sCode = $aParameters['code'] ?? '';
  138. if (!$sCode || !empty($aParameters['error'])) {
  139. $sErrorDescription = !empty($aParameters['error_description']) ? $aParameters['error_description'] : (!empty($aParameters['error']) ? $aParameters['error'] : _t('_error occured'));
  140. $this->_oTemplate->getPage(_t('_Error'), MsgBox($sErrorDescription));
  141. return;
  142. }
  143. if ($this->_oConfig->bPkce) {
  144. $sCodeVerifier = BxDolSession::getInstance()->getUnsetValue($this->_oConfig->sSessionCodeVerifier);
  145. if ($sCodeVerifier)
  146. $_SESSION['code-verifier'] = $sCodeVerifier;
  147. }
  148. try {
  149. $aAuthData = $oProvider->getAccessToken(GrantType::AuthorizationCode, $sCode, '', $this->_oConfig->bPkce)->wait();
  150. // echoDbgLog($aAuthData);
  151. if (empty($aAuthData['access_token'])) {
  152. $sErrorDescription = isset($aAuthData['error_description']) ? $aAuthData['error_description'] : _t('_error occured');
  153. $this->_oTemplate->getPage(_t('_Error'), MsgBox($sErrorDescription));
  154. return;
  155. }
  156. BxDolSession::getInstance()->setValue('cidaascon_access_token', $aAuthData['access_token']);
  157. BxDolSession::getInstance()->setValue('cidaascon_id_token', $aAuthData['id_token']);
  158. $aRemoteProfileInfo = $oProvider->getUserProfile($aAuthData['access_token'])->wait();
  159. } catch (Exception $oException) {
  160. $this->_oTemplate->getPage(_t('_Error'), MsgBox($this->_getExceptionMessage($oException)));
  161. return;
  162. }
  163. if (isset($aRemoteProfileInfo['data']) && is_array($aRemoteProfileInfo['data']))
  164. $aRemoteProfileInfo = $aRemoteProfileInfo['data'];
  165. if (empty($aRemoteProfileInfo['id']) && !empty($aRemoteProfileInfo['sub']))
  166. $aRemoteProfileInfo['id'] = $aRemoteProfileInfo['sub'];
  167. if ($aRemoteProfileInfo && !empty($aRemoteProfileInfo['id'])) {
  168. $iLocalProfileId = $this->_oDb->getProfileId($aRemoteProfileInfo['id']);
  169. if ($iLocalProfileId && $oProfile = BxDolProfile::getInstance($iLocalProfileId)) {
  170. $this->setLogged($oProfile->id(), '', true, true);
  171. }
  172. else {
  173. $this->_createProfile($aRemoteProfileInfo);
  174. }
  175. }
  176. else {
  177. $this->_oTemplate->getPage(_t('_Error'), MsgBox(_t('_sys_connect_profile_error_info')));
  178. }
  179. }
  180. public function serviceGetSafeServices()
  181. {
  182. return array_merge(parent::serviceGetSafeServices(), [
  183. 'Handle' => '',
  184. ]);
  185. }
  186. public function serviceHandle($aRemoteProfileInfo = [])
  187. {
  188. if (!$this->_bIsApi)
  189. return;
  190. if (is_string($aRemoteProfileInfo))
  191. $aRemoteProfileInfo = bx_api_get_browse_params($aRemoteProfileInfo);
  192. if (empty($aRemoteProfileInfo) || !is_array($aRemoteProfileInfo))
  193. return [
  194. bx_api_get_msg(_t('_sys_connect_profile_error_info'))
  195. ];
  196. if (empty($aRemoteProfileInfo['id']) && !empty($aRemoteProfileInfo['sub']))
  197. $aRemoteProfileInfo['id'] = $aRemoteProfileInfo['sub'];
  198. $iProfileId = $this->_oDb->getProfileId($aRemoteProfileInfo['id']);
  199. if ($iProfileId && $oProfile = BxDolProfile::getInstance($iProfileId))
  200. return $this->setLogged($oProfile->id());
  201. else
  202. return $this->_createProfile($aRemoteProfileInfo);
  203. }
  204. /**
  205. *
  206. * (
  207. * [last_used_identity_id] => 74e3bf36-d1d7-4e4c-a95b-5822abfc6b97
  208. * [sub] => 21e029a8-9da8-4fd9-ad14-e5f3c615f55a
  209. * [updated_at] => 1787238995
  210. * [createdTime] => 2026-08-20T15:16:35.513Z
  211. * [userStatus] => VERIFIED
  212. * [lastLoggedInTime] => 2026-08-24T10:05:34.3Z
  213. * [customFields] => Array
  214. * (
  215. * [city] => Sydney
  216. * [department] => Support
  217. * [job_title] => Support
  218. * [name_of_company] => UNA Inc
  219. * [other_interests] => IT
  220. * [responsibilty] => Support
  221. * )
  222. *
  223. * [email] => team@unacms.com
  224. * [email_verified] => 1
  225. * [provider] => self
  226. * [given_name] => UNA
  227. * [family_name] => Team
  228. * [address] => Array
  229. * (
  230. * [country] => Australia
  231. * )
  232. *
  233. * [user_status] => VERIFIED
  234. * [name] => UNA Team
  235. * [last_accessed_at] => 1787565934
  236. * )
  237. *
  238. * @param $aProfileInfo - remote profile info
  239. * @param $sAlternativeName - suffix to add to NickName to make it unique
  240. * @return profile array info, ready for the local database
  241. */
  242. protected function _convertRemoteFields($aProfileInfo, $sAlternativeName = '')
  243. {
  244. $aProfileFields = $aProfileInfo;
  245. $sEmail = !empty($aProfileInfo['email']) ? $aProfileInfo['email'] : '';
  246. if (!$sEmail && !empty($aProfileInfo['identities'][0]['email']))
  247. $sEmail = $aProfileInfo['identities'][0]['email'];
  248. $sName = !empty($aProfileInfo['preferred_username']) ? $aProfileInfo['preferred_username'] : '';
  249. if (!$sName && !empty($aProfileInfo['name']))
  250. $sName = $aProfileInfo['name'];
  251. if (!$sName && !empty($aProfileInfo['given_name']))
  252. $sName = $aProfileInfo['given_name'];
  253. if (!$sName && $sEmail)
  254. $sName = explode('@', $sEmail)[0];
  255. if (!$sName)
  256. $sName = !empty($aProfileInfo['sub']) ? $aProfileInfo['sub'] : '';
  257. $sFullname = !empty($aProfileInfo['name']) ? $aProfileInfo['name'] : trim((!empty($aProfileInfo['given_name']) ? $aProfileInfo['given_name'] : '') . ' ' . (!empty($aProfileInfo['family_name']) ? $aProfileInfo['family_name'] : ''));
  258. if (!$sFullname)
  259. $sFullname = $sName;
  260. $aProfileFields['name'] = $sName;
  261. $aProfileFields['fullname'] = $sFullname;
  262. $aProfileFields['first_name'] = !empty($aProfileInfo['given_name']) ? $aProfileInfo['given_name'] : '';
  263. $aProfileFields['last_name'] = !empty($aProfileInfo['family_name']) ? $aProfileInfo['family_name'] : '';
  264. $aProfileFields['email'] = $sEmail;
  265. $aProfileFields['picture'] = !empty($aProfileInfo['picture']) ? $aProfileInfo['picture'] : '';
  266. $aProfileFields['allow_view_to'] = getParam('bx_cidaascon_privacy');
  267. return $aProfileFields;
  268. }
  269. /**
  270. * @return BxCidaasConProvider
  271. */
  272. protected function _getProvider()
  273. {
  274. bx_import('Provider', $this->_aModule);
  275. return new BxCidaasConProvider(
  276. $this->_oConfig->sBaseUrl,
  277. $this->_oConfig->sClientID,
  278. $this->_oConfig->sSecret ? $this->_oConfig->sSecret : '',
  279. $this->_oConfig->sPageHandle,
  280. null,
  281. $this->_oConfig->bDebug
  282. );
  283. }
  284. protected function _getExceptionMessage($oException)
  285. {
  286. if ($oException instanceof ClientException && $oException->hasResponse()) {
  287. $aBody = json_decode((string)$oException->getResponse()->getBody(), true);
  288. if (!empty($aBody['error']['error_description']))
  289. return $aBody['error']['error_description'];
  290. if (!empty($aBody['error_description']))
  291. return $aBody['error_description'];
  292. if (!empty($aBody['error']) && is_string($aBody['error']))
  293. return $aBody['error'];
  294. }
  295. $sMessage = $oException->getMessage();
  296. return $sMessage ? $sMessage : _t('_error occured');
  297. }
  298. }
  299. /** @} */