PageRenderTime 49ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/OAuthModule.php

https://github.com/whale2/users
PHP | 639 lines | 540 code | 55 blank | 44 comment | 23 complexity | ae054ce456c78cf86df6d4023663c863 MD5 | raw file
  1. <?php
  2. include_once(dirname(__FILE__).'/oauth-php/library/OAuthStore.php');
  3. include_once(dirname(__FILE__).'/oauth-php/library/OAuthRequester.php');
  4. abstract class OAuthAuthenticationModule extends AuthenticationModule
  5. {
  6. protected $serviceName;
  7. protected $userCredentialsClass;
  8. // oauth parameters
  9. protected $oAuthAPIRootURL;
  10. protected $oAuthConsumerKey;
  11. protected $oAuthConsumerSecret;
  12. protected $oAuthAuthorizeURL;
  13. protected $oAuthRequestTokenURL;
  14. protected $oAuthAccessTokenURL;
  15. protected $oAuthSignatureMethods = array();
  16. protected $oAuthScope;
  17. // OAuth store instance - using MySQLi store as the rest of the app uses MySQLi
  18. protected $oAuthStore;
  19. // Look and feel
  20. protected $signUpButtonURL;
  21. protected $logInButtonURL;
  22. protected $connectButtonURL;
  23. // Subclasses must assign unique integer IDs
  24. protected $USERBASE_ACTIVITY_OAUTH_MODULE_LOGIN;
  25. protected $USERBASE_ACTIVITY_OAUTH_MODULE_ADDED;
  26. protected $USERBASE_ACTIVITY_OAUTH_MODULE_REMOVED;
  27. protected $USERBASE_ACTIVITY_OAUTH_MODULE_REGISTER;
  28. public function __construct($serviceName,
  29. $oAuthAPIRootURL,
  30. $oAuthConsumerKey, $oAuthConsumerSecret,
  31. $oAuthRequestTokenURL, $oAuthAccessTokenURL, $oAuthAuthorizeURL,
  32. $oAuthSignatureMethods,
  33. $oAuthScope,
  34. $signUpButtonURL = null,
  35. $logInButtonURL = null,
  36. $connectButtonURL = null,
  37. $activities = null)
  38. {
  39. parent::__construct();
  40. $this->serviceName = $serviceName;
  41. $this->oAuthAPIRootURL = $oAuthAPIRootURL;
  42. $this->oAuthConsumerKey = $oAuthConsumerKey;
  43. $this->oAuthConsumerSecret= $oAuthConsumerSecret;
  44. $this->oAuthRequestTokenURL = $oAuthRequestTokenURL;
  45. $this->oAuthAccessTokenURL = $oAuthAccessTokenURL;
  46. $this->oAuthAuthorizeURL = $oAuthAuthorizeURL;
  47. $this->oAuthSignatureMethods = $oAuthSignatureMethods;
  48. $this->oAuthScope = $oAuthScope;
  49. $this->signUpButtonURL = $signUpButtonURL;
  50. $this->logInButtonURL = $logInButtonURL;
  51. $this->connectButtonURL = $connectButtonURL;
  52. if (!is_null($activities)) {
  53. $this->USERBASE_ACTIVITY_OAUTH_MODULE_LOGIN = $activities[0][0];
  54. $this->USERBASE_ACTIVITY_OAUTH_MODULE_ADDED = $activities[1][0];
  55. $this->USERBASE_ACTIVITY_OAUTH_MODULE_REMOVED = $activities[2][0];
  56. $this->USERBASE_ACTIVITY_OAUTH_MODULE_REGISTER = $activities[3][0];
  57. UserConfig::$activities[$activities[0][0]] = array($activities[0][1], $activities[0][2]);
  58. UserConfig::$activities[$activities[1][0]] = array($activities[1][1], $activities[1][2]);
  59. UserConfig::$activities[$activities[2][0]] = array($activities[2][1], $activities[2][2]);
  60. UserConfig::$activities[$activities[3][0]] = array($activities[3][1], $activities[3][2]);
  61. }
  62. $this->oAuthStore = OAuthStore::instance('MySQLi', array(
  63. 'conn' => UserConfig::getDB(),
  64. 'table_prefix' => UserConfig::$mysql_prefix
  65. ));
  66. }
  67. ###########################################################################################
  68. # Methods related to OAuth handling
  69. ###########################################################################################
  70. /**
  71. * Each module is supposed to implement this service to retrieve identity info from the server.
  72. *
  73. * Relying on the key only is not enough as some servers might not return same access_token
  74. * for the same user. Also, access_tokens can expire or be revoked eventually, but this doesn't
  75. * mean that the system user identity has changed.
  76. *
  77. * @param array $oauth_user_id OAuth user id to get identity for
  78. * @return array $identity Identity array that includes user info including 'id' column which
  79. * uniquely identifies the user on server and 'name' value that can be
  80. * used as user's name upon registration
  81. */
  82. abstract public function getIdentity($oauth_user_id);
  83. /**
  84. * Initializes the server entry in the database if it wasn't initialized beforehand
  85. */
  86. protected function initOAuthServer() {
  87. // check if server is already registered in our database, otherwise, create the entry
  88. try {
  89. $this->oAuthStore->getServerForUri($this->oAuthAPIRootURL, null);
  90. } catch (OAuthException2 $e) {
  91. $this->oAuthStore->updateServer(array(
  92. 'server_uri' => $this->oAuthAPIRootURL,
  93. 'consumer_key' => $this->oAuthConsumerKey,
  94. 'consumer_secret' => $this->oAuthConsumerSecret,
  95. 'authorize_uri' => $this->oAuthAuthorizeURL,
  96. 'request_token_uri' => $this->oAuthRequestTokenURL,
  97. 'access_token_uri' => $this->oAuthAccessTokenURL,
  98. 'signature_methods' => $this->oAuthSignatureMethods,
  99. 'user_id' => null
  100. ), null, true);
  101. }
  102. }
  103. protected function startOAuthFlow() {
  104. // generate new user id since we're logging in and have no idea who the user is
  105. $oauth_user_id = $this->getNewOAuthUserID();
  106. $storage = new MrClay_CookieStorage(array(
  107. 'secret' => UserConfig::$SESSION_SECRET,
  108. 'mode' => MrClay_CookieStorage::MODE_ENCRYPT,
  109. 'path' => UserConfig::$SITEROOTURL,
  110. 'httponly' => true
  111. ));
  112. if (!$storage->store(UserConfig::$oauth_user_id_key, $oauth_user_id)) {
  113. throw new Exception(implode('; ', $storage->errors));
  114. }
  115. try
  116. {
  117. $callback = UserConfig::$USERSROOTFULLURL.'/oauth_callback.php?module='.$this->getID();
  118. // TODO add a way to skip this step if server was initialized
  119. $this->initOAuthServer();
  120. $params = array(
  121. 'oauth_callback' => $callback
  122. );
  123. if (!is_null($this->oAuthScope)) {
  124. $params['scope'] = $this->oAuthScope;
  125. }
  126. if (!is_null(UserConfig::$OAuthAppName)) {
  127. $params['xoauth_displayname'] = UserConfig::$OAuthAppName;
  128. }
  129. // STEP 1: get a request token
  130. $tokenResultParams = OAuthRequester::requestRequestToken(
  131. $this->oAuthConsumerKey,
  132. $oauth_user_id,
  133. $params
  134. );
  135. // redirect to the authorization page, they will redirect back
  136. header("Location: " . $this->oAuthAuthorizeURL . "?oauth_token=" . $tokenResultParams['token']);
  137. exit;
  138. } catch(OAuthException2 $e) {
  139. error_log(var_export($e, true));
  140. return null;
  141. }
  142. }
  143. /**
  144. * When we don't know current user, we need to create a new OAuth User ID to use for new connection.
  145. * If we know the user when OAuth comes through, we'll replace current OAuth User ID with the new one.
  146. */
  147. protected function getNewOAuthUserID() {
  148. // TODO add a way to skip this step if server was initialized
  149. $this->initOAuthServer();
  150. $db = UserConfig::getDB();
  151. $module = $this->getID();
  152. if ($stmt = $db->prepare('INSERT INTO '.UserConfig::$mysql_prefix.'user_oauth_identity (module) VALUES (?)'))
  153. {
  154. if (!$stmt->bind_param('s', $module))
  155. {
  156. throw new Exception("Can't bind parameter".$stmt->error);
  157. }
  158. if (!$stmt->execute())
  159. {
  160. throw new Exception("Can't execute statement: ".$stmt->error);
  161. }
  162. $oauth_user_id = $stmt->insert_id;
  163. $stmt->close();
  164. }
  165. else
  166. {
  167. throw new Exception("Can't prepare statement: ".$db->error);
  168. }
  169. return $oauth_user_id;
  170. }
  171. /**
  172. *
  173. */
  174. public function addUserOAuthIdentity($user, $identity, $oauth_user_id) {
  175. $db = UserConfig::getDB();
  176. $user_id = $user->getID();
  177. $old_oauth_user_id = null;
  178. $server_unique_id = $identity['id'];
  179. $serialized_userinfo = serialize($identity);
  180. $module = $this->getID();
  181. // updating new recently created entry
  182. if ($stmt = $db->prepare('UPDATE '.UserConfig::$mysql_prefix.'user_oauth_identity SET user_id = ?, identity = ?, userinfo = ? WHERE oauth_user_id = ? AND module = ?'))
  183. {
  184. if (!$stmt->bind_param('issis', $user_id, $server_unique_id, $serialized_userinfo, $oauth_user_id, $module))
  185. {
  186. throw new Exception("Can't bind parameter".$stmt->error);
  187. }
  188. if (!$stmt->execute())
  189. {
  190. throw new Exception("Can't execute statement: ".$stmt->error);
  191. }
  192. $stmt->close();
  193. }
  194. else
  195. {
  196. throw new Exception("Can't prepare statement: ".$db->error);
  197. }
  198. }
  199. public function deleteOAuthUser($oauth_user_id) {
  200. $db = UserConfig::getDB();
  201. if ($stmt = $db->prepare('DELETE FROM '.UserConfig::$mysql_prefix.'user_oauth_identity WHERE oauth_user_id = ?'))
  202. {
  203. if (!$stmt->bind_param('i', $oauth_user_id))
  204. {
  205. throw new Exception("Can't bind parameter".$stmt->error);
  206. }
  207. if (!$stmt->execute())
  208. {
  209. throw new Exception("Can't execute statement: ".$stmt->error);
  210. }
  211. $stmt->close();
  212. }
  213. else
  214. {
  215. throw new Exception("Can't prepare statement: ".$db->error);
  216. }
  217. }
  218. /**
  219. * Get UserBase user by server identity and reset user_id -> oauth_user_id if necessary
  220. */
  221. public function getUserByOAuthIdentity($identity, $oauth_user_id) {
  222. $db = UserConfig::getDB();
  223. $user_id = null;
  224. $old_oauth_user_id = null;
  225. $server_unique_id = $identity['id'];
  226. $module = $this->getID();
  227. if ($stmt = $db->prepare('SELECT oauth_user_id, user_id FROM '.UserConfig::$mysql_prefix.'user_oauth_identity WHERE module = ? AND identity = ?'))
  228. {
  229. if (!$stmt->bind_param('ss', $module, $server_unique_id))
  230. {
  231. throw new Exception("Can't bind parameter".$stmt->error);
  232. }
  233. if (!$stmt->execute())
  234. {
  235. throw new Exception("Can't execute statement: ".$stmt->error);
  236. }
  237. if (!$stmt->bind_result($old_oauth_user_id, $user_id))
  238. {
  239. throw new Exception("Can't bind result: ".$stmt->error);
  240. }
  241. $stmt->fetch();
  242. $stmt->close();
  243. }
  244. else
  245. {
  246. throw new Exception("Can't prepare statement: ".$db->error);
  247. }
  248. // nobody registered with this identity yet
  249. if (is_null($user_id)) {
  250. return null;
  251. }
  252. if ($old_oauth_user_id != $oauth_user_id) {
  253. // let's re-map from old oauth_user_id to new one
  254. // deleting old one first
  255. $this->deleteOAuthUser($old_oauth_user_id);
  256. $serialized_userinfo = serialize($identity);
  257. // updating new recently created entry
  258. if ($stmt = $db->prepare('UPDATE '.UserConfig::$mysql_prefix.'user_oauth_identity SET user_id = ?, identity = ?, userinfo = ? WHERE oauth_user_id = ?'))
  259. {
  260. if (!$stmt->bind_param('issi', $user_id, $server_unique_id, $serialized_userinfo, $oauth_user_id))
  261. {
  262. throw new Exception("Can't bind parameter".$stmt->error);
  263. }
  264. if (!$stmt->execute())
  265. {
  266. throw new Exception("Can't execute statement: ".$stmt->error);
  267. }
  268. $stmt->close();
  269. }
  270. else
  271. {
  272. throw new Exception("Can't prepare statement: ".$db->error);
  273. }
  274. }
  275. return User::getUser($user_id);
  276. }
  277. public function getAccessToken($oauth_user_id) {
  278. // STEP 2: Get an access token
  279. $oauthToken = $_GET["oauth_token"];
  280. // echo "oauth_verifier = '" . $oauthVerifier . "'<br/>";
  281. $tokenResultParams = $_GET;
  282. OAuthRequester::requestAccessToken(
  283. $this->oAuthConsumerKey,
  284. $oauthToken,
  285. $oauth_user_id,
  286. 'POST',
  287. $_GET
  288. );
  289. }
  290. ###########################################################################################
  291. # Methods related to UserBase mechanics
  292. ###########################################################################################
  293. public function renderLoginForm($action)
  294. {
  295. ?>
  296. <p>Sign in using your existing account with <b><?php echo UserTools::escape($this->serviceName)?></b>.</p>
  297. <form action="<?php echo $action?>" method="POST">
  298. <?php if (is_null($this->logInButtonURL)) { ?>
  299. <input type="submit" name="login" value="Log in using <?php echo UserTools::escape($this->serviceName)?> &gt;&gt;&gt;"/>
  300. <?php } else { ?>
  301. <input type="image" name="login" src="<?php echo UserTools::escape($this->logInButtonURL) ?>" value="login"/>
  302. <?php } ?>
  303. </form>
  304. <?php
  305. }
  306. public function renderRegistrationForm($full = false, $action = null, $errors = null , $data = null)
  307. {
  308. if (is_null($action))
  309. {
  310. $action = UserConfig::$USERSROOTURL.'/register.php?module='.$this->getID();
  311. }
  312. if ($full)
  313. {
  314. ?>
  315. <p>Sign in using your existing account with <b><?php echo UserTools::escape($this->serviceName)?></b>.</p>
  316. <?php
  317. }
  318. ?>
  319. <form action="<?php echo $action?>" method="POST">
  320. <?php if (is_null($this->signUpButtonURL)) { ?>
  321. <input type="submit" name="register" value="Register using <?php echo UserTools::escape($this->serviceName)?>&gt;&gt;&gt;"/>
  322. <?php } else { ?>
  323. <input type="image" name="register" src="<?php echo UserTools::escape($this->signUpButtonURL) ?>" value="register"/>
  324. <?php } ?>
  325. </form>
  326. <?php
  327. }
  328. public function getUserCredentials($user)
  329. {
  330. $db = UserConfig::getDB();
  331. $user_id = $user->getID();
  332. $module = $this->getID();
  333. $oauth_user_id = null;
  334. $serialized_userinfo = null;
  335. if ($stmt = $db->prepare('SELECT oauth_user_id, userinfo FROM '.UserConfig::$mysql_prefix.'user_oauth_identity WHERE user_id = ? AND module = ?'))
  336. {
  337. if (!$stmt->bind_param('is', $user_id, $module))
  338. {
  339. throw new Exception("Can't bind parameter".$stmt->error);
  340. }
  341. if (!$stmt->execute())
  342. {
  343. throw new Exception("Can't execute statement: ".$stmt->error);
  344. }
  345. if (!$stmt->bind_result($oauth_user_id, $serialized_userinfo))
  346. {
  347. throw new Exception("Can't bind result: ".$stmt->error);
  348. }
  349. $stmt->fetch();
  350. $stmt->close();
  351. }
  352. else
  353. {
  354. throw new Exception("Can't prepare statement: ".$db->error);
  355. }
  356. if (is_null($serialized_userinfo)) {
  357. return null;
  358. }
  359. return new $this->userCredentialsClass($oauth_user_id, unserialize($serialized_userinfo));
  360. }
  361. /*
  362. * Renders user editing form
  363. *
  364. * Parameters:
  365. * $action - form action to post back to
  366. * $errors - error messages to display
  367. * $user - user object for current user that is being edited
  368. * $data - data submitted to the form
  369. */
  370. public function renderEditUserForm($action, $errors, $user, $data)
  371. {
  372. $db = UserConfig::getDB();
  373. $user_id = $user->getID();
  374. $module = $this->getID();
  375. $oauth_user_id = null;
  376. $serialized_userinfo = null;
  377. if ($stmt = $db->prepare('SELECT oauth_user_id, userinfo FROM '.UserConfig::$mysql_prefix.'user_oauth_identity WHERE user_id = ? AND module = ?'))
  378. {
  379. if (!$stmt->bind_param('is', $user_id, $module))
  380. {
  381. throw new Exception("Can't bind parameter".$stmt->error);
  382. }
  383. if (!$stmt->execute())
  384. {
  385. throw new Exception("Can't execute statement: ".$stmt->error);
  386. }
  387. if (!$stmt->bind_result($oauth_user_id, $serialized_userinfo))
  388. {
  389. throw new Exception("Can't bind result: ".$stmt->error);
  390. }
  391. $stmt->fetch();
  392. $stmt->close();
  393. }
  394. else
  395. {
  396. throw new Exception("Can't prepare statement: ".$db->error);
  397. }
  398. ?>
  399. <form action="<?php echo $action?>" method="POST">
  400. <?php
  401. if (is_null($oauth_user_id)) {
  402. if (is_null($this->connectButtonURL)) {
  403. ?><input type="submit" name="add" value="Connect existing <?php echo $this->getTitle() ?> account &gt;&gt;&gt;"/><?php
  404. } else {
  405. ?><input type="image" name="add" src="<?php echo UserTools::escape($this->connectButtonURL) ?>" value="add"/><?php
  406. }
  407. } else {
  408. ?>
  409. <div><?php $this->renderUserInfo($serialized_userinfo) ?></div>
  410. <input type="hidden" name="oauth_user_id" value="<?php echo htmlentities($oauth_user_id) ?>"/>
  411. <input type="submit" name="remove" value="remove" style="font-size: xx-small"/>
  412. <?php
  413. }
  414. ?>
  415. <input type="hidden" name="save" value="Save &gt;&gt;&gt;"/>
  416. <?php UserTools::renderCSRFNonce(); ?>
  417. </form>
  418. <?php
  419. }
  420. protected function renderUserInfo($serialized_userinfo) {
  421. $user_info = unserialize($serialized_userinfo);
  422. echo $user_info['name'];
  423. }
  424. public function processLogin($data, &$remember)
  425. {
  426. $this->startOAuthFlow();
  427. }
  428. public function processRegistration($data, &$remember)
  429. {
  430. $this->startOAuthFlow();
  431. }
  432. /*
  433. * Updates user information
  434. *
  435. * returns true if successful and false if unsuccessful
  436. *
  437. * throws InputValidationException if there are problems with input data
  438. */
  439. public function processEditUser($user, $data)
  440. {
  441. if (array_key_exists('remove', $data) && array_key_exists('oauth_user_id', $data)) {
  442. $db = UserConfig::getDB();
  443. $oauth_user_id = $data['oauth_user_id'];
  444. $user_id = $user->getID();
  445. if ($stmt = $db->prepare('DELETE FROM '.UserConfig::$mysql_prefix.'user_oauth_identity WHERE oauth_user_id = ? AND user_id = ?'))
  446. {
  447. if (!$stmt->bind_param('ii', $oauth_user_id, $user_id))
  448. {
  449. throw new Exception("Can't bind parameter".$stmt->error);
  450. }
  451. if (!$stmt->execute())
  452. {
  453. throw new Exception("Can't execute statement: ".$stmt->error);
  454. }
  455. $stmt->close();
  456. }
  457. else
  458. {
  459. throw new Exception("Can't prepare statement: ".$db->error);
  460. }
  461. if (!is_null($this->USERBASE_ACTIVITY_OAUTH_MODULE_REMOVED)) {
  462. $user->recordActivity($this->USERBASE_ACTIVITY_OAUTH_MODULE_REMOVED);
  463. }
  464. return true;
  465. }
  466. if (array_key_exists('add', $data)) {
  467. $this->startOAuthFlow();
  468. }
  469. }
  470. public function recordLoginActivity($user) {
  471. if (!is_null($this->USERBASE_ACTIVITY_OAUTH_MODULE_LOGIN)) {
  472. $user->recordActivity($this->USERBASE_ACTIVITY_OAUTH_MODULE_LOGIN);
  473. }
  474. }
  475. public function recordRegistrationActivity($user) {
  476. if (!is_null($this->USERBASE_ACTIVITY_OAUTH_MODULE_REGISTER)) {
  477. $user->recordActivity($this->USERBASE_ACTIVITY_OAUTH_MODULE_REGISTER);
  478. }
  479. }
  480. public function recordAddActivity($user) {
  481. if (!is_null($this->USERBASE_ACTIVITY_OAUTH_MODULE_ADDED)) {
  482. $user->recordActivity($this->USERBASE_ACTIVITY_OAUTH_MODULE_ADDED);
  483. }
  484. }
  485. public function getTotalConnectedUsers()
  486. {
  487. $db = UserConfig::getDB();
  488. $module_id = $this->getID();
  489. $conns = 0;
  490. if ($stmt = $db->prepare('SELECT count(*) AS conns FROM '.UserConfig::$mysql_prefix.'users u LEFT JOIN '.UserConfig::$mysql_prefix.'user_oauth_identity oa ON u.id = oa.user_id WHERE oa.oauth_user_id IS NOT NULL AND oa.module = ?'))
  491. {
  492. if (!$stmt->bind_param('s', $module_id))
  493. {
  494. throw new Exception("Can't bind parameter".$stmt->error);
  495. }
  496. if (!$stmt->execute())
  497. {
  498. throw new Exception("Can't execute statement: ".$stmt->error);
  499. }
  500. if (!$stmt->bind_result($conns))
  501. {
  502. throw new Exception("Can't bind result: ".$stmt->error);
  503. }
  504. $stmt->fetch();
  505. $stmt->close();
  506. }
  507. else
  508. {
  509. throw new Exception("Can't prepare statement: ".$db->error);
  510. }
  511. return $conns;
  512. }
  513. }
  514. abstract class OAuthUserCredentials extends UserCredentials {
  515. // OAuth user id
  516. protected $oauth_user_id;
  517. // User info object specific to a subclass
  518. protected $userinfo;
  519. public function __construct($oauth_user_id, $userinfo) {
  520. $this->oauth_user_id = $oauth_user_id;
  521. $this->userinfo = $userinfo;
  522. }
  523. public function getOAuthUserID() {
  524. return $this->oauth_user_id;
  525. }
  526. /**
  527. * @return array Array of user-specific information
  528. */
  529. public function getUserInfo() {
  530. return $this->userinfo;
  531. }
  532. /**
  533. * This method will most likely be implemented by a subclass using $this->userinfo object
  534. *
  535. * @return string
  536. */
  537. public function getHTML() {
  538. return $this->userinfo['name'];
  539. }
  540. public function makeOAuthRequest($request, $method = null, $params = null, $body = null, $files = null) {
  541. $request = new OAuthRequester($request, $method, $params, $body, $files);
  542. return $request->doRequest($this->oauth_user_id);
  543. }
  544. }