diff --git a/controllers/auth.php b/controllers/auth.php index 4318486..837cbe0 100644 --- a/controllers/auth.php +++ b/controllers/auth.php @@ -1,27 +1,65 @@ get('/auth/start', function() use($app) { $req = $app->request(); $params = $req->params(); - // the "me" parameter is user input, and may be in a couple of different forms: - // aaronparecki.com http://aaronparecki.com http://aaronparecki.com/ - if(!array_key_exists('me', $params) || !($me = IndieAuth\Client::normalizeMeURL($params['me']))) { + $defaultScope = 'create update media'; + + list($authorizationURL, $error) = IndieAuth\Client::begin($params['me'], $defaultScope); + + // Double check for a micropub endpoint here for debugging purposes + if(!$error) { + $me = $_SESSION['indieauth_url']; // set by IndieAuth\Client::begin(), will be the normalized and resolved URL + $micropubEndpoint = $_SESSION['indieauth']['micropub_endpoint'] = IndieAuth\Client::discoverMicropubEndpoint($me); + if(!$micropubEndpoint) { + $error['error'] = 'missing_micropub_endpoint'; + } + } + + if($error && in_array($error['error'], ['missing_authorization_endpoint','missing_token_endpoint','missing_micropub_endpoint'])) { + // Display debug info for these particular errors + + $me = $_SESSION['indieauth_url']; // set by IndieAuth\Client::begin(), will be the normalized and resolved URL + $micropubEndpoint = $_SESSION['indieauth']['micropub_endpoint'] = IndieAuth\Client::discoverMicropubEndpoint($me); + $tokenEndpoint = $_SESSION['indieauth']['token_endpoint'] = IndieAuth\Client::discoverTokenEndpoint($me); + $authorizationEndpoint = $_SESSION['indieauth']['authorization_endpoint'] = IndieAuth\Client::discoverAuthorizationEndpoint($me); + + $html = render('auth_start', array( + 'title' => 'Sign In', + 'me' => $me, + 'authorizing' => $me, + 'meParts' => parse_url($me), + 'tokenEndpoint' => $tokenEndpoint, + 'micropubEndpoint' => $micropubEndpoint, + 'authorizationEndpoint' => $authorizationEndpoint, + 'authorizationURL' => false + )); + $app->response()->body($html); + return; + } + + // Handle other errors like connection errors by showing a generic error page + if($error) { $html = render('auth_error', array( 'title' => 'Sign In', - 'error' => 'Invalid "me" Parameter', - 'errorDescription' => 'The URL you entered, "' . $params['me'] . '" is not valid.' + 'error' => $error['error'], + 'errorDescription' => $error['error_description'], )); $app->response()->body($html); return; } + $me = $_SESSION['indieauth_url']; // set by IndieAuth\Client::begin(), will be the normalized and resolved URL + $micropubEndpoint = $_SESSION['indieauth']['micropub_endpoint'] = IndieAuth\Client::discoverMicropubEndpoint($me); + $tokenEndpoint = $_SESSION['indieauth']['token_endpoint'] = IndieAuth\Client::discoverTokenEndpoint($me); + $authorizationEndpoint = $_SESSION['indieauth']['authorization_endpoint'] = IndieAuth\Client::discoverAuthorizationEndpoint($me); + if(k($params, 'redirect')) { $_SESSION['redirect_after_login'] = $params['redirect']; } @@ -29,26 +67,6 @@ $app->get('/auth/start', function() use($app) { $_SESSION['reply'] = $params['reply']; } - $_SESSION['attempted_me'] = $me; - - $_SESSION['indieauth'] = [ - 'authorization_endpoint' => ($authorizationEndpoint=IndieAuth\Client::discoverAuthorizationEndpoint($me)), - 'token_endpoint' => ($tokenEndpoint=IndieAuth\Client::discoverTokenEndpoint($me)), - 'micropub_endpoint' => ($micropubEndpoint=IndieAuth\Client::discoverMicropubEndpoint($me)), - ]; - - $defaultScope = 'create update media'; - - if($tokenEndpoint && $micropubEndpoint && $authorizationEndpoint) { - // Generate a "state" parameter for the request - $state = IndieAuth\Client::generateStateParameter(); - $_SESSION['auth_state'] = $state; - - $authorizationURL = IndieAuth\Client::buildAuthorizationURL($authorizationEndpoint, $me, buildRedirectURI(), Config::$base_url, $state, $defaultScope); - } else { - $authorizationURL = false; - } - // If the user has already signed in before and has a micropub access token, // and the endpoints are all the same, skip the debugging screens and redirect // immediately to the auth endpoint. @@ -60,13 +78,6 @@ $app->get('/auth/start', function() use($app) { && $user->authorization_endpoint == $authorizationEndpoint && !array_key_exists('restart', $params)) { - // TODO: fix this by caching the endpoints maybe in the session instead of writing them to the DB here. - // Then remove the line below that blanks out the access token - $user->micropub_endpoint = $micropubEndpoint; - $user->authorization_endpoint = $authorizationEndpoint; - $user->token_endpoint = $tokenEndpoint; - $user->save(); - // Request whatever scope was previously granted $authorizationURL = parse_url($authorizationURL); $authorizationURL['scope'] = $user->micropub_scope; @@ -104,6 +115,8 @@ $app->get('/auth/redirect', function() use($app) { $req = $app->request(); $params = $req->params(); + // Override scope from the form the user selects + if(!isset($params['scope'])) $params['scope'] = ''; @@ -121,87 +134,28 @@ $app->get('/auth/callback', function() use($app) { $req = $app->request(); $params = $req->params(); - // If there is no state in the session, start the login again - if(!array_key_exists('auth_state', $_SESSION)) { - $html = render('auth_error', array( - 'title' => 'Auth Callback', - 'error' => 'Missing session state', - 'errorDescription' => 'Something went wrong, please try signing in again, and make sure cookies are enabled for this domain.' - )); - $app->response()->body($html); - return; - } - - if(!array_key_exists('code', $params) || trim($params['code']) == '') { - $html = render('auth_error', array( - 'title' => 'Auth Callback', - 'error' => 'Missing authorization code', - 'errorDescription' => 'No authorization code was provided in the request.' - )); - $app->response()->body($html); - return; - } - - // Verify the state came back and matches what we set in the session - // Should only fail for malicious attempts, ok to show a not as nice error message - if(!array_key_exists('state', $params)) { - $html = render('auth_error', array( - 'title' => 'Auth Callback', - 'error' => 'Missing state parameter', - '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.' - )); - $app->response()->body($html); - return; - } - - if($params['state'] != $_SESSION['auth_state']) { - $html = render('auth_error', array( - 'title' => 'Auth Callback', - 'error' => 'Invalid state', - '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.' - )); - $app->response()->body($html); - return; - } + list($token, $error) = IndieAuth\Client::complete($params, true); - if(!isset($_SESSION['attempted_me'])) { + if($error) { $html = render('auth_error', [ 'title' => 'Auth Callback', - 'error' => 'Missing data', - '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.' + 'error' => $error['error'], + 'errorDescription' => $error['error_description'], ]); $app->response()->body($html); return; } - $me = $_SESSION['attempted_me']; - // Now the basic sanity checks have passed. Time to start providing more helpful messages when there is an error. - // An authorization code is in the query string, and we want to exchange that for an access token at the token endpoint. + $me = $token['me']; - // Discover the endpoints + // Use the discovered endpoints saved in the session $micropubEndpoint = $_SESSION['indieauth']['micropub_endpoint']; $tokenEndpoint = $_SESSION['indieauth']['token_endpoint']; - if($tokenEndpoint) { - $token = IndieAuth\Client::getAccessToken($tokenEndpoint, $params['code'], $me, buildRedirectURI(), Config::$base_url, true); - } else { - $token = array('auth'=>false, 'response'=>false); - } - $redirectToDashboardImmediately = false; // If a valid access token was returned, store the token info in the session and they are signed in if(k($token['auth'], array('me','access_token','scope'))) { - // Double check that the domain of the returned "me" matches the expected - if(!\p3k\url\host_matches($token['auth']['me'], $me)) { - $html = render('auth_error', [ - 'title' => 'Error Signing In', - 'error' => 'Invalid user', - 'errorDescription' => 'The user URL that was returned from the token endpoint ('.$token['auth']['me'].') did not match the domain of the user signing in ('.$me.').' - ]); - $app->response()->body($html); - return; - } $_SESSION['auth'] = $token['auth']; $_SESSION['me'] = $me = $token['auth']['me']; @@ -233,8 +187,6 @@ $app->get('/auth/callback', function() use($app) { get_micropub_config($user, ['q'=>'config']); } - unset($_SESSION['auth_state']); - unset($_SESSION['attempted_me']); unset($_SESSION['indieauth']); if($redirectToDashboardImmediately || k($_SESSION, 'dontask')) {