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.

182 lines
6.2 KiB

  1. <?php
  2. use Symfony\Component\HttpFoundation\Request;
  3. use Symfony\Component\HttpFoundation\Response;
  4. use \Firebase\JWT\JWT;
  5. class Auth {
  6. public function login(Request $request, Response $response) {
  7. session_start();
  8. if(session('user_id')) {
  9. $response->setStatusCode(302);
  10. $response->headers->set('Location', '/dashboard');
  11. } else {
  12. $response->setContent(view('login', [
  13. 'title' => 'Sign In to Telegraph',
  14. 'return_to' => $request->get('return_to')
  15. ]));
  16. }
  17. return $response;
  18. }
  19. public function logout(Request $request, Response $response) {
  20. session_start();
  21. if(session('user_id')) {
  22. $_SESSION['user_id'] = null;
  23. session_destroy();
  24. }
  25. $response->setStatusCode(302);
  26. $response->headers->set('Location', '/login');
  27. return $response;
  28. }
  29. public function login_start(Request $request, Response $response) {
  30. if(!$request->get('url') || !($me = IndieAuth\Client::normalizeMeURL($request->get('url')))) {
  31. $response->setContent(view('login', [
  32. 'title' => 'Sign In to Telegraph',
  33. 'error' => 'Invalid URL',
  34. 'error_description' => 'The URL you entered, "<strong>' . htmlspecialchars($request->get('url')) . '</strong>" is not valid.'
  35. ]));
  36. return $response;
  37. }
  38. // Check if the user's URL defines an authorization endpoint
  39. $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($me);
  40. if(!$authorizationEndpoint) {
  41. $authorizationEndpoint = Config::$defaultAuthorizationEndpoint;
  42. }
  43. $codeVerifier = IndieAuth\Client::generatePKCECodeVerifier();
  44. $state = JWT::encode([
  45. 'az' => $authorizationEndpoint,
  46. 'me' => $me,
  47. 'code_verifier' => $codeVerifier,
  48. 'return_to' => $request->get('return_to'),
  49. 'time' => time(),
  50. 'exp' => time()+300 // verified by the JWT library
  51. ], Config::$secretKey);
  52. $authorizationURL = IndieAuth\Client::buildAuthorizationURL($authorizationEndpoint, [
  53. 'me' => $me,
  54. 'redirect_uri' => self::_buildRedirectURI(),
  55. 'client_id' => Config::$clientID,
  56. 'state' => $state,
  57. 'code_verifier' => $codeVerifier,
  58. ]);
  59. $response->setStatusCode(302);
  60. $response->headers->set('Location', $authorizationURL);
  61. return $response;
  62. }
  63. public function login_callback(Request $request, Response $response) {
  64. if(!$request->get('state') || !$request->get('code')) {
  65. $response->setContent(view('login', [
  66. 'title' => 'Sign In to Telegraph',
  67. 'error' => 'Missing Parameters',
  68. 'error_description' => 'The auth server did not return the necessary parameters, <code>state</code> and <code>code</code>.'
  69. ]));
  70. return $response;
  71. }
  72. // Validate the "state" parameter to ensure this request originated at this client
  73. try {
  74. $state = JWT::decode($request->get('state'), Config::$secretKey, ['HS256']);
  75. if(!$state) {
  76. $response->setContent(view('login', [
  77. 'title' => 'Sign In to Telegraph',
  78. 'error' => 'Invalid State',
  79. 'error_description' => 'The <code>state</code> parameter was not valid.'
  80. ]));
  81. return $response;
  82. }
  83. } catch(Exception $e) {
  84. $response->setContent(view('login', [
  85. 'title' => 'Sign In to Telegraph',
  86. 'error' => 'Invalid State',
  87. 'error_description' => 'The <code>state</code> parameter was invalid:<br>'.htmlspecialchars($e->getMessage())
  88. ]));
  89. return $response;
  90. }
  91. $authorizationEndpoint = $state->az;
  92. // Verify the code with the auth server
  93. $data = IndieAuth\Client::exchangeAuthorizationCode($state->az, [
  94. 'code' => $request->get('code'),
  95. 'redirect_uri' => self::_buildRedirectURI(),
  96. 'client_id' => Config::$clientID,
  97. 'code_verifier' => $state->code_verifier,
  98. ]);
  99. if(!isset($data['response']['me'])) {
  100. // The authorization server didn't return a "me" URL
  101. $response->setContent(view('login', [
  102. 'title' => 'Sign In to Telegraph',
  103. 'error' => 'Invalid Auth Server Response',
  104. 'error_description' => 'The authorization server ('.$authorizationEndpoint.') did not return a valid response:<br><pre style="text-align:left; max-height: 400px; overflow: scroll;">HTTP '.$data['response_code']."\n\n".htmlspecialchars(json_encode($data)).'</pre>'
  105. ]));
  106. return $response;
  107. }
  108. // Verify the authorization endpoint matches
  109. if($data['response']['me'] != $state->me) {
  110. $newAuthorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($data['response']['me']);
  111. if($newAuthorizationEndpoint != $authorizationEndpoint) {
  112. $response->setContent(view('login', [
  113. 'title' => 'Sign In to Telegraph',
  114. 'error' => 'Invalid Authorization Endpoint',
  115. 'error_description' => 'The authorization endpoint for the returned profile URL ('.$data['response']['me'].') did not match the authorization endpoint used to begin the login.'
  116. ]));
  117. return $response;
  118. }
  119. }
  120. $me = $data['response']['me'];
  121. // Create or load the user
  122. $user = ORM::for_table('users')->where('url', $me)->find_one();
  123. if(!$user) {
  124. $user = ORM::for_table('users')->create();
  125. $user->url = $me;
  126. $user->created_at = date('Y-m-d H:i:s');
  127. $user->last_login = date('Y-m-d H:i:s');
  128. $user->save();
  129. // Create a site for them with the default role
  130. $site = ORM::for_table('sites')->create();
  131. $site->name = 'My Website';
  132. $site->url = $me;
  133. $site->created_by = $user->id;
  134. $site->created_at = date('Y-m-d H:i:s');
  135. $site->save();
  136. $role = ORM::for_table('roles')->create();
  137. $role->site_id = $site->id;
  138. $role->user_id = $user->id;
  139. $role->role = 'owner';
  140. $role->token = random_string(32);
  141. $role->save();
  142. } else {
  143. $user->last_login = date('Y-m-d H:i:s');
  144. $user->save();
  145. }
  146. q()->queue('Telegraph\ProfileFetcher', 'fetch', [$user->id]);
  147. session_start();
  148. $_SESSION['user_id'] = $user->id;
  149. $response->setStatusCode(302);
  150. $response->headers->set('Location', ($state->return_to ?: '/dashboard'));
  151. return $response;
  152. }
  153. private static function _buildRedirectURI() {
  154. return Config::$base . 'login/callback';
  155. }
  156. }