<?php
/**
* @copyright Copyright (c) 2020 Biceps
*/
namespace Biceps\OAuth\Security;
use Biceps\OAuth\Client\Provider;
use Biceps\OAuth\Grant\ResetToken;
use Exception;
use Biceps\OAuth\Client\OAuthClient;
use Biceps\OAuth\Client\RemoteUser;
use Biceps\OAuth\Entity\User;
use Biceps\OAuth\Security\Exception\NoTokenFoundException;
use Doctrine\ORM\EntityManagerInterface;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use KnpU\OAuth2ClientBundle\Exception\InvalidStateException;
use KnpU\OAuth2ClientBundle\Exception\MissingAuthorizationCodeException;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Security\Exception\IdentityProviderAuthenticationException;
use KnpU\OAuth2ClientBundle\Security\Exception\InvalidStateAuthenticationException;
use KnpU\OAuth2ClientBundle\Security\Exception\NoAuthCodeAuthenticationException;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class Authenticator extends SocialAuthenticator
{
const ROLE_OWNER = 'ROLE_BICEPS_OWNER';
const ROLE_PROFILE = 'ROLE_BICEPS_PROFILE';
const TOKEN_SESSION_KEY = 'BicepsOAuthSecurityAccessToken';
const VALIDITY_SESSION_KEY = 'BicepsOAuthSecurityValidity';
const URI_SESSION_KEY = 'BicepsOAuthSecurityURI';
const ACTION_SESSION_KEY = 'BicepsOAuthSecurityAction';
const ERROR_SESSION_KEY = 'BicepsOAuthSecurityError';
const TOKEN_VALIDITY = 5;
const ACTION_RESET = 'reset';
/** @var ClientRegistry */
protected $clientRegistry;
/** @var EntityManagerInterface */
protected $em;
/** @var RouterInterface */
protected $router;
/**
* Authenticator constructor.
*
* @param ClientRegistry $clientRegistry
* @param EntityManagerInterface $em
* @param RouterInterface $router
*/
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $em, RouterInterface $router)
{
$this->clientRegistry = $clientRegistry;
$this->em = $em;
$this->router = $router;
}
/**
* @inheritDoc
*/
public function start(Request $request, AuthenticationException $authException = null)
{
if (!$request->getSession()->get(self::URI_SESSION_KEY)) {
$request->getSession()->set(self::URI_SESSION_KEY, $request->getUri());
}
return new RedirectResponse($this->router->generate('oauth_biceps_connect'));
}
/**
* @inheritDoc
*/
public function supports(Request $request)
{
return $request->getPathInfo() === $this->router->generate('oauth_biceps_check') && $request->isMethod('GET');
}
/**
* @param Request $request
*/
public function reset(Request $request, $redirectUri)
{
$session = $request->getSession();
$this->clearSessionData($request, true);
$session->set(self::URI_SESSION_KEY, $redirectUri);
$session->set(self::ACTION_SESSION_KEY, self::ACTION_RESET);
return new RedirectResponse($this->router->generate('oauth_biceps_connect'));
}
/**
* @param Request $request
* @param $redirectUri
*/
public function logout(Request $request)
{
$session = $request->getSession();
$accessToken = $this->restoreAccessTokenFromSession($session);
$this->clearSessionData($request);
if ($accessToken) {
try {
$this->getClient()->getLogoutToken($accessToken);
} catch (Exception $e) {
}
}
}
/**
* @inheritDoc
*/
public function getCredentials(Request $request)
{
try {
$session = $request->getSession();
/** @var AccessToken $accessToken */
$accessToken = $this->fetchAccessToken($this->getClient(), [
'scope' => implode(' ', $this->getCurrentScopes($request)),
]);
switch ($session->get(self::ACTION_SESSION_KEY)) {
case self::ACTION_RESET:
$accessToken = $this->getClient()->getResetToken($accessToken);
break;
}
$session->set(self::TOKEN_SESSION_KEY, $accessToken->jsonSerialize());
$session->set(self::VALIDITY_SESSION_KEY, time() + self::TOKEN_VALIDITY);
return $accessToken;
} catch (\Exception $e) {
$this->handleException($e, $request);
} finally {
$request->getSession()->remove(self::ACTION_SESSION_KEY);
}
return false;
}
/**
* @param Request $request
* @return array
*/
public function getCurrentScopes(Request $request)
{
$action = $request->getSession()->get(self::ACTION_SESSION_KEY, null);
$additionals = [];
switch ($action) {
case self::ACTION_RESET:
$additionals = ['online_mode'];
}
return $this->getClient()->getOAuth2Provider()->getScopes($additionals);
}
/**
* @inheritDoc
*/
public function getUser($credentials, UserProviderInterface $userProvider)
{
/** @var RemoteUser $remoteUser */
$remoteUser = $this->getClient()->fetchUserFromToken($credentials);
// get className from userProvider
if (method_exists($userProvider, 'getClass')) {
$getUserClass = function () {
return $this->getClass();
};
$userClassName = $getUserClass->call($userProvider);
if (!($userClassName === User::class || is_subclass_of($userClassName, User::class))) {
throw new Exception(sprintf('"%s" do not extends "%s"', $userClassName, User::class), 1591553523);
}
} else {
throw new Exception(sprintf('"%s" must have method "getClass" to get user class name', get_class($userProvider)), 1591605229);
}
$username = $remoteUser->getUsername();
$user = $this->em->getRepository($userClassName)->findOneBy(['username' => $username]);
if (!$user) {
$user = new $userClassName();
$user->setUsername($username);
$roles = $this->getClient()->getOAuth2Provider()->getRoles([$remoteUser->isOwner() ? self::ROLE_OWNER : self::ROLE_PROFILE]);
foreach($roles as $role){
$user->addRole($role);
}
$this->em->persist($user);
$this->em->flush();
}
return $user;
}
/**
* @inheritDoc
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
// TODO: Implement onAuthenticationFailure() method.
}
/**
* @inheritDoc
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function refreshToken(Request $request)
{
$session = $request->getSession();
$accessToken = $this->restoreAccessTokenFromSession($session);
if ($accessToken && !$accessToken->hasExpired()) {
$validity = (int)$session->get(self::VALIDITY_SESSION_KEY, 0);
if (time() < $validity) {
return true;
}
$newAccessToken = $this->getClient()->getRefreshToken($accessToken);
if (!$newAccessToken->hasExpired()) {
$session->set(self::TOKEN_SESSION_KEY, $newAccessToken->jsonSerialize());
$session->set(self::VALIDITY_SESSION_KEY, time() + self::TOKEN_VALIDITY);
}
return true;
}
return false;
}
/**
* @return OAuthClient
*/
protected function getClient()
{
$client = $this->clientRegistry->getClient('biceps');
if (!($client instanceof OAuthClient)) {
$exception = new InvalidConfigurationException('Invalid client class! Client must implement OAuthClientInterface.', 1588239787);
$exception->setPath('knpu_oauth2_client.clients.biceps.client_class');
throw $exception;
}
return $client;
}
/**
* @param Exception $exception
*/
protected function handleException(Exception $exception, Request $request)
{
$session = $request->getSession();
if ($exception instanceof IdentityProviderAuthenticationException) {
/** @var IdentityProviderException $previous */
$exception = $exception->getPrevious();
}
if ($exception instanceof IdentityProviderException) {
$body = $exception->getResponseBody();
if (!is_array($body) || !isset($body['error'])) {
// If body do not contain an error code, user should contact an administrator
throw new IdentityProviderAuthenticationException($exception);
}
$session->set(self::ERROR_SESSION_KEY, [
'code' => $body['error'],
'description' => $body['error_description'] ?? '',
'licence_id' => $body['licence_id'] ?? '',
'redirectUri' => $session->get(self::URI_SESSION_KEY),
]);;
// Clear session data
$this->clearSessionData($request, false);
throw new LazyResponseException(new RedirectResponse($this->router->generate('oauth_biceps_exception')));
}
$this->clearSessionData($request, true);
throw $exception;
}
/**
* @param Session $session
*
* @return AccessToken|null
*/
protected function restoreAccessTokenFromSession(Session $session): ?AccessToken
{
if (!$session->has(self::TOKEN_SESSION_KEY)) {
return null;
}
$accessTokenData = $session->get(self::TOKEN_SESSION_KEY);
if (!is_array($accessTokenData)) {
$session->remove(self::TOKEN_SESSION_KEY);
return null;
}
$accessToken = new AccessToken($accessTokenData);
return !$accessToken->hasExpired() ? $accessToken : null;
}
/**
* @param Request $request
* @param bool $includeError
*/
protected function clearSessionData(Request $request, $includeError = true)
{
$session = $request->getSession();
$session->remove(self::VALIDITY_SESSION_KEY);
$session->remove(self::URI_SESSION_KEY);
$session->remove(self::ACTION_SESSION_KEY);
if ($includeError) {
$session->remove(self::ERROR_SESSION_KEY);
}
}
}