src/Service/IdentityManagement/UserSession.php
<?php
namespace Lullabot\Mpx\Service\IdentityManagement;
use Lullabot\Mpx\Cache\Adapter\PHPArray\ArrayCachePool;
use Lullabot\Mpx\Client;
use Lullabot\Mpx\Exception\TokenNotFoundException;
use Lullabot\Mpx\Token;
use Lullabot\Mpx\TokenCachePool;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Lock\PersistingStoreInterface;
/**
* Defines a class for authenticating a user with mpx.
*
* @see http://help.theplatform.com/display/wsf2/Identity+management+service+API+reference
* @see http://help.theplatform.com/display/wsf2/User+operations
*/
class UserSession
{
use LoggerAwareTrait;
/**
* The URL to sign in a user.
*/
final public const SIGN_IN_URL = 'https://identity.auth.theplatform.com/idm/web/Authentication/signIn';
/**
* The URL to sign out a given token for a user.
*/
final public const SIGN_OUT_URL = 'https://identity.auth.theplatform.com/idm/web/Authentication/signOut';
/**
* @var Client
*/
protected $client;
/**
* The backend lock store used to store a lock when signing in to mpx.
*
* @var PersistingStoreInterface
*/
protected $store;
/**
* The cache of authentication tokens.
*
* @var TokenCachePool
*/
protected $tokenCachePool;
/**
* The user to authenticate as.
*
* @var UserInterface
*/
protected $user;
/**
* Construct a new mpx user.
*
* @param UserInterface $user The user to authenticate as.
* @param Client $client The client used to access mpx.
* @param \Symfony\Component\Lock\PersistingStoreInterface|null $store (optional) The lock backend to store locks in.
* @param \Lullabot\Mpx\TokenCachePool|null $tokenCachePool (optional) The cache of authentication tokens.
*
* @see \Psr\Log\NullLogger To disable logging of token requests.
*/
public function __construct(UserInterface $user, Client $client, PersistingStoreInterface $store = null, TokenCachePool $tokenCachePool = null)
{
$this->user = $user;
$this->client = $client;
$this->store = $store;
if (!$tokenCachePool) {
$tokenCachePool = new TokenCachePool(new ArrayCachePool());
}
$this->tokenCachePool = $tokenCachePool;
$this->logger = new NullLogger();
}
/**
* Get a current authentication token for the account.
*
* This method will automatically generate a new token if one does not exist.
*
* @todo Do we want to make this async?
*
* @param int $duration (optional) The number of seconds for which the token should be valid.
* @param bool $reset Force fetching a new token, even if one exists.
*
* @return Token A valid mpx authentication token.
*/
public function acquireToken(int $duration = null, bool $reset = false): Token
{
if ($reset) {
$this->tokenCachePool->deleteToken($this);
}
// We assume that the cache is backed by shared storage across multiple
// requests. In that case, it's possible for another thread to set a
// token between the above delete and the next try block.
try {
$token = $this->tokenCachePool->getToken($this);
} catch (TokenNotFoundException) {
$token = $this->signInWithLock($duration);
}
return $token;
}
/**
* Sign in the user and return the current token.
*
* @param int $duration (optional) The number of seconds for which the token should be valid.
*/
protected function signIn($duration = null): Token
{
$options = $this->signInOptions($duration);
$response = $this->client->request(
'GET',
self::SIGN_IN_URL,
$options
);
$data = \GuzzleHttp\Utils::jsonDecode($response->getBody(), true);
$token = $this->tokenFromResponse($data);
$this->logger->info(
'Retrieved a new mpx token {token} for user {username} that expires on {date}.',
[
'token' => $token->getValue(),
'username' => $this->user->getMpxUsername(),
'date' => date(\DATE_ISO8601, $token->getExpiration()),
]
);
return $token;
}
/**
* Sign out the user.
*/
public function signOut()
{
// @todo Handle that the token may be expired.
// @todo Handle and log that mpx may error on the signout.
$this->client->request(
'GET',
self::SIGN_OUT_URL,
[
'query' => [
'schema' => '1.0',
'form' => 'json',
'_token' => (string) $this->tokenCachePool->getToken($this),
],
]
);
$this->tokenCachePool->deleteToken($this);
}
/**
* Sign in to mpx, with a lock to prevent sign-in stampedes.
*
* @param int|null $duration (optional) The number of seconds that the sign-in token should be valid for.
*
* @return \Lullabot\Mpx\Token The token.
*/
protected function signInWithLock(int $duration = null): Token
{
if ($this->store) {
$factory = new LockFactory($this->store);
$factory->setLogger($this->logger);
$lock = $factory->createLock($this->user->getMpxUsername(), 10);
// Blocking means this will throw an exception on failure.
$lock->acquire(true);
}
try {
// It's possible another thread has signed in for us, so check for a token first.
$token = $this->tokenCachePool->getToken($this);
} catch (TokenNotFoundException) {
// We have the lock, and there's no token, so sign in.
$token = $this->signIn($duration);
}
return $token;
}
/**
* Instantiate and cache a token.
*
* @param array $data The mpx signIn() response data.
*
* @return Token The new token.
*/
private function tokenFromResponse(array $data): Token
{
$token = Token::fromResponseData($data);
// Save the token to the cache and return it.
$this->tokenCachePool->setToken($this, $token);
return $token;
}
/**
* Return the query parameters for signing in.
*
* @param int $duration The duration to sign in for.
*
* @return array An array of query parameters.
*/
private function signInOptions($duration = null): array
{
$options = [];
$options['auth'] = [
$this->user->getMpxUsername(),
$this->user->getMpxPassword(),
];
// @todo Make this a class constant.
$options['query'] = [
'schema' => '1.0',
'form' => 'json',
];
// @todo move these to POST.
// https://docs.theplatform.com/help/wsf-signin-method#signInmethod-JSONPOSTexample
if (!empty($duration)) {
// API expects this value in milliseconds, not seconds.
$options['query']['_duration'] = $duration * 1000;
$options['query']['_idleTimeout'] = $duration * 1000;
}
return $options;
}
/**
* Return the user associated with this session.
*
* @return User
*/
public function getUser(): UserInterface
{
return $this->user;
}
}