Mirror of Quill
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

387 lines
14 KiB

10 years ago
10 years ago
  1. <?php
  2. use Abraham\TwitterOAuth\TwitterOAuth;
  3. function buildRedirectURI() {
  4. return Config::$base_url . 'auth/callback';
  5. }
  6. $app->get('/auth/start', function() use($app) {
  7. $req = $app->request();
  8. $params = $req->params();
  9. // the "me" parameter is user input, and may be in a couple of different forms:
  10. // aaronparecki.com http://aaronparecki.com http://aaronparecki.com/
  11. if(!array_key_exists('me', $params) || !($me = IndieAuth\Client::normalizeMeURL($params['me']))) {
  12. $html = render('auth_error', array(
  13. 'title' => 'Sign In',
  14. 'error' => 'Invalid "me" Parameter',
  15. 'errorDescription' => 'The URL you entered, "<strong>' . $params['me'] . '</strong>" is not valid.'
  16. ));
  17. $app->response()->body($html);
  18. return;
  19. }
  20. if(k($params, 'redirect')) {
  21. $_SESSION['redirect_after_login'] = $params['redirect'];
  22. }
  23. if(k($params, 'reply')) {
  24. $_SESSION['reply'] = $params['reply'];
  25. }
  26. $_SESSION['attempted_me'] = $me;
  27. $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($me);
  28. $tokenEndpoint = IndieAuth\Client::discoverTokenEndpoint($me);
  29. $micropubEndpoint = IndieAuth\Client::discoverMicropubEndpoint($me);
  30. $defaultScope = 'create update';
  31. if($tokenEndpoint && $micropubEndpoint && $authorizationEndpoint) {
  32. // Generate a "state" parameter for the request
  33. $state = IndieAuth\Client::generateStateParameter();
  34. $_SESSION['auth_state'] = $state;
  35. $authorizationURL = IndieAuth\Client::buildAuthorizationURL($authorizationEndpoint, $me, buildRedirectURI(), Config::$base_url, $state, $defaultScope);
  36. } else {
  37. $authorizationURL = false;
  38. }
  39. // If the user has already signed in before and has a micropub access token,
  40. // and the endpoints are all the same, skip the debugging screens and redirect
  41. // immediately to the auth endpoint.
  42. // This will still generate a new access token when they finish logging in.
  43. $user = ORM::for_table('users')->where('url', $me)->find_one();
  44. if($user && $user->micropub_access_token
  45. && $user->micropub_endpoint == $micropubEndpoint
  46. && $user->token_endpoint == $tokenEndpoint
  47. && $user->authorization_endpoint == $authorizationEndpoint
  48. && !array_key_exists('restart', $params)) {
  49. // TODO: fix this by caching the endpoints maybe in the session instead of writing them to the DB here.
  50. // Then remove the line below that blanks out the access token
  51. $user->micropub_endpoint = $micropubEndpoint;
  52. $user->authorization_endpoint = $authorizationEndpoint;
  53. $user->token_endpoint = $tokenEndpoint;
  54. $user->save();
  55. // Request whatever scope was previously granted
  56. $authorizationURL = parse_url($authorizationURL);
  57. $authorizationURL['scope'] = $user->micropub_scope;
  58. $authorizationURL = http_build_url($authorizationURL);
  59. $app->redirect($authorizationURL, 302);
  60. } else {
  61. if(!$user)
  62. $user = ORM::for_table('users')->create();
  63. $user->url = $me;
  64. $user->date_created = date('Y-m-d H:i:s');
  65. $user->micropub_endpoint = $micropubEndpoint;
  66. $user->authorization_endpoint = $authorizationEndpoint;
  67. $user->token_endpoint = $tokenEndpoint;
  68. $user->micropub_access_token = ''; // blank out the access token if they attempt to sign in again
  69. $user->save();
  70. if(k($params, 'dontask') && $params['dontask']) {
  71. // Request whatever scope was previously granted
  72. $authorizationURL = parse_url($authorizationURL);
  73. $authorizationURL['scope'] = $user->micropub_scope ?: $defaultScope;
  74. $authorizationURL = http_build_url($authorizationURL);
  75. $_SESSION['dontask'] = 1;
  76. $app->redirect($authorizationURL, 302);
  77. }
  78. $html = render('auth_start', array(
  79. 'title' => 'Sign In',
  80. 'me' => $me,
  81. 'authorizing' => $me,
  82. 'meParts' => parse_url($me),
  83. 'tokenEndpoint' => $tokenEndpoint,
  84. 'micropubEndpoint' => $micropubEndpoint,
  85. 'authorizationEndpoint' => $authorizationEndpoint,
  86. 'authorizationURL' => $authorizationURL
  87. ));
  88. $app->response()->body($html);
  89. }
  90. });
  91. $app->get('/auth/redirect', function() use($app) {
  92. $req = $app->request();
  93. $params = $req->params();
  94. if(!isset($params['scope']))
  95. $params['scope'] = '';
  96. $authorizationURL = parse_url($params['authorization_url']);
  97. parse_str($authorizationURL['query'], $query);
  98. $query['scope'] = $params['scope'];
  99. $authorizationURL['query'] = http_build_query($query);
  100. $authorizationURL = http_build_url($authorizationURL);
  101. $app->redirect($authorizationURL);
  102. return;
  103. });
  104. $app->get('/auth/callback', function() use($app) {
  105. $req = $app->request();
  106. $params = $req->params();
  107. // If there is no state in the session, start the login again
  108. if(!array_key_exists('auth_state', $_SESSION)) {
  109. $app->redirect('/?error=missing_session_state');
  110. return;
  111. }
  112. if(!array_key_exists('code', $params) || trim($params['code']) == '') {
  113. $html = render('auth_error', array(
  114. 'title' => 'Auth Callback',
  115. 'error' => 'Missing authorization code',
  116. 'errorDescription' => 'No authorization code was provided in the request.'
  117. ));
  118. $app->response()->body($html);
  119. return;
  120. }
  121. // Verify the state came back and matches what we set in the session
  122. // Should only fail for malicious attempts, ok to show a not as nice error message
  123. if(!array_key_exists('state', $params)) {
  124. $html = render('auth_error', array(
  125. 'title' => 'Auth Callback',
  126. 'error' => 'Missing state parameter',
  127. 'errorDescription' => 'No state parameter was provided in the request. This shouldn\'t happen. It is possible this is a malicious authorization attempt, or your authorization server failed to pass back the "state" parameter.'
  128. ));
  129. $app->response()->body($html);
  130. return;
  131. }
  132. if($params['state'] != $_SESSION['auth_state']) {
  133. $html = render('auth_error', array(
  134. 'title' => 'Auth Callback',
  135. 'error' => 'Invalid state',
  136. 'errorDescription' => 'The state parameter provided did not match the state provided at the start of authorization. This is most likely caused by a malicious authorization attempt.'
  137. ));
  138. $app->response()->body($html);
  139. return;
  140. }
  141. if(!isset($_SESSION['attempted_me'])) {
  142. $html = render('auth_error', [
  143. 'title' => 'Auth Callback',
  144. 'error' => 'Missing data',
  145. 'errorDescription' => 'We forgot who was logging in. It\'s possible you took too long to finish signing in, or something got mixed up by signing in in another tab.'
  146. ]);
  147. $app->response()->body($html);
  148. return;
  149. }
  150. $me = $_SESSION['attempted_me'];
  151. // Now the basic sanity checks have passed. Time to start providing more helpful messages when there is an error.
  152. // An authorization code is in the query string, and we want to exchange that for an access token at the token endpoint.
  153. // Discover the endpoints
  154. $micropubEndpoint = IndieAuth\Client::discoverMicropubEndpoint($me);
  155. $tokenEndpoint = IndieAuth\Client::discoverTokenEndpoint($me);
  156. if($tokenEndpoint) {
  157. $token = IndieAuth\Client::getAccessToken($tokenEndpoint, $params['code'], $me, buildRedirectURI(), Config::$base_url, k($params,'state'), true);
  158. } else {
  159. $token = array('auth'=>false, 'response'=>false);
  160. }
  161. $redirectToDashboardImmediately = false;
  162. // If a valid access token was returned, store the token info in the session and they are signed in
  163. if(k($token['auth'], array('me','access_token','scope'))) {
  164. // Double check that the domain of the returned "me" matches the expected
  165. if(parse_url($token['auth']['me'], PHP_URL_HOST) != parse_url($me, PHP_URL_HOST)) {
  166. $html = render('auth_error', [
  167. 'title' => 'Error Signing In',
  168. 'error' => 'Invalid user',
  169. 'errorDescription' => 'The user URL that was returned in the access token did not match the domain of the user signing in.'
  170. ]);
  171. $app->response()->body($html);
  172. return;
  173. }
  174. $_SESSION['auth'] = $token['auth'];
  175. $_SESSION['me'] = $me = $token['auth']['me'];
  176. $user = ORM::for_table('users')->where('url', $me)->find_one();
  177. if($user) {
  178. // Already logged in, update the last login date
  179. $user->last_login = date('Y-m-d H:i:s');
  180. // If they have logged in before and we already have an access token, then redirect to the dashboard now
  181. if($user->micropub_access_token)
  182. $redirectToDashboardImmediately = true;
  183. } else {
  184. // New user! Store the user in the database
  185. $user = ORM::for_table('users')->create();
  186. $user->url = $me;
  187. $user->date_created = date('Y-m-d H:i:s');
  188. }
  189. $user->micropub_endpoint = $micropubEndpoint;
  190. $user->micropub_access_token = $token['auth']['access_token'];
  191. $user->micropub_scope = $token['auth']['scope'];
  192. $user->micropub_response = $token['response'];
  193. $user->save();
  194. $_SESSION['user_id'] = $user->id();
  195. // Make a request to the micropub endpoint to discover the syndication targets and media endpoint if any.
  196. // Errors are silently ignored here. The user will be able to retry from the new post interface and get feedback.
  197. get_micropub_config($user, ['q'=>'config']);
  198. }
  199. unset($_SESSION['auth_state']);
  200. unset($_SESSION['attempted_me']);
  201. if($redirectToDashboardImmediately || k($_SESSION, 'dontask')) {
  202. unset($_SESSION['dontask']);
  203. if(k($_SESSION, 'redirect_after_login')) {
  204. $dest = $_SESSION['redirect_after_login'];
  205. unset($_SESSION['redirect_after_login']);
  206. $app->redirect($dest, 302);
  207. } else {
  208. $query = [];
  209. if(k($_SESSION, 'reply')) {
  210. $query['reply'] = $_SESSION['reply'];
  211. unset($_SESSION['reply']);
  212. }
  213. $app->redirect('/new?' . http_build_query($query), 302);
  214. }
  215. } else {
  216. $html = render('auth_callback', array(
  217. 'title' => 'Sign In',
  218. 'me' => $me,
  219. 'authorizing' => $me,
  220. 'meParts' => parse_url($me),
  221. 'tokenEndpoint' => $tokenEndpoint,
  222. 'auth' => $token['auth'],
  223. 'response' => $token['response'],
  224. 'curl_error' => (array_key_exists('error', $token) ? $token['error'] : false),
  225. 'destination' => (k($_SESSION, 'redirect_after_login') ?: '/new')
  226. ));
  227. $app->response()->body($html);
  228. }
  229. });
  230. $app->get('/signout', function() use($app) {
  231. unset($_SESSION['auth']);
  232. unset($_SESSION['me']);
  233. unset($_SESSION['auth_state']);
  234. unset($_SESSION['user_id']);
  235. $app->redirect('/', 302);
  236. });
  237. $app->post('/auth/reset', function() use($app) {
  238. if($user=require_login($app, false)) {
  239. revoke_micropub_token($user->micropub_access_token, $user->token_endpoint);
  240. $user->authorization_endpoint = '';
  241. $user->token_endpoint = '';
  242. $user->micropub_endpoint = '';
  243. $user->authorization_endpoint = '';
  244. $user->micropub_media_endpoint = '';
  245. $user->micropub_scope = '';
  246. $user->micropub_access_token = '';
  247. $user->save();
  248. unset($_SESSION['auth']);
  249. unset($_SESSION['me']);
  250. unset($_SESSION['auth_state']);
  251. unset($_SESSION['user_id']);
  252. }
  253. $app->redirect('/', 302);
  254. });
  255. $app->post('/auth/twitter', function() use($app) {
  256. if($user=require_login($app, false)) {
  257. $params = $app->request()->params();
  258. // User just auth'd with twitter, store the access token
  259. $user->twitter_access_token = $params['twitter_token'];
  260. $user->twitter_token_secret = $params['twitter_secret'];
  261. $user->save();
  262. $app->response()['Content-type'] = 'application/json';
  263. $app->response()->body(json_encode(array(
  264. 'result' => 'ok'
  265. )));
  266. } else {
  267. $app->response()['Content-type'] = 'application/json';
  268. $app->response()->body(json_encode(array(
  269. 'result' => 'error'
  270. )));
  271. }
  272. });
  273. function getTwitterLoginURL(&$twitter) {
  274. $request_token = $twitter->oauth('oauth/request_token', [
  275. 'oauth_callback' => Config::$base_url . 'auth/twitter/callback'
  276. ]);
  277. $_SESSION['twitter_auth'] = $request_token;
  278. return $twitter->url('oauth/authorize', ['oauth_token' => $request_token['oauth_token']]);
  279. }
  280. $app->get('/auth/twitter', function() use($app) {
  281. $params = $app->request()->params();
  282. if($user=require_login($app, false)) {
  283. // If there is an existing Twitter token, check if it is valid
  284. // Otherwise, generate a Twitter login link
  285. $twitter_login_url = false;
  286. if(array_key_exists('login', $params)) {
  287. $twitter = new TwitterOAuth(Config::$twitterClientID, Config::$twitterClientSecret);
  288. $twitter_login_url = getTwitterLoginURL($twitter);
  289. } else {
  290. $twitter = new TwitterOAuth(Config::$twitterClientID, Config::$twitterClientSecret,
  291. $user->twitter_access_token, $user->twitter_token_secret);
  292. if($user->twitter_access_token) {
  293. if($twitter->get('account/verify_credentials')) {
  294. $app->response()['Content-type'] = 'application/json';
  295. $app->response()->body(json_encode(array(
  296. 'result' => 'ok'
  297. )));
  298. return;
  299. } else {
  300. // If the existing twitter token is not valid, generate a login link
  301. $twitter_login_url = getTwitterLoginURL($twitter);
  302. }
  303. } else {
  304. $twitter_login_url = getTwitterLoginURL($twitter);
  305. }
  306. }
  307. $app->response()['Content-type'] = 'application/json';
  308. $app->response()->body(json_encode(array(
  309. 'url' => $twitter_login_url
  310. )));
  311. } else {
  312. $app->response()['Content-type'] = 'application/json';
  313. $app->response()->body(json_encode(array(
  314. 'result' => 'error'
  315. )));
  316. }
  317. });
  318. $app->get('/auth/twitter/callback', function() use($app) {
  319. if($user=require_login($app)) {
  320. $params = $app->request()->params();
  321. $twitter = new TwitterOAuth(Config::$twitterClientID, Config::$twitterClientSecret,
  322. $_SESSION['twitter_auth']['oauth_token'], $_SESSION['twitter_auth']['oauth_token_secret']);
  323. $credentials = $twitter->oauth('oauth/access_token', ['oauth_verifier' => $params['oauth_verifier']]);
  324. $user->twitter_access_token = $credentials['oauth_token'];
  325. $user->twitter_token_secret = $credentials['oauth_token_secret'];
  326. $user->twitter_username = $credentials['screen_name'];
  327. $user->save();
  328. $app->redirect('/settings');
  329. }
  330. });