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.

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