function getContent() { $this->gatekeeper(); $user = Idno::site()->session()->currentUser(); $code = $this->getInput('code'); $state = $this->getInput('state'); $me = $this->getInput('me'); $token_endpoint = IndieAuthClient::discoverTokenEndpoint($me); $micropub_endpoint = IndieAuthClient::discoverMicropubEndpoint($me); $hcard = IndieAuthClient::representativeHCard($me); $client_id = Idno::site()->config()->getDisplayURL(); $redirect_uri = Idno::site()->config()->getDisplayURL() . 'account/indiesyndicate/cb'; $result = IndieAuthClient::getAccessToken($token_endpoint, $code, $me, $redirect_uri, $client_id, $state); if (isset($result['me']) && isset($result['access_token'])) { $me = $result['me']; $token = $result['access_token']; $name = $me; if (!empty($hcard['properties']['name'])) { $name = $hcard['properties']['name'][0]; } else { $name = $me; } $user->indiesyndicate[$me] = ['name' => $name, 'access_token' => $token, 'micropub_endpoint' => $micropub_endpoint, 'method' => 'micropub']; $user->save(); Idno::site()->session()->addMessage('Successfully authorized ' . $me); } else { Idno::site()->session()->addErrorMessage('Authorization was declined or failed for ' . $me); } $this->forward(Idno::site()->config()->getDisplayURL() . 'account/indiesyndicate'); }
/** * Client * * Create a micropub client app — allows users to log in and authorize this app to make requests on their behalf to, * e.g. a micropub endpoint, authenticates requests based on remember-me cookie. * $dataToCookie and $dataFromCookie map between the array of information about the current user and the string value * stored in the remember-me cookie * * Adds routes: * /login (indieauth.login) — POST to this URL with required “me” and optional “next” parameters to start the login process * /authorize (indieauth.authorize) — the URL redirected to by the user’s authorization server after successful authorization. Checks details and sets remember-me cookie * /logout (indieauth.logout) — POST to this URL (with optional “next” parameter) whilst logged in to log out, i.e. remove the remmeber-me cookies. * * Adds ->before() handler which attaches data about the current user, which scopes they’ve granted this app (if any) * their URL, access token and micropub endpoint to $request. Example usage within a controller: * * $token = $request->attributes->get('indieauth.client.token', null); * if ($token !== null) { * // User is logged in as $token['me'] * if (!empty($token['access_token'] and !empty($token['micropub_endpoint'])) { * // The user has granted this app privileges detailed in $token['scope'], which can be carried out by sending * // requests to $token['micropub_endpoint'] with $token['access_token'] * // Now you might check that the “post” scope is granted, and create some new content on their site (pseudocode): * if (in_array('post', explode($scope, ' '))) { * micrpub_post($token['micropub_endpoint'], $token['access_token'], $postDetails); * } * } else { * // The user has logged in using the basic indieauth flow — we know that they’re authenticated as $token['me'], * // but they haven’t granted us any permissions. * } * } * * @param \Silex\Application $app * @param callable|null $dataToCookie * @param callable|null $dataFromCookie * @return \Symfony\Component\Routing\RouteCollection */ function client($app, $dataToCookie = null, $dataFromCookie = null) { $auth = $app['controllers_factory']; // If cookie mapping functions aren’t defined, use the simplest approach of encrypting the data. if ($dataToCookie === null) { $dataToCookie = function ($data) use($app) { return $app['encryption']->encrypt($data); }; } if ($dataFromCookie === null) { $dataFromCookie = function ($token) use($app) { return $app['encryption']->decrypt($token); }; } $app['indieauth'] = new IndieauthHelpers($app); // If no cookie lifetime is set, default to 60 days. $cookieLifetime = !empty($app['indieauth.cookielifetime']) ? $app['indieauth.cookielifetime'] : 60 * 60 * 24 * 60; $cookieName = !empty($app['indieauth.cookiename']) ? $app['indieauth.cookiename'] : 'indieauth_token'; $secureCookies = isset($app['indieauth.securecookies']) ? $app['indieauth.securecookies'] : true; $clientIdForRequest = function (Http\Request $request) use($app) { // If no explicit client ID is set, use the domain name of this site. return !empty($app['indieauth.clientid']) ? $app['indieauth.clientid'] : $request->getHttpHost(); }; $redirectUrlForRequest = function (Http\Request $request) use($app) { // If no default login redirect URL is set (it can be reset on a request-by-request basis by setting the “next” parameter), default to the homepage $defaultLoginRedirectUrl = !empty($app['indieauth.loginredirecturl']) ? $app['indieauth.loginredirecturl'] : "{$request->getScheme()}://{$request->getHttpHost()}"; return $request->request->get('next', $defaultLoginRedirectUrl); }; $auth->post('/login/', function (Http\Request $request) use($app, $cookieName, $redirectUrlForRequest, $clientIdForRequest, $secureCookies) { $me = $request->request->get('me'); $next = $redirectUrlForRequest($request); if ($me === null) { // TODO: better error handling, although in practical cases this will never happen. return $app->redirect($next); } $authorizationEndpoint = IndieAuth\Client::discoverAuthorizationEndpoint(ensureUrlHasHttp($me)); if ($authorizationEndpoint === false) { // If the current user has no authorization endpoint set, they are using the basic indieauth flow. $authorizationEndpoint = rtrim($app['indieauth.url'], '/') . '/auth'; return $app->redirect("{$authorizationEndpoint}?me={$me}&redirect_uri={$next}"); } // As more scopes become defined, this will need to be expanded + probably made configurable. $micropubEndpoint = IndieAuth\Client::discoverMicropubEndpoint(ensureUrlHasHttp($me)); $scope = !empty($micropubEndpoint) ? 'post' : ''; $random = mt_rand(1000000, pow(2, 31)); $redirectEndpoint = $app['url_generator']->generate('indieauth.authorize', [], true); $authorizationUrl = IndieAuth\Client::buildAuthorizationUrl($authorizationEndpoint, $me, $redirectEndpoint, $clientIdForRequest($request), $random, $scope); $response = $app->redirect($authorizationUrl); // Retain random state for five minutes in secure, HTTP-only cookie. $cookie = new Http\Cookie("{$cookieName}_random", $app['encryption']->encrypt($random), time() + 60 * 5, null, null, $secureCookies, true); $response->headers->setCookie($cookie); return $response; })->bind('indieauth.login'); $auth->get('/authorize/', function (Http\Request $request) use($app, $dataToCookie, $cookieName, $cookieLifetime, $redirectUrlForRequest, $clientIdForRequest, $secureCookies) { $random = $app['encryption']->decrypt($request->cookies->get("{$cookieName}_random")); $me = $request->query->get('me'); $state = $request->query->get('state'); $code = $request->query->get('code'); if ($state != $random) { $app['logger']->info('Authentication failed as state didn’t match random in cookie', ['state' => $state, 'cookie.random' => $random]); return $app->redirect('/'); } $tokenEndpoint = IndieAuth\Client::discoverTokenEndpoint($me); $redirectUrl = $app['url_generator']->generate('indieauth.authorize', [], true); $token = IndieAuth\Client::getAccessToken($tokenEndpoint, $code, $me, $redirectUrl, $clientIdForRequest($request), $state); $token['micropub_endpoint'] = IndieAuth\Client::discoverMicropubEndpoint(ensureUrlHasHttp($me)); $app['logger']->info("Indieauth: Got token, discovered micropub endpoint", ['token' => $token]); $response = $app->redirect($redirectUrlForRequest($request)); // Store token data in secure, HTTP-only session cookie. $tokenCookie = new Http\Cookie($cookieName, $dataToCookie($token), time() + $cookieLifetime, null, null, $secureCookies, true); $response->headers->setCookie($tokenCookie); return $response; })->bind('indieauth.authorize'); $auth->post('/logout/', function (Http\Request $request) use($app, $redirectUrlForRequest, $cookieName) { // In the bizarre case that a request to /logout/ also had a basic-flow indieauth token, prevent the ->after() handler // from setting remember-me cookies. $request->attributes->set('indieauth.islogoutrequest', true); $response = $app->redirect($redirectUrlForRequest($request)); $app['indieauth']->logoutResponse($response); return $response; })->bind('logout'); $app->before(function (Http\Request $request) use($app, $dataFromCookie, $cookieName) { // If the user has full indieauth credentials, make their token information (scope, // access key, micropub endpoint) available to controllers. if ($request->cookies->has($cookieName)) { try { /** * indieauth.client.token is an array potentially containing the following properties: * * me: URL of the current user (guaranteed to exist + be a valid URL) * * scope: space-separated list of scopes the user has granted this app * * access_token: the user’s access token * * micropub_endpoint: the user’s micropub endpoint * * If only “me” exists then the user is logged in using the basic indieauth flow — their URL is confirmed but they * haven’t granted us any permissions, and maybe don’t even have a micropub endpoint. */ $token = $dataFromCookie($request->cookies->get($cookieName)); $loggableToken = $token; // Don’t log the sensitive access key, only the length, so devs can see if there *was* an access token or not. $loggableToken['access_token'] = 'Unlogged string of length ' . strlen($token['access_token']); $request->attributes->set('indieauth.client.token', $token); $app['logger']->info('Request has indieauth token', ['token' => $loggableToken]); } catch (Exception $e) { $app['logger']->warning("Caught an unhandled exception whilst running \$dataFromCookie on the current user’s indieauth token — consider handling this exception appropriately", ['exception class' => get_class($e), 'message' => $e->getMessage()]); } } elseif ($request->query->has('token')) { // The user is logging in using the basic indieauth flow, so all we know about them is their URL. // A remember-me cookie will be set for them later. $client = new Guzzle\Http\Client($app['indieauth.url']); try { $response = $client->get('session?token=' . $request->query->get('token'))->send(); $basicToken = json_decode($response->getBody(true), true); $request->attributes->set('indieauth.client.token', $basicToken); } catch (Guzzle\Common\Exception\GuzzleException $e) { $app['logger']->warning('Authenticating user with indieauth.com failed: ' . $e->getMessage()); } } }); $app->after(function (Http\Request $request, Http\Response $response) use($app, $dataToCookie, $cookieName, $cookieLifetime, $secureCookies) { // If the request is a basic-flow indieauth login request, set a remember-me cookie. if ($request->query->has('token') and $request->attributes->has('indieauth.client.token') and !$request->attributes->get('indieauth.islogoutrequest', false)) { $tokenCookie = new Http\Cookie($cookieName, $dataToCookie($request->attributes->get('indieauth.client.token')), time() + $cookieLifetime, null, null, $secureCookies, true); $response->headers->setCookie($tokenCookie); } }); return $auth; }