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.

163 lines
6.0 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. $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($me);
  39. $state = JWT::encode([
  40. 'me' => $me,
  41. 'authorization_endpoint' => $authorizationEndpoint,
  42. 'return_to' => $request->get('return_to'),
  43. 'time' => time(),
  44. 'exp' => time()+300 // verified by the JWT library
  45. ], Config::$secretKey);
  46. if($authorizationEndpoint) {
  47. // If the user specified only an authorization endpoint, use that
  48. $authorizationURL = IndieAuth\Client::buildAuthorizationURL($authorizationEndpoint, $me, self::_buildRedirectURI(), Config::$clientID, $state);
  49. } else {
  50. // Otherwise, fall back to indieauth.com
  51. $authorizationURL = IndieAuth\Client::buildAuthorizationURL(Config::$defaultAuthorizationEndpoint, $me, self::_buildRedirectURI(), Config::$clientID, $state);
  52. }
  53. $response->setStatusCode(302);
  54. $response->headers->set('Location', $authorizationURL);
  55. return $response;
  56. }
  57. public function login_callback(Request $request, Response $response) {
  58. if(!$request->get('state') || !$request->get('code') || !$request->get('me')) {
  59. $response->setContent(view('login', [
  60. 'title' => 'Sign In to Telegraph',
  61. 'error' => 'Missing Parameters',
  62. 'error_description' => 'The auth server did not return the necessary parameters, <code>state</code> and <code>code</code> and <code>me</code>.'
  63. ]));
  64. return $response;
  65. }
  66. // Validate the "state" parameter to ensure this request originated at this client
  67. try {
  68. $state = JWT::decode($request->get('state'), Config::$secretKey, ['HS256']);
  69. if(!$state) {
  70. $response->setContent(view('login', [
  71. 'title' => 'Sign In to Telegraph',
  72. 'error' => 'Invalid State',
  73. 'error_description' => 'The <code>state</code> parameter was not valid.'
  74. ]));
  75. return $response;
  76. }
  77. } catch(Exception $e) {
  78. $response->setContent(view('login', [
  79. 'title' => 'Sign In to Telegraph',
  80. 'error' => 'Invalid State',
  81. 'error_description' => 'The <code>state</code> parameter was invalid:<br>'.htmlspecialchars($e->getMessage())
  82. ]));
  83. return $response;
  84. }
  85. // Discover the authorization endpoint from the "me" that was returned by the auth server
  86. // This allows the auth server to return a different URL than the user originally entered,
  87. // for example if the user enters multiusersite.example the auth server can return multiusersite.example/alice
  88. if($state->authorization_endpoint) { // only discover the auth endpoint if one was originally found, otherwise use our fallback
  89. $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint($request->get('me'));
  90. } else {
  91. $authorizationEndpoint = Config::$defaultAuthorizationEndpoint;
  92. }
  93. // Verify the code with the auth server
  94. $token = IndieAuth\Client::verifyIndieAuthCode($authorizationEndpoint, $request->get('code'), $request->get('me'), self::_buildRedirectURI(), Config::$clientID, $request->get('state'), true);
  95. if(!array_key_exists('auth', $token) || !array_key_exists('me', $token['auth'])) {
  96. // The auth server didn't return a "me" URL
  97. $response->setContent(view('login', [
  98. 'title' => 'Sign In to Telegraph',
  99. 'error' => 'Invalid Auth Server Response',
  100. 'error_description' => 'The authorization server did not return a valid response:<br>'.htmlspecialchars(json_encode($token))
  101. ]));
  102. return $response;
  103. }
  104. // Create or load the user
  105. $user = ORM::for_table('users')->where('url', $token['auth']['me'])->find_one();
  106. if(!$user) {
  107. $user = ORM::for_table('users')->create();
  108. $user->url = $token['auth']['me'];
  109. $user->created_at = date('Y-m-d H:i:s');
  110. $user->last_login = date('Y-m-d H:i:s');
  111. $user->save();
  112. // Create a site for them with the default role
  113. $site = ORM::for_table('sites')->create();
  114. $site->name = 'My Website';
  115. $site->url = $token['auth']['me'];
  116. $site->created_by = $user->id;
  117. $site->created_at = date('Y-m-d H:i:s');
  118. $site->save();
  119. $role = ORM::for_table('roles')->create();
  120. $role->site_id = $site->id;
  121. $role->user_id = $user->id;
  122. $role->role = 'owner';
  123. $role->token = random_string(32);
  124. $role->save();
  125. } else {
  126. $user->last_login = date('Y-m-d H:i:s');
  127. $user->save();
  128. }
  129. q()->queue('Telegraph\ProfileFetcher', 'fetch', [$user->id]);
  130. session_start();
  131. $_SESSION['user_id'] = $user->id;
  132. $response->setStatusCode(302);
  133. $response->headers->set('Location', ($state->return_to ?: '/dashboard'));
  134. return $response;
  135. }
  136. private static function _buildRedirectURI() {
  137. return Config::$base . 'login/callback';
  138. }
  139. }