apioauthauthorize.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708
  1. <?php
  2. /**
  3. * StatusNet, the distributed open-source microblogging tool
  4. *
  5. * Authorize an OAuth request token
  6. *
  7. * PHP version 5
  8. *
  9. * LICENCE: This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU Affero General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU Affero General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU Affero General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. *
  22. * @category API
  23. * @package StatusNet
  24. * @author Zach Copley <zach@status.net>
  25. * @copyright 2010-2011 StatusNet, Inc.
  26. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  27. * @link http://status.net/
  28. */
  29. if (!defined('STATUSNET')) {
  30. exit(1);
  31. }
  32. /**
  33. * Authorize an OAuth request token
  34. *
  35. * @category API
  36. * @package StatusNet
  37. * @author Zach Copley <zach@status.net>
  38. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
  39. * @link http://status.net/
  40. */
  41. class ApiOAuthAuthorizeAction extends ApiOAuthAction
  42. {
  43. var $oauthTokenParam;
  44. var $reqToken;
  45. var $callback;
  46. var $app;
  47. var $nickname;
  48. var $password;
  49. var $store;
  50. /**
  51. * Is this a read-only action?
  52. *
  53. * @return boolean false
  54. */
  55. function isReadOnly($args)
  56. {
  57. return false;
  58. }
  59. function prepare($args)
  60. {
  61. parent::prepare($args);
  62. $this->nickname = $this->trimmed('nickname');
  63. $this->password = $this->arg('password');
  64. $this->oauthTokenParam = $this->arg('oauth_token');
  65. $this->mode = $this->arg('mode');
  66. $this->store = new ApiGNUsocialOAuthDataStore();
  67. try {
  68. $this->app = $this->store->getAppByRequestToken($this->oauthTokenParam);
  69. } catch (Exception $e) {
  70. $this->clientError($e->getMessage());
  71. }
  72. return true;
  73. }
  74. /**
  75. * Handle input, produce output
  76. *
  77. * Switches on request method; either shows the form or handles its input.
  78. *
  79. * @param array $args $_REQUEST data
  80. *
  81. * @return void
  82. */
  83. function handle($args)
  84. {
  85. parent::handle($args);
  86. if ($_SERVER['REQUEST_METHOD'] == 'POST') {
  87. $this->handlePost();
  88. } else {
  89. // Make sure a oauth_token parameter was provided
  90. if (empty($this->oauthTokenParam)) {
  91. // TRANS: Client error given when no oauth_token was passed to the OAuth API.
  92. $this->clientError(_('No oauth_token parameter provided.'));
  93. } else {
  94. // Check to make sure the token exists
  95. $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam);
  96. if (empty($this->reqToken)) {
  97. // TRANS: Client error given when an invalid request token was passed to the OAuth API.
  98. $this->clientError(_('Invalid request token.'));
  99. } else {
  100. // Check to make sure we haven't already authorized the token
  101. if ($this->reqToken->state != 0) {
  102. // TRANS: Client error given when an invalid request token was passed to the OAuth API.
  103. $this->clientError(_('Request token already authorized.'));
  104. }
  105. }
  106. }
  107. // make sure there's an app associated with this token
  108. if (empty($this->app)) {
  109. // TRANS: Client error given when an invalid request token was passed to the OAuth API.
  110. $this->clientError(_('Invalid request token.'));
  111. }
  112. $name = $this->app->name;
  113. $this->showForm();
  114. }
  115. }
  116. function handlePost()
  117. {
  118. // check session token for CSRF protection.
  119. $token = $this->trimmed('token');
  120. if (!$token || $token != common_session_token()) {
  121. $this->showForm(
  122. // TRANS: Form validation error in API OAuth authorisation because of an invalid session token.
  123. _('There was a problem with your session token. Try again, please.'));
  124. return;
  125. }
  126. // check creds
  127. $user = null;
  128. if (!common_logged_in()) {
  129. // XXX Force credentials check?
  130. // @fixme this should probably use a unified login form handler
  131. $user = null;
  132. if (Event::handle('StartOAuthLoginCheck', array($this, &$user))) {
  133. $user = common_check_user($this->nickname, $this->password);
  134. }
  135. Event::handle('EndOAuthLoginCheck', array($this, &$user));
  136. if (empty($user)) {
  137. // TRANS: Form validation error given when an invalid username and/or password was passed to the OAuth API.
  138. $this->showForm(_("Invalid nickname / password!"));
  139. return;
  140. }
  141. } else {
  142. $user = common_current_user();
  143. }
  144. // fetch the token
  145. $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam);
  146. assert(!empty($this->reqToken));
  147. if ($this->arg('allow')) {
  148. // mark the req token as authorized
  149. try {
  150. $this->store->authorize_token($this->oauthTokenParam);
  151. } catch (Exception $e) {
  152. $this->serverError($e->getMessage());
  153. }
  154. common_log(
  155. LOG_INFO,
  156. sprintf(
  157. "API OAuth - User %d (%s) has authorized request token %s for OAuth application %d (%s).",
  158. $user->id,
  159. $user->nickname,
  160. $this->reqToken->tok,
  161. $this->app->id,
  162. $this->app->name
  163. )
  164. );
  165. $tokenAssoc = new Oauth_token_association();
  166. $tokenAssoc->profile_id = $user->id;
  167. $tokenAssoc->application_id = $this->app->id;
  168. $tokenAssoc->token = $this->oauthTokenParam;
  169. $tokenAssoc->created = common_sql_now();
  170. $result = $tokenAssoc->insert();
  171. if (!$result) {
  172. common_log_db_error($tokenAssoc, 'INSERT', __FILE__);
  173. // TRANS: Server error displayed when a database action fails.
  174. $this->serverError(_('Database error inserting oauth_token_association.'));
  175. }
  176. $callback = $this->getCallback();
  177. if (!empty($callback) && $this->reqToken->verified_callback != 'oob') {
  178. $targetUrl = $this->buildCallbackUrl(
  179. $callback,
  180. array(
  181. 'oauth_token' => $this->oauthTokenParam,
  182. 'oauth_verifier' => $this->reqToken->verifier // 1.0a
  183. )
  184. );
  185. common_log(LOG_INFO, "Redirecting to callback: $targetUrl");
  186. // Redirect the user to the provided OAuth callback
  187. common_redirect($targetUrl, 303);
  188. } elseif ($this->app->type == 2) {
  189. // Strangely, a web application seems to want to do the OOB
  190. // workflow. Because no callback was specified anywhere.
  191. common_log(
  192. LOG_WARNING,
  193. sprintf(
  194. "API OAuth - No callback provided for OAuth web client ID %s (%s) "
  195. . "during authorization step. Falling back to OOB workflow.",
  196. $this->app->id,
  197. $this->app->name
  198. )
  199. );
  200. }
  201. // Otherwise, inform the user that the rt was authorized
  202. $this->showAuthorized();
  203. } else if ($this->arg('cancel')) {
  204. common_log(
  205. LOG_INFO,
  206. sprintf(
  207. "API OAuth - User %d (%s) refused to authorize request token %s for OAuth application %d (%s).",
  208. $user->id,
  209. $user->nickname,
  210. $this->reqToken->tok,
  211. $this->app->id,
  212. $this->app->name
  213. )
  214. );
  215. try {
  216. $this->store->revoke_token($this->oauthTokenParam, 0);
  217. } catch (Exception $e) {
  218. $this->ServerError($e->getMessage());
  219. }
  220. $callback = $this->getCallback();
  221. // If there's a callback available, inform the consumer the user
  222. // has refused authorization
  223. if (!empty($callback) && $this->reqToken->verified_callback != 'oob') {
  224. $targetUrl = $this->buildCallbackUrl(
  225. $callback,
  226. array(
  227. 'oauth_problem' => 'user_refused',
  228. )
  229. );
  230. common_log(LOG_INFO, "Redirecting to callback: $targetUrl");
  231. // Redirect the user to the provided OAuth callback
  232. common_redirect($targetUrl, 303);
  233. }
  234. // otherwise inform the user that authorization for the rt was declined
  235. $this->showCanceled();
  236. } else {
  237. // TRANS: Client error given on when invalid data was passed through a form in the OAuth API.
  238. $this->clientError(_('Unexpected form submission.'));
  239. }
  240. }
  241. /**
  242. * Show body - override to add a special CSS class for the authorize
  243. * page's "desktop mode" (minimal display)
  244. *
  245. * Calls template methods
  246. *
  247. * @return nothing
  248. */
  249. function showBody()
  250. {
  251. $bodyClasses = array();
  252. if ($this->desktopMode()) {
  253. $bodyClasses[] = 'oauth-desktop-mode';
  254. }
  255. if (common_current_user()) {
  256. $bodyClasses[] = 'user_in';
  257. }
  258. $attrs = array('id' => strtolower($this->trimmed('action')));
  259. if (!empty($bodyClasses)) {
  260. $attrs['class'] = implode(' ', $bodyClasses);
  261. }
  262. $this->elementStart('body', $attrs);
  263. $this->elementStart('div', array('id' => 'wrap'));
  264. if (Event::handle('StartShowHeader', array($this))) {
  265. $this->showHeader();
  266. Event::handle('EndShowHeader', array($this));
  267. }
  268. $this->showCore();
  269. if (Event::handle('StartShowFooter', array($this))) {
  270. $this->showFooter();
  271. Event::handle('EndShowFooter', array($this));
  272. }
  273. $this->elementEnd('div');
  274. $this->showScripts();
  275. $this->elementEnd('body');
  276. }
  277. function showForm($error=null)
  278. {
  279. $this->error = $error;
  280. $this->showPage();
  281. }
  282. function showScripts()
  283. {
  284. parent::showScripts();
  285. if (!common_logged_in()) {
  286. $this->autofocus('nickname');
  287. }
  288. }
  289. /**
  290. * Title of the page
  291. *
  292. * @return string title of the page
  293. */
  294. function title()
  295. {
  296. // TRANS: Title for a page where a user can confirm/deny account access by an external application.
  297. return _('An application would like to connect to your account');
  298. }
  299. /**
  300. * Shows the authorization form.
  301. *
  302. * @return void
  303. */
  304. function showContent()
  305. {
  306. $this->elementStart('form', array('method' => 'post',
  307. 'id' => 'form_apioauthauthorize',
  308. 'class' => 'form_settings',
  309. 'action' => common_local_url('ApiOAuthAuthorize')));
  310. $this->elementStart('fieldset');
  311. $this->element('legend', array('id' => 'apioauthauthorize_allowdeny'),
  312. // TRANS: Fieldset legend.
  313. _('Allow or deny access'));
  314. $this->hidden('token', common_session_token());
  315. $this->hidden('mode', $this->mode);
  316. $this->hidden('oauth_token', $this->oauthTokenParam);
  317. $this->hidden('oauth_callback', $this->callback);
  318. $this->elementStart('ul', 'form_data');
  319. $this->elementStart('li');
  320. $this->elementStart('p');
  321. if (!empty($this->app->icon) && $this->app->name != 'anonymous') {
  322. $this->element('img', array('src' => $this->app->icon));
  323. }
  324. $access = ($this->app->access_type & Oauth_application::$writeAccess) ?
  325. 'access and update' : 'access';
  326. if ($this->app->name == 'anonymous') {
  327. // Special message for the anonymous app and consumer.
  328. // TRANS: User notification of external application requesting account access.
  329. // TRANS: %3$s is the access type requested (read-write or read-only), %4$s is the StatusNet sitename.
  330. $msg = _('An application would like the ability ' .
  331. 'to <strong>%3$s</strong> your %4$s account data. ' .
  332. 'You should only give access to your %4$s account ' .
  333. 'to third parties you trust.');
  334. } else {
  335. // TRANS: User notification of external application requesting account access.
  336. // TRANS: %1$s is the application name requesting access, %2$s is the organisation behind the application,
  337. // TRANS: %3$s is the access type requested, %4$s is the StatusNet sitename.
  338. $msg = _('The application <strong>%1$s</strong> by ' .
  339. '<strong>%2$s</strong> would like the ability ' .
  340. 'to <strong>%3$s</strong> your %4$s account data. ' .
  341. 'You should only give access to your %4$s account ' .
  342. 'to third parties you trust.');
  343. }
  344. $this->raw(sprintf($msg,
  345. $this->app->name,
  346. $this->app->organization,
  347. $access,
  348. common_config('site', 'name')));
  349. $this->elementEnd('p');
  350. $this->elementEnd('li');
  351. $this->elementEnd('ul');
  352. // quickie hack
  353. $button = false;
  354. if (!common_logged_in()) {
  355. if (Event::handle('StartOAuthLoginForm', array($this, &$button))) {
  356. $this->elementStart('fieldset');
  357. // TRANS: Fieldset legend.
  358. $this->element('legend', null, _m('LEGEND','Account'));
  359. $this->elementStart('ul', 'form_data');
  360. $this->elementStart('li');
  361. // TRANS: Field label on OAuth API authorisation form.
  362. $this->input('nickname', _('Nickname'));
  363. $this->elementEnd('li');
  364. $this->elementStart('li');
  365. // TRANS: Field label on OAuth API authorisation form.
  366. $this->password('password', _('Password'));
  367. $this->elementEnd('li');
  368. $this->elementEnd('ul');
  369. $this->elementEnd('fieldset');
  370. }
  371. Event::handle('EndOAuthLoginForm', array($this, &$button));
  372. }
  373. $this->element('input', array('id' => 'cancel_submit',
  374. 'class' => 'submit submit form_action-primary',
  375. 'name' => 'cancel',
  376. 'type' => 'submit',
  377. // TRANS: Button text that when clicked will cancel the process of allowing access to an account
  378. // TRANS: by an external application.
  379. 'value' => _m('BUTTON','Cancel')));
  380. $this->element('input', array('id' => 'allow_submit',
  381. 'class' => 'submit submit form_action-secondary',
  382. 'name' => 'allow',
  383. 'type' => 'submit',
  384. // TRANS: Button text that when clicked will allow access to an account by an external application.
  385. 'value' => $button ? $button : _m('BUTTON','Allow')));
  386. $this->elementEnd('fieldset');
  387. $this->elementEnd('form');
  388. }
  389. /**
  390. * Instructions for using the form
  391. *
  392. * For "remembered" logins, we make the user re-login when they
  393. * try to change settings. Different instructions for this case.
  394. *
  395. * @return void
  396. */
  397. function getInstructions()
  398. {
  399. // TRANS: Form instructions.
  400. return _('Authorize access to your account information.');
  401. }
  402. /**
  403. * A local menu
  404. *
  405. * Shows different login/register actions.
  406. *
  407. * @return void
  408. */
  409. function showLocalNav()
  410. {
  411. // NOP
  412. }
  413. /*
  414. * Checks to see if a the "mode" parameter is present in the request
  415. * and set to "desktop". If it is, the page is meant to be displayed in
  416. * a small frame of another application, and we should suppress the
  417. * header, aside, and footer.
  418. */
  419. function desktopMode()
  420. {
  421. if (isset($this->mode) && $this->mode == 'desktop') {
  422. return true;
  423. } else {
  424. return false;
  425. }
  426. }
  427. /*
  428. * Override - suppress output in "desktop" mode
  429. */
  430. function showHeader()
  431. {
  432. if ($this->desktopMode() == false) {
  433. parent::showHeader();
  434. }
  435. }
  436. /*
  437. * Override - suppress output in "desktop" mode
  438. */
  439. function showAside()
  440. {
  441. if ($this->desktopMode() == false) {
  442. parent::showAside();
  443. }
  444. }
  445. /*
  446. * Override - suppress output in "desktop" mode
  447. */
  448. function showFooter()
  449. {
  450. if ($this->desktopMode() == false) {
  451. parent::showFooter();
  452. }
  453. }
  454. /**
  455. * Show site notice.
  456. *
  457. * @return nothing
  458. */
  459. function showSiteNotice()
  460. {
  461. // NOP
  462. }
  463. /**
  464. * Show notice form.
  465. *
  466. * Show the form for posting a new notice
  467. *
  468. * @return nothing
  469. */
  470. function showNoticeForm()
  471. {
  472. // NOP
  473. }
  474. /*
  475. * Show a nice message confirming the authorization
  476. * operation was canceled.
  477. *
  478. * @return nothing
  479. */
  480. function showCanceled()
  481. {
  482. $info = new InfoAction(
  483. // TRANS: Header for user notification after revoking OAuth access to an application.
  484. _('Authorization canceled.'),
  485. sprintf(
  486. // TRANS: User notification after revoking OAuth access to an application.
  487. // TRANS: %s is an OAuth token.
  488. _('The request token %s has been revoked.'),
  489. $this->oauthTokenParam
  490. )
  491. );
  492. $info->showPage();
  493. }
  494. /*
  495. * Show a nice message that the authorization was successful.
  496. * If the operation is out-of-band, show a pin.
  497. *
  498. * @return nothing
  499. */
  500. function showAuthorized()
  501. {
  502. $title = null;
  503. $msg = null;
  504. if ($this->app->name == 'anonymous') {
  505. $title =
  506. // TRANS: Title of the page notifying the user that an anonymous client application was successfully authorized to access the user's account with OAuth.
  507. _('You have successfully authorized the application');
  508. $msg =
  509. // TRANS: Message notifying the user that an anonymous client application was successfully authorized to access the user's account with OAuth.
  510. _('Please return to the application and enter the following security code to complete the process.');
  511. } else {
  512. $title = sprintf(
  513. // TRANS: Title of the page notifying the user that the client application was successfully authorized to access the user's account with OAuth.
  514. // TRANS: %s is the authorised application name.
  515. _('You have successfully authorized %s'),
  516. $this->app->name
  517. );
  518. $msg = sprintf(
  519. // TRANS: Message notifying the user that the client application was successfully authorized to access the user's account with OAuth.
  520. // TRANS: %s is the authorised application name.
  521. _('Please return to %s and enter the following security code to complete the process.'),
  522. $this->app->name
  523. );
  524. }
  525. if ($this->reqToken->verified_callback == 'oob') {
  526. $pin = new ApiOAuthPinAction(
  527. $title,
  528. $msg,
  529. $this->reqToken->verifier,
  530. $this->desktopMode()
  531. );
  532. $pin->showPage();
  533. } else {
  534. // NOTE: This would only happen if an application registered as
  535. // a web application but sent in 'oob' for the oauth_callback
  536. // parameter. Usually web apps will send in a callback and
  537. // not use the pin-based workflow.
  538. $info = new InfoAction(
  539. $title,
  540. $msg,
  541. $this->oauthTokenParam,
  542. $this->reqToken->verifier
  543. );
  544. $info->showPage();
  545. }
  546. }
  547. /*
  548. * Figure out what the callback should be
  549. */
  550. function getCallback()
  551. {
  552. $callback = null;
  553. // Return the verified callback if we have one
  554. if ($this->reqToken->verified_callback != 'oob') {
  555. $callback = $this->reqToken->verified_callback;
  556. // Otherwise return the callback that was provided when
  557. // registering the app
  558. if (empty($callback)) {
  559. common_debug(
  560. "No verified callback found for request token, using application callback: "
  561. . $this->app->callback_url,
  562. __FILE__
  563. );
  564. $callback = $this->app->callback_url;
  565. }
  566. }
  567. return $callback;
  568. }
  569. /*
  570. * Properly format the callback URL and parameters so it's
  571. * suitable for a redirect in the OAuth dance
  572. *
  573. * @param string $url the URL
  574. * @param array $params an array of parameters
  575. *
  576. * @return string $url a URL to use for redirecting to
  577. */
  578. function buildCallbackUrl($url, $params)
  579. {
  580. foreach ($params as $k => $v) {
  581. $url = $this->appendQueryVar(
  582. $url,
  583. OAuthUtil::urlencode_rfc3986($k),
  584. OAuthUtil::urlencode_rfc3986($v)
  585. );
  586. }
  587. return $url;
  588. }
  589. /*
  590. * Append a new query parameter after any existing query
  591. * parameters.
  592. *
  593. * @param string $url the URL
  594. * @prarm string $k the parameter name
  595. * @param string $v value of the paramter
  596. *
  597. * @return string $url the new URL with added parameter
  598. */
  599. function appendQueryVar($url, $k, $v) {
  600. $url = preg_replace('/(.*)(\?|&)' . $k . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&');
  601. $url = substr($url, 0, -1);
  602. if (strpos($url, '?') === false) {
  603. return ($url . '?' . $k . '=' . $v);
  604. } else {
  605. return ($url . '&' . $k . '=' . $v);
  606. }
  607. }
  608. }