Skip to content

Commit

Permalink
2.0 Auth POC (#1)
Browse files Browse the repository at this point in the history
* add the scratch file

* Cleanup 2.0

* remove scratch file; add more readme text

* lowercasing

* Add the asset data and fix some type typos
  • Loading branch information
chrisvanpatten authored Aug 4, 2019
1 parent b6a31b6 commit 6859e2f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 100 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Vanilla Authenticator

PHP library for remotely authenticating with Vanilla Forums.
PHP library for programmatically authenticating with Vanilla Forums.

## Example

```
```php
<?php

$auth = new VanillaAuthentication('http://my-forum-name.vanillaforums.com');
Expand All @@ -17,6 +17,19 @@ try {
}
```

After authenticating, use the Guzzle client to fetch additional data as the authenticated user:

```php
<?php

$client = $auth->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.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"require": {
"php": "^7.0",
"guzzlehttp/guzzle": "^6.0",
"querypath/QueryPath": "^3.0"
"querypath/querypath": "^3.0"
},
"autoload": {
"psr-4": {
Expand Down
223 changes: 126 additions & 97 deletions src/Authenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 = [
Expand All @@ -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
Expand All @@ -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;
}
Expand All @@ -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();
Expand All @@ -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;
}
Expand All @@ -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( "<html><body>{$body}</body></html>" )->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( "<html><body>{$body}</body></html>" )->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;
}
}

0 comments on commit 6859e2f

Please sign in to comment.