openid.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. /*
  3. * StatusNet - the distributed open-source microblogging tool
  4. * Copyright (C) 2008, 2009, StatusNet, Inc.
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. if (!defined('STATUSNET')) {
  20. exit(1);
  21. }
  22. require_once('Auth/OpenID.php');
  23. require_once('Auth/OpenID/Consumer.php');
  24. require_once('Auth/OpenID/Server.php');
  25. require_once('Auth/OpenID/SReg.php');
  26. require_once('Auth/OpenID/MySQLStore.php');
  27. // About one year cookie expiry
  28. define('OPENID_COOKIE_EXPIRY', round(365.25 * 24 * 60 * 60));
  29. define('OPENID_COOKIE_KEY', 'lastusedopenid');
  30. function oid_store()
  31. {
  32. static $store = null;
  33. if (is_null($store)) {
  34. // To create a new Database connection is an absolute must
  35. // because database is in transaction (auto-commit = false)
  36. // mode during OpenID operation
  37. // Is a must because our Internal Session Handler uses database
  38. // and depends on auto-commit = true
  39. $dsn = common_config('db', 'database');
  40. $options = PEAR::getStaticProperty('DB', 'options');
  41. if (!is_array($options)) {
  42. $options = [];
  43. }
  44. $db = DB::connect($dsn, $options);
  45. if (PEAR::isError($db)) {
  46. throw new ServerException($db->getMessage());
  47. }
  48. switch (common_config('db', 'type')) {
  49. case 'mysql':
  50. $store = new Auth_OpenID_MySQLStore($db);
  51. break;
  52. case 'postgresql':
  53. $store = new Auth_OpenID_PostgreSQLStore($db);
  54. break;
  55. default:
  56. throw new ServerException(_m('Unknown DB type for OpenID.'));
  57. }
  58. }
  59. return $store;
  60. }
  61. function oid_consumer()
  62. {
  63. $store = oid_store();
  64. // No need to declare a Yadis Session Handler
  65. common_ensure_session(); // This is transparent to OpenID's eyes
  66. $consumer = new Auth_OpenID_Consumer($store);
  67. return $consumer;
  68. }
  69. function oid_server()
  70. {
  71. $store = oid_store();
  72. $server = new Auth_OpenID_Server($store, common_local_url('openidserver'));
  73. return $server;
  74. }
  75. function oid_clear_last()
  76. {
  77. oid_set_last('');
  78. }
  79. function oid_set_last($openid_url)
  80. {
  81. common_set_cookie(OPENID_COOKIE_KEY,
  82. $openid_url,
  83. time() + OPENID_COOKIE_EXPIRY);
  84. }
  85. function oid_get_last()
  86. {
  87. if (empty($_COOKIE[OPENID_COOKIE_KEY])) {
  88. return null;
  89. }
  90. $openid_url = $_COOKIE[OPENID_COOKIE_KEY];
  91. if ($openid_url && strlen($openid_url) > 0) {
  92. return $openid_url;
  93. } else {
  94. return null;
  95. }
  96. }
  97. function oid_link_user($id, $canonical, $display)
  98. {
  99. global $_PEAR;
  100. $oid = new User_openid();
  101. $oid->user_id = $id;
  102. $oid->canonical = $canonical;
  103. $oid->display = $display;
  104. $oid->created = common_sql_now();
  105. if (!$oid->insert()) {
  106. $err = &$_PEAR->getStaticProperty('DB_DataObject','lastError');
  107. return false;
  108. }
  109. return true;
  110. }
  111. function oid_get_user($openid_url)
  112. {
  113. $user = null;
  114. $oid = User_openid::getKV('canonical', $openid_url);
  115. if ($oid) {
  116. $user = User::getKV('id', $oid->user_id);
  117. }
  118. return $user;
  119. }
  120. function oid_check_immediate($openid_url, $backto=null)
  121. {
  122. if (!$backto) {
  123. $action = $_REQUEST['action'];
  124. $args = common_copy_args($_GET);
  125. unset($args['action']);
  126. $backto = common_local_url($action, $args);
  127. }
  128. common_ensure_session();
  129. $_SESSION['openid_immediate_backto'] = $backto;
  130. oid_authenticate($openid_url,
  131. 'finishimmediate',
  132. true);
  133. }
  134. function oid_authenticate($openid_url, $returnto, $immediate=false)
  135. {
  136. $openid_url = Auth_OpenID::normalizeUrl($openid_url);
  137. if (!common_valid_http_url($openid_url)) {
  138. throw new ClientException(_m('No valid URL provided for OpenID.'));
  139. }
  140. $consumer = oid_consumer();
  141. if (!$consumer) {
  142. // TRANS: OpenID plugin server error.
  143. throw new ServerException(_m('Cannot instantiate OpenID consumer object.'));
  144. }
  145. common_ensure_session();
  146. $auth_request = $consumer->begin($openid_url);
  147. // Handle failure status return values.
  148. if (!$auth_request) {
  149. common_log(LOG_ERR, __METHOD__ . ": mystery fail contacting $openid_url");
  150. // TRANS: OpenID plugin message. Given when an OpenID is not valid.
  151. throw new ServerException(_m('Not a valid OpenID.'));
  152. } else if (Auth_OpenID::isFailure($auth_request)) {
  153. common_log(LOG_ERR, __METHOD__ . ": OpenID fail to $openid_url: $auth_request->message");
  154. // TRANS: OpenID plugin server error. Given when the OpenID authentication request fails.
  155. // TRANS: %s is the failure message.
  156. throw new ServerException(sprintf(_m('OpenID failure: %s.'), $auth_request->message));
  157. }
  158. $sreg_request = Auth_OpenID_SRegRequest::build(// Required
  159. array(),
  160. // Optional
  161. array('nickname',
  162. 'email',
  163. 'fullname',
  164. 'language',
  165. 'timezone',
  166. 'postcode',
  167. 'country'));
  168. if ($sreg_request) {
  169. $auth_request->addExtension($sreg_request);
  170. }
  171. $requiredTeam = common_config('openid', 'required_team');
  172. if ($requiredTeam) {
  173. // LaunchPad OpenID extension
  174. $team_request = new Auth_OpenID_TeamsRequest(array($requiredTeam));
  175. if ($team_request) {
  176. $auth_request->addExtension($team_request);
  177. }
  178. }
  179. $trust_root = common_root_url(true);
  180. $process_url = common_local_url($returnto);
  181. // Net::OpenID::Server as used on LiveJournal appears to incorrectly
  182. // reject POST requests for data submissions that OpenID 1.1 specs
  183. // as GET, although 2.0 allows them:
  184. // https://rt.cpan.org/Public/Bug/Display.html?id=42202
  185. //
  186. // Our OpenID libraries would have switched in the redirect automatically
  187. // if it were detecting 1.1 compatibility mode, however the server is
  188. // advertising itself as 2.0-compatible, so we got switched to the POST.
  189. //
  190. // Since the GET should always work anyway, we'll just take out the
  191. // autosubmitter for now.
  192. //
  193. //if ($auth_request->shouldSendRedirect()) {
  194. $redirect_url = $auth_request->redirectURL($trust_root,
  195. $process_url,
  196. $immediate);
  197. if (Auth_OpenID::isFailure($redirect_url)) {
  198. // TRANS: OpenID plugin server error. Given when the OpenID authentication request cannot be redirected.
  199. // TRANS: %s is the failure message.
  200. throw new ServerException(sprintf(_m('Could not redirect to server: %s.'), $redirect_url->message));
  201. }
  202. common_redirect($redirect_url, 303);
  203. /*
  204. } else {
  205. // Generate form markup and render it.
  206. $form_id = 'openid_message';
  207. $form_html = $auth_request->formMarkup($trust_root, $process_url,
  208. $immediate, array('id' => $form_id));
  209. // XXX: This is cheap, but things choke if we don't escape ampersands
  210. // in the HTML attributes
  211. $form_html = preg_replace('/&/', '&amp;', $form_html);
  212. // Display an error if the form markup couldn't be generated;
  213. // otherwise, render the HTML.
  214. if (Auth_OpenID::isFailure($form_html)) {
  215. // TRANS: OpenID plugin server error if the form markup could not be generated.
  216. // TRANS: %s is the failure message.
  217. common_server_error(sprintf(_m('Could not create OpenID form: %s'), $form_html->message));
  218. } else {
  219. $action = new AutosubmitAction(); // see below
  220. $action->form_html = $form_html;
  221. $action->form_id = $form_id;
  222. $action->prepare(array('action' => 'autosubmit'));
  223. $action->handle(array('action' => 'autosubmit'));
  224. }
  225. }
  226. */
  227. }
  228. // Half-assed attempt at a module-private function
  229. function _oid_print_instructions()
  230. {
  231. common_element('div', 'instructions',
  232. // TRANS: OpenID plugin user instructions.
  233. _m('This form should automatically submit itself. '.
  234. 'If not, click the submit button to go to your '.
  235. 'OpenID provider.'));
  236. }
  237. /**
  238. * Update a user from sreg parameters
  239. * @param User $user
  240. * @param array $sreg fields from OpenID sreg response
  241. * @access private
  242. */
  243. function oid_update_user($user, $sreg)
  244. {
  245. $profile = $user->getProfile();
  246. $orig_profile = clone($profile);
  247. if (!empty($sreg['fullname']) && strlen($sreg['fullname']) <= 255) {
  248. $profile->fullname = $sreg['fullname'];
  249. }
  250. if (!empty($sreg['country'])) {
  251. if ($sreg['postcode']) {
  252. // XXX: use postcode to get city and region
  253. // XXX: also, store postcode somewhere -- it's valuable!
  254. $profile->location = $sreg['postcode'] . ', ' . $sreg['country'];
  255. } else {
  256. $profile->location = $sreg['country'];
  257. }
  258. }
  259. // XXX save language if it's passed
  260. // XXX save timezone if it's passed
  261. if (!$profile->update($orig_profile)) {
  262. // TRANS: OpenID plugin server error.
  263. common_server_error(_m('Error saving the profile.'));
  264. return false;
  265. }
  266. $orig_user = clone($user);
  267. if (!empty($sreg['email']) && Validate::email($sreg['email'], common_config('email', 'check_domain'))) {
  268. $user->email = $sreg['email'];
  269. }
  270. if (!$user->update($orig_user)) {
  271. // TRANS: OpenID plugin server error.
  272. common_server_error(_m('Error saving the user.'));
  273. return false;
  274. }
  275. return true;
  276. }
  277. function oid_assert_allowed($url)
  278. {
  279. $blacklist = common_config('openid', 'blacklist');
  280. $whitelist = common_config('openid', 'whitelist');
  281. if (empty($blacklist)) {
  282. $blacklist = array();
  283. }
  284. if (empty($whitelist)) {
  285. $whitelist = array();
  286. }
  287. foreach ($blacklist as $pattern) {
  288. if (preg_match("/$pattern/", $url)) {
  289. common_log(LOG_INFO, "Matched OpenID blacklist pattern {$pattern} with {$url}");
  290. foreach ($whitelist as $exception) {
  291. if (preg_match("/$exception/", $url)) {
  292. common_log(LOG_INFO, "Matched OpenID whitelist pattern {$exception} with {$url}");
  293. return;
  294. }
  295. }
  296. // TRANS: OpenID plugin client exception (403).
  297. throw new ClientException(_m('Unauthorized URL used for OpenID login.'), 403);
  298. }
  299. }
  300. return;
  301. }
  302. /**
  303. * Check the teams available in the given OpenID response
  304. * Using Launchpad's OpenID teams extension
  305. *
  306. * @return boolean whether this user is acceptable
  307. */
  308. function oid_check_teams($response)
  309. {
  310. $requiredTeam = common_config('openid', 'required_team');
  311. if ($requiredTeam) {
  312. $team_resp = new Auth_OpenID_TeamsResponse($response);
  313. if ($team_resp) {
  314. $teams = $team_resp->getTeams();
  315. } else {
  316. $teams = array();
  317. }
  318. $match = in_array($requiredTeam, $teams);
  319. $is = $match ? 'is' : 'is not';
  320. common_log(LOG_DEBUG, "Remote user $is in required team $requiredTeam: [" . implode(', ', $teams) . "]");
  321. return $match;
  322. }
  323. return true;
  324. }
  325. class AutosubmitAction extends Action
  326. {
  327. var $form_html = null;
  328. var $form_id = null;
  329. function handle()
  330. {
  331. parent::handle();
  332. $this->showPage();
  333. }
  334. function title()
  335. {
  336. // TRANS: Title
  337. return _m('OpenID Login Submission');
  338. }
  339. function showContent()
  340. {
  341. $this->raw('<p style="margin: 20px 80px">');
  342. // @todo FIXME: This would be better using standard CSS class, but the present theme's a bit scary.
  343. $this->element('img', array('src' => Theme::path('images/icons/icon_processing.gif', 'base'),
  344. // for some reason the base CSS sets <img>s as block display?!
  345. 'style' => 'display: inline'));
  346. // TRANS: OpenID plugin message used while requesting authorization user's OpenID login provider.
  347. $this->text(_m('Requesting authorization from your login provider...'));
  348. $this->raw('</p>');
  349. $this->raw('<p style="margin-top: 60px; font-style: italic">');
  350. // TRANS: OpenID plugin message. User instruction while requesting authorization user's OpenID login provider.
  351. $this->text(_m('If you are not redirected to your login provider in a few seconds, try pushing the button below.'));
  352. $this->raw('</p>');
  353. $this->raw($this->form_html);
  354. }
  355. function showScripts()
  356. {
  357. parent::showScripts();
  358. $this->element('script', null,
  359. 'document.getElementById(\'' . $this->form_id . '\').submit();');
  360. }
  361. }