<?php
namespace App\Service\Security;
use App\Exception\Security\ChangePasswordException;
use App\Exception\Security\CreateUserException;
use App\Exception\Security\InvalidAccessTokenException;
use App\Exception\Security\UpdateUserException;
use Aqarmap\Bundle\UserBundle\Constant\Oauth2GrantTypes;
use Aqarmap\Bundle\UserBundle\Entity\User;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request as httpRequest;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class AuthServer
{
private HttpClientInterface $client;
private CacheItemPoolInterface $cachePool;
private LoggerInterface $logger;
private string $authClientId;
private string $authClientSecret;
private string $publicServerUrl;
public function __construct(HttpClientInterface $authClient, CacheItemPoolInterface $cachePool, LoggerInterface $logger, string $authClientId, string $authClientSecret, string $publicServerUrl)
{
$this->client = $authClient;
$this->authClientId = $authClientId;
$this->authClientSecret = $authClientSecret;
$this->publicServerUrl = $publicServerUrl;
$this->cachePool = $cachePool;
$this->logger = $logger;
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
*/
public function getUser(string $token): array
{
return $this->client->request('GET', 'api/profile', [
'auth_bearer' => $token,
'headers' => [
'Accept' => 'application/json',
],
])->toArray();
}
public function getTokenInfo(string $token, bool $useCache = true): array
{
$cachedRequest = $this->cachePool->getItem(sprintf('token-info-%s', sha1($token)));
if (!$useCache || !$cachedRequest->isHit() || empty($cachedRequest->get())) {
$request = $this->client->request('GET', 'api/token-info', [
'auth_bearer' => $token,
'headers' => [
'Accept' => 'application/json',
],
]);
$this->logger->debug('Token Info: Validating against auth server.');
if (Response::HTTP_OK == $request->getStatusCode()) {
$cachedRequest->set($request->toArray(false));
$cachedRequest->expiresAfter(60);
$this->cachePool->save($cachedRequest);
} else {
$this->logger->debug(sprintf('Token Info: Invalid token. Status code: %s', $request->getStatusCode()));
throw new InvalidAccessTokenException(sprintf('Token Info: Invalid token. Status code: %s', $request->getStatusCode()));
}
} else {
$this->logger->debug('Token Info: Retrieved from cache.');
}
return $cachedRequest->get();
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
* @throws CreateUserException
*/
public function register(User $user): array
{
$countryCode = $user->getCountryCode() ?? $user->getTempCountryCode();
try {
$request = $this->client->request('POST', 'api/register', [
'body' => [
'fullName' => $user->getFullName(),
'email' => $user->getEmailCanonical(),
'plainPassword' => $user->getPlainPassword(),
'phoneNumber' => $countryCode.ltrim($user->getMainPhoneNumber(), '0'),
],
]);
if (Response::HTTP_OK != $request->getStatusCode()) {
$response = $request->toArray(false);
$errors = $this->parseFormErrors($response['form'] ?? null);
throw new CreateUserException($errors['firstError'], $request->getStatusCode());
}
return $request->toArray();
} catch (\Exception $exception) {
throw new CreateUserException($exception->getMessage(), $exception->getCode(), $exception);
}
}
public function createUser(string $name, ?string $email, ?string $phoneNumber, ?string $password): array
{
try {
$request = $this->client->request('POST', 'api/register', [
'body' => [
'fullName' => $name,
'email' => $email,
'plainPassword' => $password,
'phoneNumber' => $phoneNumber,
],
]);
if (Response::HTTP_OK != $request->getStatusCode()) {
$response = $request->toArray(false);
$errors = $this->parseFormErrors($response['form'] ?? null);
throw new CreateUserException(reset($errors), $request->getStatusCode());
}
return $request->toArray();
} catch (\Exception $exception) {
throw new CreateUserException($exception->getMessage(), $exception->getCode(), $exception);
}
}
/**
* @throws TransportExceptionInterface
* @throws ServerExceptionInterface
* @throws RedirectionExceptionInterface
* @throws ClientExceptionInterface
* @throws DecodingExceptionInterface
*/
public function getToken(httpRequest $request)
{
$grantType = $request->get('grant_type', Oauth2GrantTypes::PASSWORD);
$username = $request->get('username', $request->get('email'));
$password = $request->request->get('plainPassword')['first'] ?? $request->request->get('plainPassword');
return $this->client->request('POST', '/token', [
'body' => [
'client_id' => $this->authClientId,
'client_secret' => $this->authClientSecret,
'grant_type' => $grantType,
'username' => $username,
'password' => $password,
],
])->toArray();
}
public function getClientCredentialsAccessToken($useCache = true)
{
$cache = $this->cachePool->getItem('client-credentials-access-token');
if (!$useCache || !$cache->isHit() || empty($cache->get())) {
$request = $this->client->request('POST', '/token', [
'body' => [
'client_id' => $this->authClientId,
'client_secret' => $this->authClientSecret,
'grant_type' => 'client_credentials',
],
])->toArray();
$cache->set($request['access_token']);
$cache->expiresAfter((int) floor(($request['expires_in'] ?? 60) / 2));
$this->cachePool->save($cache);
}
return $cache->get();
}
public function lookupUserByLoginIdentifier(string $loginIdentifier): ?string
{
$request = $this->client->request('POST', 'api/user/lookup', [
'body' => [
'loginIdentifier' => $loginIdentifier,
],
'auth_bearer' => $this->getClientCredentialsAccessToken(),
'headers' => [
'Host' => $this->getPublicHost(),
],
]);
return $request->toArray()['user']['id'] ?? null;
}
/**
* @throws TransportExceptionInterface
* @throws ChangePasswordException
*/
public function changePassword(UserInterface $user, string $currentPassword, string $newPassword): void
{
try {
$request = $this->client->request('POST', 'api/user/change-password', [
'body' => [
'plainPassword[first]' => $newPassword,
'plainPassword[second]' => $newPassword,
'currentPassword' => $currentPassword,
],
'auth_bearer' => $user->getUserAccessToken(),
]);
if (Response::HTTP_OK != $request->getStatusCode()) {
$response = $request->toArray(false);
throw new ChangePasswordException($response['message'], $request->getStatusCode());
}
} catch (\Exception $exception) {
throw new ChangePasswordException($exception->getMessage(), $exception->getCode(), $exception);
}
}
public function resetPassword(string $email): void
{
try {
$this->client->request('POST', 'api/request-reset-password', [
'body' => [
'email' => $email,
],
'auth_bearer' => $this->getClientCredentialsAccessToken(),
'headers' => [
'Host' => $this->getPublicHost(),
],
]);
} catch (\Exception $exception) {
throw new ChangePasswordException($exception->getMessage(), $exception->getCode(), $exception);
}
}
/**
* @throws UpdateUserException
*/
public function updateUser(UserInterface $user): void
{
$body = [
'fullName' => $user->getFullName(),
'phoneNumber' => ltrim($user->getPhoneNumber(), '+'),
'email' => $user->getEmail(),
];
$response = $this->client->request('POST', sprintf('api/user/update?_locale=%s', $user->getLanguage()), [
'body' => $body,
'auth_bearer' => $user->getUserAccessToken(),
'headers' => [
'Accept-Language' => $user->getLanguage(),
],
]);
if (Response::HTTP_OK != $response->getStatusCode()) {
$responseBody = $response->toArray(false);
$errors = $this->parseFormErrors($responseBody['form'] ?? null);
throw new UpdateUserException(reset($errors) ?? json_encode(['body' => $body, 'response' => $responseBody]), $response->getStatusCode());
}
}
public function parseFormErrors(?array $response): array
{
$errorList = [];
if (!empty($response['errors'])) {
foreach ($response['errors'] as $error) {
if ($message = $error['message'] ?? null) {
$errorList[] = $message;
}
}
}
if (isset($response['children'])) {
foreach ($response['children'] as $children) {
$errorList = array_merge($errorList, $this->parseFormErrors($children));
}
}
return $errorList;
}
private function getPublicHost(): string
{
return parse_url($this->publicServerUrl, PHP_URL_HOST);
}
}