Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony custom authentication provider log out on request overlap

This problem has been reproduced with Symfony 3.3.17 and 3.4.9

I have a custom authenticaton provider that ties together a legacy application and a Symfony application:

app/config/security.yml

security:
    providers:
        zog:
            id: app.zog_user_provider


    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:

            anonymous: ~
            logout:
                path:   /logout
                target: /
            guard:
                authenticators:
                    - app.legacy_token_authenticator
                    - app.token_authenticator
                entry_point: app.legacy_token_authenticator

src/AppBundle/Security/LegacyTokenAuthenticator:

class LegacyTokenAuthenticator extends AbstractGuardAuthenticator
{
    private $session;

    private $router;

    public function __construct(
        RouterInterface $router,
        SessionInterface $session,
        $environment
    ) {
        if (session_status() != PHP_SESSION_ACTIVE) {
            if ($environment != 'test'){
                session_start();
            }
            $session->start();
            $this->setSession($session);
        }
        //if (!$session->isStarted()) {

        //}


        $this->router = $router;
    }


    /**
     * @return mixed
     */
    public function getSession()
    {
        return $this->session;
    }


    /**
     * @param mixed $session
     */
    public function setSession($session)
    {
        $this->session = $session;
    }


    /**
     * Called on every request. Return whatever credentials you want,
     * or null to stop authentication.
     */
    public function getCredentials(Request $request)
    {
        $session = $this->getSession();

        if (isset($_SESSION['ADMIN_logged_in']) && intval($_SESSION['ADMIN_logged_in'])){
            return $_SESSION['ADMIN_logged_in'];
        }
        return;
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        return $userProvider->loadUserByUserId($credentials);
    }

    public function checkCredentials($credentials, UserInterface $user)
    {
        return $user->getUsername() == $credentials;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        return null;
    }

    /**
     * Called when authentication is needed, but it's not sent
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $url = $this->router->generate('app_security_login');
        return new RedirectResponse($url);
    }

    public function supportsRememberMe()
    {
        return false;
    }
}

src/AppBundle/Security/TokenAuthenticator:

class TokenAuthenticator extends AbstractGuardAuthenticator
{

    /**
     * @var \Symfony\Component\Routing\RouterInterface
     */
    private $router;

    /**
     * Default message for authentication failure.
     *
     * @var string
     */
    private $failMessage = 'Invalid credentials';

    /**
     * @var UserPasswordEncoderInterface
     */
    private $passwordEncoder;

    /**
     * Creates a new instance of FormAuthenticator
     */
    public function __construct(
        RouterInterface $router,
        SessionInterface $session,
        $environment,
        UserPasswordEncoderInterface $passwordEncoder
    ) {
        $this->passwordEncoder = $passwordEncoder;
        $this->router = $router;

        if (session_status() != PHP_SESSION_ACTIVE) {
            if ($environment != 'test') {
                session_start();
            }
            $session->start();
        }

    }

    /**
     * {@inheritdoc}
     */
    public function getCredentials(Request $request)
    {
        if ($request->getPathInfo() != '/security/login' || !$request->isMethod('POST')) {
            return;
        }

        return ['username' => $request->request->get('username'), 'password' => $request->request->get('password')];
    }

    /**
     * {@inheritdoc}
     */
    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        try {
            return $userProvider->loadUserByUsername($credentials['username']);
        } catch (UsernameNotFoundException $e) {
            throw new CustomUserMessageAuthenticationException(
                $e->getMessage() != '' ?$e->getMessage():$this->failMessage
            );
        }
    }

    /**
     * {@inheritdoc}
     */
    public function checkCredentials($credentials, UserInterface $user)
    {
        if ($this->passwordEncoder->isPasswordValid($user, $credentials['password'])) {
            return true;
        }
        throw new CustomUserMessageAuthenticationException($this->failMessage);
    }

    /**
     * {@inheritdoc}
     */
    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $_SESSION['ADMIN_logged_in'] = $token->getUser()->getUsername();
        if ($_SESSION['legacy_page_requested'] ?? '/'){
            $url = $_SESSION['legacy_page_requested'] ?? '/';
        }else{
            $url = '/workflow_detailv2view.php';
        }
        unset($_SESSION['legacy_page_requested']);
        return new RedirectResponse($url);
    }

    /**
     * {@inheritdoc}
     */
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);

        $url = $this->router->generate('app_security_login');
        return new RedirectResponse($url);
    }

    /**
     * {@inheritdoc}
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        $url = $this->router->generate('app_security_login');
        return new RedirectResponse($url);
    }

    /**
     * {@inheritdoc}
     */
    public function supportsRememberMe()
    {
        return false;
    }
}

