From 6859e2f98b2e8f6a75f59e5ff48a52b6ba176f6c Mon Sep 17 00:00:00 2001 From: Chris Van Patten Date: Sun, 4 Aug 2019 08:31:32 -0400 Subject: [PATCH] 2.0 Auth POC (#1) * add the scratch file * Cleanup 2.0 * remove scratch file; add more readme text * lowercasing * Add the asset data and fix some type typos --- README.md | 17 +++- composer.json | 2 +- src/Authenticator.php | 223 ++++++++++++++++++++++++------------------ 3 files changed, 142 insertions(+), 100 deletions(-) diff --git a/README.md b/README.md index b21bf7e..7a15a31 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Vanilla Authenticator -PHP library for remotely authenticating with Vanilla Forums. +PHP library for programmatically authenticating with Vanilla Forums. ## Example -``` +```php getClient(); + +$response = $client->get('http://my-forum-name.vanillaforums.com/profile.json'); +$profile = json_decode((string) $response->getBody(), true); + +print_r($profile); +``` + ## About Tomodomo Tomodomo is a creative agency for magazine publishers. We use custom design and technology to speed up your editorial workflow, engage your readers, and build sustainable subscription revenue for your business. diff --git a/composer.json b/composer.json index 55b8faa..e4e263b 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "require": { "php": "^7.0", "guzzlehttp/guzzle": "^6.0", - "querypath/QueryPath": "^3.0" + "querypath/querypath": "^3.0" }, "autoload": { "psr-4": { diff --git a/src/Authenticator.php b/src/Authenticator.php index 6f9da76..4053dfc 100644 --- a/src/Authenticator.php +++ b/src/Authenticator.php @@ -11,14 +11,7 @@ class Authenticator * * @var string */ - protected $loginUrl = '/entry/signin?Target='; - - /** - * Default page to redirect to and fetch - * - * @var string - */ - protected $targetPath = 'profile.json'; + protected $loginUrl = '/entry/signin'; /** * The Guzzle object of the login page @@ -34,12 +27,12 @@ class Authenticator */ protected $postData = []; - /** - * Guzzle client - * - * @var \GuzzleHttp\Client - */ - public $client; + /** + * Guzzle client + * + * @var \GuzzleHttp\Client + */ + public $client; /** * Instantiate a Guzzle client @@ -49,8 +42,8 @@ class Authenticator public function __construct(string $baseUrl, array $options = []) { if ($baseUrl === null) { - throw new Exception('missing_baseurl', 'You need to provide a base URL.'); - } + throw new \Exception('missing_baseurl', 'You need to provide a base URL.'); + } // Set default args $defaults = [ @@ -65,58 +58,34 @@ public function __construct(string $baseUrl, array $options = []) // Instantiate the Guzzle client $this->client = new GuzzleClient($options); - return; + return; } - /** - * Retrieve the Guzzle client - * - * @return \GuzzleHttp\Client - */ - public function getClient() - { - return $this->client; - } - - /** - * Set the authentication credentials - * - * @param string $email - * @param string $password - * - * @return void - */ - public function setCredentials(string $email, string $password) - { - $this->email = $email; - $this->password = $password; - - return; - } - - /** - * Set the target path to load on sign-in - * - * @param string $targetPath - * - * @return void - */ - public function setTargetPath(string $targetPath) - { - $this->targetPath = $targetPath; + /** + * Retrieve the Guzzle client + * + * @return \GuzzleHttp\Client + */ + public function getClient() + { + return $this->client; + } - return; - } + /** + * Set the authentication credentials + * + * @param string $email + * @param string $password + * + * @return void + */ + public function setCredentials(string $email, string $password) + { + $this->email = $email; + $this->password = $password; - /** - * Get the full target URL - * - * @return string - */ - public function getLoginUrl() - { - return $this->loginUrl . $this->targetPath; - } + return; + } /** * Retrive the Guzzle object for the login page @@ -125,8 +94,8 @@ public function getLoginUrl() */ private function getLoginPage() { - // Set the login page - $this->loginPage = $this->loginPage ?? $this->getClient()->request('GET', $this->getLoginUrl()); + // Set the login page + $this->loginPage = $this->loginPage ?? $this->getClient()->get($this->loginUrl); return $this->loginPage; } @@ -147,14 +116,13 @@ private function getDefaultLoginFields() // Loop through the inputs foreach($find as $item) { - // Grab the input name $key = $item->attr('name'); // If the item is a checkbox or the RememberMe field, skip it if (in_array($key, ['Checkboxes[]', 'RememberMe', 'Sign In'])) { continue; - } + } // Otherwise, add to our array $fields[$key] = $item->val(); @@ -175,17 +143,21 @@ private function getPostData() // If we don't have postData already, retrieve the default login fields if (empty($this->postData)) { $this->postData = $this->getDefaultLoginFields(); - } + } // If the username is set, add it to the postData if ($this->email !== null) { $this->setPostData('Email', $this->email); - } + } // If the password is set, add it to the postData if ($this->password !== null) { $this->setPostData('Password', $this->password); - } + } + + // This helps us spoof the AJAX call + $this->setPostData('DeliveryType', 'ASSET'); + $this->setPostData('DeliveryMethod', 'JSON'); return $this->postData; } @@ -202,53 +174,110 @@ private function setPostData(string $key, string $value) { $this->postData[$key] = $value; - return; + return; } /** * Authenticate a user * - * @return array|bool + * @return bool */ - public function authenticate() + public function authenticate() : bool { + // Get our custom POST request data + $postData = $this->getPostData(); + + // Get a transient key + $body = $this->makeAuthenticationRequest($postData); + + // Add the transient key to the payload + $postData['TransientKey'] = $this->getTransientKeyFromResponseBody($body['Data'] ?? ''); + + // Really login now + $body = $this->makeAuthenticationRequest($postData); + + // Handle errors + if ($this->bodyHasErrors($body['Data'] ?? '') || $body['FormSaved'] === false) { + throw new \Exception('Your login was incorrect. Double-check your username and password, and try again.'); + } + + // We have successfully authenticated + return true; + } + + /** + * Get the transient key from the response body. + * + * @param string $body + * @return string + */ + public function getTransientKeyFromResponseBody(string $body) : string + { + $qp = html5qp( "{$body}" )->find('#Form_TransientKey'); + + foreach ($qp as $input) { + return $input->val(); + } + + throw new \Exception('no_transient', 'Could not find a transient key.'); + } + + /** + * Make the authentication request. + * + * @param array $postData + * @return string + */ + public function makeAuthenticationRequest(array $postData) : array + { // POST with our postData - $response = $this->getClient()->request('POST', $this->getLoginUrl(), [ - 'form_params' => $this->getPostData(), - ]); + $response = $this->getClient()->request( + 'POST', + $this->loginUrl, + [ + 'form_params' => $postData, + 'headers' => [ + 'X-Requested-With' => 'XMLHttpRequest', + ], + ] + ); // If the response code isn't 200... if ($response->getStatusCode() !== 200) { throw new \Exception('There was an error authenticating your account.'); } - // Get the body (cast to a string, because Guzzle) - $body = (string) $response->getBody(); - - // If looking for JSON, and we have valid/decodable JSON... - if ($this->targetPath === 'profile.json' && $this->user = json_decode($body, true)) { - return $this->user; - } + // Return the response body + return json_decode((string) $response->getBody(), true); + } + /** + * Check for errors in a response body. + * + * @param string $body + * @return bool + */ + public function bodyHasErrors(string $body) : bool + { // Get a queryable version of response errors - $qp = html5qp($body)->find('.Messages.Errors'); + $qp = html5qp( "{$body}" )->find('.Messages.Errors'); - // Throw an exception when the username is wrong - if (!empty($qp->get()) && strpos($qp->text(), 'no account could be found')) { - throw new \Exception('Your account could not be found.'); + if (empty($qp->get())) { + return false; + } + + if (strpos($qp->text(), 'no account could be found')) { + return true; } - // Throw an exception for invalid passwords - if (!empty($qp->get()) && strpos($qp->text(), 'password you entered was incorrect')) { - throw new \Exception('Your password was incorrect.'); + if (strpos($qp->text(), 'password you entered was incorrect')) { + return true; } - // Throw an exception for a generic error - if (!empty($qp->get()) && strpos($qp->text(), 'Bad login, double-check')) { - throw new \Exception('Your login was incorrect. Double-check your username and password, and try again.'); + if (strpos($qp->text(), 'login, double-check')) { + return true; } - // Return the response body - return (string) $response->getBody(); - } + return false; + } }