move TOTP check to deeper implemented login method, remove possible login vulnerability

This commit is contained in:
Daniel Seifert 2022-11-09 10:18:31 +01:00
parent 4e8bae08e7
commit 4691b7d0c0
Signed by: DanielS
GPG Key ID: 8A7C4C6ED1915C6F
2 changed files with 128 additions and 96 deletions

View File

@ -22,18 +22,18 @@ use OxidEsales\Eshop\Application\Model\User;
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Registry;
use OxidEsales\Eshop\Core\Session; use OxidEsales\Eshop\Core\Session;
use OxidEsales\Eshop\Core\Utils;
use OxidEsales\Eshop\Core\UtilsView; use OxidEsales\Eshop\Core\UtilsView;
class d3_totp_UserComponent extends d3_totp_UserComponent_parent class d3_totp_UserComponent extends d3_totp_UserComponent_parent
{ {
/** /**
* @return string|void * @return void
* @throws DBALException
* @throws DatabaseConnectionException * @throws DatabaseConnectionException
*/ */
public function login_noredirect() public function login()
{ {
parent::login_noredirect(); parent::login();
$oUser = $this->getUser(); $oUser = $this->getUser();
@ -42,21 +42,23 @@ class d3_totp_UserComponent extends d3_totp_UserComponent_parent
$totp->loadByUserId($oUser->getId()); $totp->loadByUserId($oUser->getId());
if ($totp->isActive() if ($totp->isActive()
&& !Registry::getSession()->getVariable(d3totp::TOTP_SESSION_VARNAME) && !$this->d3GetSession()->getVariable(d3totp::TOTP_SESSION_VARNAME)
) { ) {
Registry::getSession()->setVariable( $this->d3GetSession()->setVariable(
d3totp::TOTP_SESSION_CURRENTCLASS, d3totp::TOTP_SESSION_CURRENTCLASS,
$this->getParent()->getClassKey() != 'd3totplogin' ? $this->getParent()->getClassKey() : 'start' $this->getParent()->getClassKey() != 'd3totplogin' ? $this->getParent()->getClassKey() : 'start'
); );
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_CURRENTUSER, $oUser->getId());
Registry::getSession()->setVariable( $this->d3GetSession()->setVariable(d3totp::TOTP_SESSION_CURRENTUSER, $oUser->getId());
$this->d3GetSession()->setVariable(
d3totp::TOTP_SESSION_NAVFORMPARAMS, d3totp::TOTP_SESSION_NAVFORMPARAMS,
$this->getParent()->getViewConfig()->getNavFormParams() $this->getParent()->getViewConfig()->getNavFormParams()
); );
$oUser->logout(); $oUser->logout();
return "d3totplogin"; $sUrl = Registry::getConfig()->getShopHomeUrl() . 'cl=d3totplogin';
$this->d3GetUtils()->redirect($sUrl, false);
} }
} }
} }
@ -64,7 +66,7 @@ class d3_totp_UserComponent extends d3_totp_UserComponent_parent
/** /**
* @return d3totp * @return d3totp
*/ */
public function d3GetTotpObject() public function d3GetTotpObject(): d3totp
{ {
return oxNew(d3totp::class); return oxNew(d3totp::class);
} }
@ -86,7 +88,13 @@ class d3_totp_UserComponent extends d3_totp_UserComponent_parent
try { try {
if (!$this->isNoTotpOrNoLogin($totp) && $this->hasValidTotp($sTotp, $totp)) { if (!$this->isNoTotpOrNoLogin($totp) && $this->hasValidTotp($sTotp, $totp)) {
$this->d3TotpRelogin($oUser, $sTotp); // relogin, don't extract from this try block
$this->d3GetSession()->setVariable(d3totp::TOTP_SESSION_VARNAME, $sTotp);
$this->d3GetSession()->setVariable('usr', $oUser->getId());
$this->setUser(null);
$this->setLoginStatus(USER_LOGIN_SUCCESS);
$this->_afterLogin($oUser);
$this->d3TotpClearSessionVariables(); $this->d3TotpClearSessionVariables();
return false; return false;
@ -106,6 +114,14 @@ class d3_totp_UserComponent extends d3_totp_UserComponent_parent
return Registry::getUtilsView(); return Registry::getUtilsView();
} }
/**
* @return Utils
*/
public function d3GetUtils()
{
return Registry::getUtils();
}
public function cancelTotpLogin() public function cancelTotpLogin()
{ {
$this->d3TotpClearSessionVariables(); $this->d3TotpClearSessionVariables();
@ -138,19 +154,6 @@ class d3_totp_UserComponent extends d3_totp_UserComponent_parent
); );
} }
/**
* @param User $oUser
* @param $sTotp
*/
public function d3TotpRelogin(User $oUser, $sTotp)
{
$this->d3GetSession()->setVariable(d3totp::TOTP_SESSION_VARNAME, $sTotp);
$this->d3GetSession()->setVariable('usr', $oUser->getId());
$this->setUser(null);
$this->setLoginStatus(USER_LOGIN_SUCCESS);
$this->_afterLogin($oUser);
}
public function d3TotpClearSessionVariables() public function d3TotpClearSessionVariables()
{ {
$this->d3GetSession()->deleteVariable(d3totp::TOTP_SESSION_CURRENTCLASS); $this->d3GetSession()->deleteVariable(d3totp::TOTP_SESSION_CURRENTCLASS);

View File

@ -22,6 +22,7 @@ use OxidEsales\Eshop\Application\Model\User;
use OxidEsales\Eshop\Core\Controller\BaseController; use OxidEsales\Eshop\Core\Controller\BaseController;
use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Registry;
use OxidEsales\Eshop\Core\Session; use OxidEsales\Eshop\Core\Session;
use OxidEsales\Eshop\Core\Utils;
use OxidEsales\Eshop\Core\UtilsView; use OxidEsales\Eshop\Core\UtilsView;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use ReflectionException; use ReflectionException;
@ -53,17 +54,23 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
/** /**
* @test * @test
* @throws ReflectionException * @throws ReflectionException
* @covers \D3\Totp\Modules\Application\Component\d3_totp_UserComponent::login_noredirect * @covers \D3\Totp\Modules\Application\Component\d3_totp_UserComponent::login
*/ */
public function login_noredirectFailsIfNoUserLoggedIn() public function loginFailsIfNoUserLoggedIn()
{ {
$oUser = false; $oUser = false;
/** @var BaseController|MockObject $oParentMock */ /** @var Utils|MockObject $oUtilsMock */
$oParentMock = $this->getMockBuilder(BaseController::class) $oUtilsMock = $this->getMockBuilder(Utils::class)
->addMethods(['isEnabledPrivateSales']) ->onlyMethods(['redirect'])
->getMock();
$oUtilsMock->expects($this->never())->method('redirect')->willReturn(true);
/** @var Session|MockObject $oSessionMock */
$oSessionMock = $this->getMockBuilder(Session::class)
->onlyMethods(['setVariable'])
->getMock(); ->getMock();
$oParentMock->method('isEnabledPrivateSales')->willReturn(false); $oSessionMock->expects($this->never())->method('setVariable');
/** @var d3totp|MockObject $oTotpMock */ /** @var d3totp|MockObject $oTotpMock */
$oTotpMock = $this->getMockBuilder(d3totp::class) $oTotpMock = $this->getMockBuilder(d3totp::class)
@ -77,26 +84,29 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->onlyMethods([ ->onlyMethods([
'getUser', 'getUser',
'd3GetTotpObject', 'd3GetTotpObject',
'getParent', 'd3GetSession',
'd3GetUtils',
]) ])
->getMock(); ->getMock();
$oControllerMock->method('getUser')->willReturn($oUser); $oControllerMock->method('getUser')->willReturn($oUser);
$oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock); $oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock);
$oControllerMock->method('getParent')->willReturn($oParentMock); $oControllerMock->method('d3GetSession')->willReturn($oSessionMock);
$oControllerMock->method('d3GetUtils')->willReturn($oUtilsMock);
$_GET['lgn_usr'] = 'username'; $_GET['lgn_usr'] = 'username';
$this->_oController = $oControllerMock; $this->_oController = $oControllerMock;
$this->callMethod($this->_oController, 'login_noredirect'); $this->callMethod($this->_oController, 'login');
} }
/** /**
* @test * @test
* @throws ReflectionException * @throws ReflectionException
* @covers \D3\Totp\Modules\Application\Component\d3_totp_UserComponent::login_noredirect * @covers \D3\Totp\Modules\Application\Component\d3_totp_UserComponent::login
* @dataProvider loginFailTotpNotActiveOrAlreadyCheckedDataProvider
*/ */
public function login_noredirectFailTotpNotActive() public function loginFailTotpNotActiveOrAlreadyChecked($isActive, $checked)
{ {
/** @var User|MockObject $oUserMock */ /** @var User|MockObject $oUserMock */
$oUserMock = $this->getMockBuilder(User::class) $oUserMock = $this->getMockBuilder(User::class)
@ -108,11 +118,18 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
$oUserMock->expects($this->never())->method('logout')->willReturn(false); $oUserMock->expects($this->never())->method('logout')->willReturn(false);
$oUserMock->method('getId')->willReturn('foo'); $oUserMock->method('getId')->willReturn('foo');
/** @var BaseController|MockObject $oParentMock */ /** @var Utils|MockObject $oUtilsMock */
$oParentMock = $this->getMockBuilder(BaseController::class) $oUtilsMock = $this->getMockBuilder(Utils::class)
->addMethods(['isEnabledPrivateSales']) ->onlyMethods(['redirect'])
->getMock();
$oUtilsMock->expects($this->never())->method('redirect')->willReturn(true);
/** @var Session|MockObject $oSessionMock */
$oSessionMock = $this->getMockBuilder(Session::class)
->onlyMethods(['setVariable', 'getVariable'])
->getMock(); ->getMock();
$oParentMock->method('isEnabledPrivateSales')->willReturn(false); $oSessionMock->expects($this->never())->method('setVariable');
$oSessionMock->method('getVariable')->willReturn($checked);
/** @var d3totp|MockObject $oTotpMock */ /** @var d3totp|MockObject $oTotpMock */
$oTotpMock = $this->getMockBuilder(d3totp::class) $oTotpMock = $this->getMockBuilder(d3totp::class)
@ -122,7 +139,7 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
]) ])
->disableOriginalConstructor() ->disableOriginalConstructor()
->getMock(); ->getMock();
$oTotpMock->expects($this->once())->method('isActive')->willReturn(false); $oTotpMock->expects($this->once())->method('isActive')->willReturn($isActive);
$oTotpMock->method('loadByUserId')->willReturn(true); $oTotpMock->method('loadByUserId')->willReturn(true);
/** @var UserComponent|MockObject $oControllerMock */ /** @var UserComponent|MockObject $oControllerMock */
@ -130,26 +147,40 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->onlyMethods([ ->onlyMethods([
'getUser', 'getUser',
'd3GetTotpObject', 'd3GetTotpObject',
'getParent', 'd3GetSession',
'd3GetUtils',
]) ])
->getMock(); ->getMock();
$oControllerMock->method('getUser')->willReturn($oUserMock); $oControllerMock->method('getUser')->willReturn($oUserMock);
$oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock); $oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock);
$oControllerMock->method('getParent')->willReturn($oParentMock); $oControllerMock->method('d3GetSession')->willReturn($oSessionMock);
$oControllerMock->method('d3GetUtils')->willReturn($oUtilsMock);
$_GET['lgn_usr'] = 'username'; $_GET['lgn_usr'] = 'username';
$this->_oController = $oControllerMock; $this->_oController = $oControllerMock;
$this->callMethod($this->_oController, 'login_noredirect'); $this->callMethod($this->_oController, 'login');
}
/**
* @return array
*/
public function loginFailTotpNotActiveOrAlreadyCheckedDataProvider(): array
{
return [
'TOTP not active, not checked' => [false, null],
'TOTP not active, checked' => [false, true],
'TOTP active, checked' => [true, true],
];
} }
/** /**
* @test * @test
* @throws ReflectionException * @throws ReflectionException
* @covers \D3\Totp\Modules\Application\Component\d3_totp_UserComponent::login_noredirect * @covers \D3\Totp\Modules\Application\Component\d3_totp_UserComponent::login
*/ */
public function login_noredirectPass() public function loginPass()
{ {
/** @var User|MockObject $oUserMock */ /** @var User|MockObject $oUserMock */
$oUserMock = $this->getMockBuilder(User::class) $oUserMock = $this->getMockBuilder(User::class)
@ -161,11 +192,24 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
$oUserMock->expects($this->once())->method('logout')->willReturn(false); $oUserMock->expects($this->once())->method('logout')->willReturn(false);
$oUserMock->method('getId')->willReturn('foo'); $oUserMock->method('getId')->willReturn('foo');
/** @var Utils|MockObject $oUtilsMock */
$oUtilsMock = $this->getMockBuilder(Utils::class)
->onlyMethods(['redirect'])
->getMock();
$oUtilsMock->expects($this->once())->method('redirect')->willReturn(true);
/** @var Session|MockObject $oSessionMock */
$oSessionMock = $this->getMockBuilder(Session::class)
->onlyMethods(['setVariable', 'getVariable'])
->getMock();
$oSessionMock->expects($this->atLeast(3))->method('setVariable');
$oSessionMock->method('getVariable')->willReturn(null);
/** @var BaseController|MockObject $oParentMock */ /** @var BaseController|MockObject $oParentMock */
$oParentMock = $this->getMockBuilder(BaseController::class) $oParentMock = $this->getMockBuilder(BaseController::class)
->addMethods(['isEnabledPrivateSales']) ->onlyMethods(['getClassKey'])
->getMock(); ->getMock();
$oParentMock->method('isEnabledPrivateSales')->willReturn(false); $oParentMock->method('getClassKey')->willReturn('foo');
/** @var d3totp|MockObject $oTotpMock */ /** @var d3totp|MockObject $oTotpMock */
$oTotpMock = $this->getMockBuilder(d3totp::class) $oTotpMock = $this->getMockBuilder(d3totp::class)
@ -183,21 +227,22 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->onlyMethods([ ->onlyMethods([
'getUser', 'getUser',
'd3GetTotpObject', 'd3GetTotpObject',
'getParent', 'd3GetSession',
'd3GetUtils',
'getParent'
]) ])
->getMock(); ->getMock();
$oControllerMock->method('getUser')->willReturn($oUserMock); $oControllerMock->method('getUser')->willReturn($oUserMock);
$oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock); $oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock);
$oControllerMock->method('getParent')->willReturn($oParentMock); $oControllerMock->method('getParent')->willReturn($oParentMock);
$oControllerMock->method('d3GetSession')->willReturn($oSessionMock);
$oControllerMock->method('d3GetUtils')->willReturn($oUtilsMock);
$_GET['lgn_usr'] = 'username'; $_GET['lgn_usr'] = 'username';
$this->_oController = $oControllerMock; $this->_oController = $oControllerMock;
$this->assertSame( $this->callMethod($this->_oController, 'login');
'd3totplogin',
$this->callMethod($this->_oController, 'login_noredirect')
);
} }
/** /**
@ -227,19 +272,25 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->getMock(); ->getMock();
$oTotpMock->method('loadByUserId')->willReturn(true); $oTotpMock->method('loadByUserId')->willReturn(true);
/** @var Session|MockObject $oSessionMock */
$oSessionMock = $this->getMockBuilder(Session::class)
->onlyMethods(['setVariable'])
->getMock();
$oSessionMock->expects($this->never())->method('setVariable');
/** @var UserComponent|MockObject $oControllerMock */ /** @var UserComponent|MockObject $oControllerMock */
$oControllerMock = $this->getMockBuilder(UserComponent::class) $oControllerMock = $this->getMockBuilder(UserComponent::class)
->onlyMethods([ ->onlyMethods([
'isNoTotpOrNoLogin', 'isNoTotpOrNoLogin',
'hasValidTotp', 'hasValidTotp',
'd3TotpRelogin',
'd3GetTotpObject', 'd3GetTotpObject',
'd3GetSession',
]) ])
->getMock(); ->getMock();
$oControllerMock->method('isNoTotpOrNoLogin')->willReturn(true); $oControllerMock->method('isNoTotpOrNoLogin')->willReturn(true);
$oControllerMock->expects($this->never())->method('hasValidTotp')->willReturn(false); $oControllerMock->expects($this->never())->method('hasValidTotp')->willReturn(false);
$oControllerMock->expects($this->never())->method('d3TotpRelogin')->willReturn(false);
$oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock); $oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock);
$oControllerMock->method('d3GetSession')->willReturn($oSessionMock);
$this->_oController = $oControllerMock; $this->_oController = $oControllerMock;
@ -263,6 +314,12 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->getMock(); ->getMock();
$oTotpMock->method('loadByUserId')->willReturn(true); $oTotpMock->method('loadByUserId')->willReturn(true);
/** @var Session|MockObject $oSessionMock */
$oSessionMock = $this->getMockBuilder(Session::class)
->onlyMethods(['setVariable'])
->getMock();
$oSessionMock->expects($this->never())->method('setVariable');
/** @var d3totp_wrongOtpException|MockObject $oUtilsViewMock */ /** @var d3totp_wrongOtpException|MockObject $oUtilsViewMock */
$oTotpExceptionMock = $this->getMockBuilder(d3totp_wrongOtpException::class) $oTotpExceptionMock = $this->getMockBuilder(d3totp_wrongOtpException::class)
->disableOriginalConstructor() ->disableOriginalConstructor()
@ -279,16 +336,16 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->onlyMethods([ ->onlyMethods([
'isNoTotpOrNoLogin', 'isNoTotpOrNoLogin',
'hasValidTotp', 'hasValidTotp',
'd3TotpRelogin',
'd3GetUtilsView', 'd3GetUtilsView',
'd3GetTotpObject', 'd3GetTotpObject',
'd3GetSession',
]) ])
->getMock(); ->getMock();
$oControllerMock->method('isNoTotpOrNoLogin')->willReturn(false); $oControllerMock->method('isNoTotpOrNoLogin')->willReturn(false);
$oControllerMock->expects($this->once())->method('hasValidTotp')->willThrowException($oTotpExceptionMock); $oControllerMock->expects($this->once())->method('hasValidTotp')->willThrowException($oTotpExceptionMock);
$oControllerMock->expects($this->never())->method('d3TotpRelogin')->willReturn(false);
$oControllerMock->method('d3GetUtilsView')->willReturn($oUtilsViewMock); $oControllerMock->method('d3GetUtilsView')->willReturn($oUtilsViewMock);
$oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock); $oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock);
$oControllerMock->method('d3GetSession')->willReturn($oSessionMock);
$this->_oController = $oControllerMock; $this->_oController = $oControllerMock;
@ -312,6 +369,12 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->getMock(); ->getMock();
$oTotpMock->method('loadByUserId')->willReturn(true); $oTotpMock->method('loadByUserId')->willReturn(true);
/** @var Session|MockObject $oSessionMock */
$oSessionMock = $this->getMockBuilder(Session::class)
->onlyMethods(['setVariable'])
->getMock();
$oSessionMock->expects($this->atLeast(2))->method('setVariable');
/** @var UtilsView|MockObject $oUtilsViewMock */ /** @var UtilsView|MockObject $oUtilsViewMock */
$oUtilsViewMock = $this->getMockBuilder(UtilsView::class) $oUtilsViewMock = $this->getMockBuilder(UtilsView::class)
->onlyMethods(['addErrorToDisplay']) ->onlyMethods(['addErrorToDisplay'])
@ -323,16 +386,20 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
->onlyMethods([ ->onlyMethods([
'isNoTotpOrNoLogin', 'isNoTotpOrNoLogin',
'hasValidTotp', 'hasValidTotp',
'd3TotpRelogin',
'd3GetUtilsView', 'd3GetUtilsView',
'd3GetTotpObject', 'd3GetTotpObject',
'd3GetSession',
'setLoginStatus'
]) ])
->getMock(); ->getMock();
$oControllerMock->method('isNoTotpOrNoLogin')->willReturn(false); $oControllerMock->method('isNoTotpOrNoLogin')->willReturn(false);
$oControllerMock->expects($this->once())->method('hasValidTotp')->willReturn(true); $oControllerMock->expects($this->once())->method('hasValidTotp')->willReturn(true);
$oControllerMock->expects($this->once())->method('d3TotpRelogin')->willReturn(true);
$oControllerMock->method('d3GetUtilsView')->willReturn($oUtilsViewMock); $oControllerMock->method('d3GetUtilsView')->willReturn($oUtilsViewMock);
$oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock); $oControllerMock->method('d3GetTotpObject')->willReturn($oTotpMock);
$oControllerMock->method('d3GetSession')->willReturn($oSessionMock);
$oControllerMock->expects($this->once())->method('setLoginStatus')->with(
$this->identicalTo(USER_LOGIN_SUCCESS)
);
$this->_oController = $oControllerMock; $this->_oController = $oControllerMock;
@ -519,44 +586,6 @@ class d3_totp_UserComponentTest extends d3TotpUnitTestCase
); );
} }
/**
* @test
* @throws ReflectionException
* @covers \D3\Totp\Modules\Application\Component\d3_totp_UserComponent::d3TotpRelogin
*/
public function d3TotpReloginPass()
{
/** @var Session|MockObject $oSessionMock */
$oSessionMock = $this->getMockBuilder(Session::class)
->onlyMethods(['setVariable'])
->getMock();
$oSessionMock->expects($this->atLeast(2))->method('setVariable')->willReturn(false);
/** @var User|MockObject $oUserMock */
$oUserMock = $this->getMockBuilder(User::class)
->onlyMethods(['getId'])
->getMock();
$oUserMock->method('getId')->willReturn('foo');
/** @var UserComponent|MockObject $oControllerMock */
$oControllerMock = $this->getMockBuilder(UserComponent::class)
->onlyMethods([
'd3GetSession',
'setUser',
'setLoginStatus',
'_afterLogin',
])
->getMock();
$oControllerMock->method('d3GetSession')->willReturn($oSessionMock);
$oControllerMock->expects($this->once())->method('setUser')->willReturn(false);
$oControllerMock->expects($this->once())->method('setLoginStatus')->willReturn(false);
$oControllerMock->expects($this->once())->method('_afterLogin')->willReturn(false);
$this->_oController = $oControllerMock;
$this->callMethod($this->_oController, 'd3TotpRelogin', [$oUserMock, '123456']);
}
/** /**
* @test * @test
* @throws ReflectionException * @throws ReflectionException