What I find is that this system works fine. However a new feature in a legacy page is running 2 Symfony async application requests which are overlapping.

In this case what happens is the 1st request shows 2 Session Cookie

Request URL: https://somedomain.com/system/staff_meeting/edit/1
Request Method: GET
Status Code: 200 OK
Remote Address: 222.154.225.22:443
Referrer Policy: no-referrer-when-downgrade
Cache-Control: max-age=0, must-revalidate, private
Cache-Control: no-store, no-cache, must-revalidate
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Date: Wed, 13 Jun 2018 21:57:19 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Keep-Alive: timeout=15, max=90
Server: Apache/2.4.6 (CentOS) mpm-itk/2.4.7-04 OpenSSL/1.0.2k-fips PHP/7.0.20
Set-Cookie: PHPSESSID=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/
Set-Cookie: PHPSESSID=tn7jhi5n2iu16le1os971vn024; path=/
Transfer-Encoding: chunked
X-Powered-By: PHP/7.0.20

and the second request is being logged out:

Request URL: https://somedomain.com/system/staff_meeting/edit/1
Request Method: GET
Status Code: 302 Found
Remote Address: 222.154.225.22:443
Referrer Policy: no-referrer-when-downgrade
Cache-Control: no-store, no-cache, must-revalidate
Cache-Control: max-age=0, must-revalidate, private
Connection: Keep-Alive
Content-Length: 332
Content-Type: text/html; charset=UTF-8
Date: Thu, 14 Jun 2018 01:26:43 GMT
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Keep-Alive: timeout=15, max=90
Location: /system/security/login
Server: Apache/2.4.6 (CentOS) mpm-itk/2.4.7-04 OpenSSL/1.0.2k-fips PHP/7.0.20
Set-Cookie: PHPSESSID=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/
X-Powered-By: PHP/7.0.20

I believe this must be some race condition in accessing the $_SESSION variable we use to hold together legacy system and Symfony application.

Any ideas how to resolve this issue?

like image 930
jdog Avatar asked Jun 14 '18 01:06

jdog


2 Answers

make the following changes to your authenticators to keep the session management to symfony:

src/AppBundle/Security/LegacyTokenAuthenticator:

public function __construct(RouterInterface $router) {
    $this->router = $router;
}

public function getUser($credentials, UserProviderInterface $userProvider)
{
    return $userProvider->loadUserByUsername($credentials);
}

public function getCredentials(Request $request)
{
    $session = $request->getSession();
    if ($session->has('ADMIN_logged_in') && intval($session->get('ADMIN_logged_in'))){
        return $session->get('ADMIN_logged_in');
    }
    return null;
}

src/AppBundle/Security/TokenAuthenticator:

public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
    $session = $request->getSession();
    $session->set('ADMIN_logged_in', $token->getUser()->getUsername());
    if ($session->has('legacy_page_requested')) {
        $url = $session->get('legacy_page_requested') ?? '/';
        $session->remove('legacy_page_requested');
    } else {
        $url = '/workflow_detailv2view.php';
    }
    return new RedirectResponse($url);
}
like image 146
Abdelhafid El Kadiri Avatar answered Nov 09 '22 23:11

Abdelhafid El Kadiri


I was able to circumvent the problem by setting

security.yml

security:
    session_fixation_strategy: none

I am not sure however how the regeneration of the session and corresponding renaming of Cookie value can be fixed.

I'm still keen to hear other thoughts on this.

like image 42
jdog Avatar answered Nov 09 '22 23:11

jdog