diff --git a/plugins/PassboltCe/MultiFactorAuthentication/config/routes.php b/plugins/PassboltCe/MultiFactorAuthentication/config/routes.php index 07fa297395..82783d91e9 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/config/routes.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/config/routes.php @@ -103,6 +103,11 @@ ]) ->setMethods(['POST']); + $routes->connect('/verify/duo/callback', [ + 'prefix' => 'Duo', 'controller' => 'DuoVerifyCallbackGet', 'action' => 'get', + ]) + ->setMethods(['GET']); + $routes->connect('/verify/error', ['controller' => 'MfaVerifyAjaxError', 'action' => 'get']) ->setMethods(['GET', 'POST', 'PUT', 'DELETE']); diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Controller/Duo/DuoVerifyCallbackGetController.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Controller/Duo/DuoVerifyCallbackGetController.php new file mode 100644 index 0000000000..f101fc0981 --- /dev/null +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Controller/Duo/DuoVerifyCallbackGetController.php @@ -0,0 +1,164 @@ +_assertRequestNotJson(); + $this->_handleVerifiedNotRequired($sessionIdentificationService); + $redirect = $this->_handleInvalidSettings(MfaSettings::PROVIDER_DUO); + if ($redirect) { + return $redirect; + } + + $uac = $this->User->getAccessControl(); + $mfaDuoCallbackDto = $this->getAndAssertMfaDuoCallbackData(); + $cookieToken = $this->consumeAndAssertCookieToken(); + + $authenticationToken = (new MfaDuoLoginService($duoSdkClient))->login( + $uac, + $mfaDuoCallbackDto, + $cookieToken + ); + $this->addMfaVerifiedCookieToResponse($uac, $sessionIdentificationService); + + $this->disableAutoRender(); + $this->redirectIfDefinedInToken($authenticationToken); + } + + /** + * Get the Mfa Duo Callback data from the query and assert them. + * + * @throws \App\Error\Exception\FormValidationException If the data provided on the query does not validate + * @throws \Cake\Http\Exception\BadRequestException If Duo was not able to authenticate the user and provided error details + * @return \Passbolt\MultiFactorAuthentication\Model\Dto\MfaDuoCallbackDto + */ + private function getAndAssertMfaDuoCallbackData(): MfaDuoCallbackDto + { + $mfaDuoCallbackData = $this->getRequest()->getQueryParams(); + $mfaDuoCallbackForm = new DuoCallbackForm(); + $isValid = $mfaDuoCallbackForm->execute($mfaDuoCallbackData); + $mfaDuoCallbackDto = new MfaDuoCallbackDto($mfaDuoCallbackForm->getData()); + + if ($mfaDuoCallbackDto->hasError()) { + $msg = __('Unable to authenticate to Duo.'); + $msg .= " {$mfaDuoCallbackDto->formatError()}"; + throw new BadRequestException($msg); + } + + if (!$isValid) { + $msg = __('Unable to validate the Duo callback data.'); + throw new FormValidationException($msg, $mfaDuoCallbackForm); + } + + return $mfaDuoCallbackDto; + } + + /** + * Consume the duo state cookie containing the user authentication token id and assert the format this one. + * + * @return string The token id stored in the cookie + * @throws \Cake\Http\Exception\BadRequestException if the cookie is not defined + * @throws \Cake\Http\Exception\BadRequestException if the cookie value is not a string + * @throws \Cake\Http\Exception\BadRequestException if the cookie value is not a valid uuid + */ + private function consumeAndAssertCookieToken(): string + { + $cookieToken = (new MfaDuoStateCookieService())->readDuoStateCookieValue($this->getRequest()); + if (is_null($cookieToken)) { + throw new BadRequestException(__('A Duo state cookie is required.')); + } + $cookieToExpire = new Cookie(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE); + $this->setResponse($this->getResponse()->withExpiredCookie($cookieToExpire)); + + if (!is_string($cookieToken)) { + throw new BadRequestException(__('The Duo state cookie value should be a string.')); + } elseif (!Validation::uuid($cookieToken)) { + throw new BadRequestException(__('The Duo state cookie should be a valid UUID.')); + } + + return $cookieToken; + } + + /** + * Add to the response the MFA verified cookie. + * + * @param \App\Utility\UserAccessControl $uac User access control + * @param \App\Authenticator\SessionIdentificationServiceInterface $sessionIdentificationService session ID service + * @return void + * @throws \Cake\Http\Exception\InternalErrorException if it cannot create MFA cookie + */ + private function addMfaVerifiedCookieToResponse( + UserAccessControl $uac, + SessionIdentificationServiceInterface $sessionIdentificationService + ): void { + try { + $cookie = (new MfaVerifiedCookieService())->createDuoMfaVerifiedCookie( + $uac, + $sessionIdentificationService, + $this->getRequest() + ); + } catch (\Throwable $e) { + throw new InternalErrorException('Could not create MFA verified cookie.', null, $e); + } + + $this->setResponse($this->getResponse()->withCookie($cookie)); + } + + /** + * Redirect the user if the authentication token contains a redirect path. + * + * @param \App\Model\Entity\AuthenticationToken $authenticationToken The authentication token + * @return void + */ + private function redirectIfDefinedInToken(AuthenticationToken $authenticationToken): void + { + $redirect = $authenticationToken->getDataValue('redirect'); + if (!empty($redirect)) { + $this->redirect($redirect); + } + } +} diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Controller/MfaVerifyController.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Controller/MfaVerifyController.php index 86213a5616..395dd77e5c 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Controller/MfaVerifyController.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Controller/MfaVerifyController.php @@ -32,6 +32,7 @@ abstract class MfaVerifyController extends MfaController { /** * @return void + * @throws \Exception if loadComponent failed */ public function initialize(): void { diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Middleware/InjectMfaFormMiddleware.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Middleware/InjectMfaFormMiddleware.php index d3fb9965a6..1d3501738d 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Middleware/InjectMfaFormMiddleware.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Middleware/InjectMfaFormMiddleware.php @@ -105,6 +105,7 @@ public function services( $concrete = DuoSetupForm::class; break; case 'setup/duo/callback': + case 'verify/duo/callback': $concrete = DuoCallbackForm::class; break; case 'verify/duo': diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php index 8711c45ead..64138252a1 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoCallbackAuthenticationTokenService.php @@ -56,12 +56,12 @@ public function consumeAndVerifyAuthenticationToken( string $duoState ): AuthenticationToken { if (!Validation::uuid($token)) { - throw new \InvalidArgumentException(__('The authentication token should be a valid UUID.')); + throw new \InvalidArgumentException('The authentication token should be a valid UUID.'); } if (!Validation::inList($tokenType, MfaDuoCallbackAuthenticationTokenService::$ALLOWED_TOKEN_TYPES)) { $readableAllowedTokenTypes = implode(', ', MfaDuoCallbackAuthenticationTokenService::$ALLOWED_TOKEN_TYPES); - $msg = __('The authentication token type should be one of the following: {0}.', $readableAllowedTokenTypes); - throw new \InvalidArgumentException($msg); + $msg = 'The authentication token type should be one of the following: ' . $readableAllowedTokenTypes . '.'; + throw new \InvalidArgumentException($msg); } $authToken = $this->consumeAuthenticationTokenOrFail($uac, $tokenType, $token); diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoEnableService.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoEnableService.php index 019c911050..6b870a6c41 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoEnableService.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoEnableService.php @@ -49,7 +49,8 @@ public function __construct(?Client $client = null) { try { $this->duoClient = $client ?? (new MfaDuoGetSdkClientService())->getOrFail( - new MfaOrgSettingsDuoService(MfaOrgSettings::get()->getSettings()) + new MfaOrgSettingsDuoService(MfaOrgSettings::get()->getSettings()), + AuthenticationToken::TYPE_MFA_SETUP ); } catch (\Throwable $th) { $msg = __('Could not enable Duo MFA provider.'); @@ -76,16 +77,18 @@ public function enable( string $token ): AuthenticationToken { if (!Validation::uuid($token)) { - throw new \InvalidArgumentException(__('The authentication token should be a valid UUID.')); + throw new \InvalidArgumentException('The authentication token should be a valid UUID.'); } + $authenticationTokenType = AuthenticationToken::TYPE_MFA_SETUP; $authenticationToken = (new MfaDuoCallbackAuthenticationTokenService()) ->consumeAndVerifyAuthenticationToken( $uac, - AuthenticationToken::TYPE_MFA_SETUP, + $authenticationTokenType, $token, $duoCallbackDto->state ); - (new MfaDuoVerifyDuoCodeService($this->duoClient))->verify($uac, $duoCallbackDto->duoCode); + (new MfaDuoVerifyDuoCodeService($authenticationTokenType, $this->duoClient)) + ->verify($uac, $duoCallbackDto->duoCode); $this->enableProvider($uac); return $authenticationToken; diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoGetSdkClientService.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoGetSdkClientService.php index 4e304c323f..e26fa4ac9d 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoGetSdkClientService.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoGetSdkClientService.php @@ -17,8 +17,10 @@ namespace Passbolt\MultiFactorAuthentication\Service\Duo; +use App\Model\Entity\AuthenticationToken; use Cake\Http\Exception\InternalErrorException; use Cake\Routing\Router; +use Cake\Validation\Validation; use Duo\DuoUniversal\Client; use Duo\DuoUniversal\DuoException; use Passbolt\MultiFactorAuthentication\Service\MfaOrgSettings\MfaOrgSettingsDuoService; @@ -32,21 +34,41 @@ class MfaDuoGetSdkClientService * Get the Duo Sdk Client object or fail. * * @param \Passbolt\MultiFactorAuthentication\Service\MfaOrgSettings\MfaOrgSettingsDuoService $settings Duo org settings + * @param string $tokenType Authentication token type -- used to know whether the callback is for the setup or verify flow * @return \Duo\DuoUniversal\Client * @throws \Cake\Http\Exception\InternalErrorException If it cannot instantiate the Duo Sdk client. */ - public function getOrFail(MfaOrgSettingsDuoService $settings): Client + public function getOrFail(MfaOrgSettingsDuoService $settings, string $tokenType): Client { try { return new Client( $settings->getDuoClientId(), $settings->getDuoClientSecret(), $settings->getDuoApiHostname(), - Router::url('/mfa/setup/duo/callback', true), + $this->getCallbackRedirectUrl($tokenType), true, ); } catch (DuoException $e) { throw new InternalErrorException(__('Could not validate the Duo settings.'), null, $e); } } + + /** + * Get the callback redirect URL to redirect the user from Duo back to Passbolt + * + * @param string $tokenType Authentication token type, which determines which endpoint to redirect users to + * @return string + */ + public function getCallbackRedirectUrl(string $tokenType): string + { + if (!Validation::inList($tokenType, MfaDuoCallbackAuthenticationTokenService::$ALLOWED_TOKEN_TYPES)) { + $readableAllowedTokenTypes = implode(', ', MfaDuoCallbackAuthenticationTokenService::$ALLOWED_TOKEN_TYPES); + $msg = 'The authentication token type should be one of the following: ' . $readableAllowedTokenTypes . '.'; + throw new \InvalidArgumentException($msg); + } + $path = $tokenType === AuthenticationToken::TYPE_MFA_SETUP ? 'setup' : 'verify'; + $url = '/mfa/' . $path . '/duo/callback'; + + return Router::url($url, true); + } } diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoLoginService.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoLoginService.php new file mode 100644 index 0000000000..092ca73dd2 --- /dev/null +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoLoginService.php @@ -0,0 +1,92 @@ +duoClient = $client ?? (new MfaDuoGetSdkClientService())->getOrFail( + new MfaOrgSettingsDuoService(MfaOrgSettings::get()->getSettings()), + AuthenticationToken::TYPE_MFA_VERIFY + ); + } catch (\Throwable $th) { + $msg = __('Could not login using Duo MFA provider.'); + throw new InternalErrorException($msg, null, $th); + } + } + + /** + * Login using Duo for the operator. + * + * @param \App\Utility\UserAccessControl $uac The user access control + * @param \Passbolt\MultiFactorAuthentication\Model\Dto\MfaDuoCallbackDto $duoCallbackDto The Duo callback data + * @param string $token The authentication token. + * @return \App\Model\Entity\AuthenticationToken + * @throws \InvalidArgumentException if the provided token is not a UUID + * @throws \Cake\Http\Exception\UnauthorizedException If no active Duo callback authentication can be found. + * @throws \Cake\Http\Exception\UnauthorizedException If the duo state cannot be verified. + * @throws \Cake\Http\Exception\UnauthorizedException If the Duo code cannot be verified. + */ + public function login( + UserAccessControl $uac, + MfaDuoCallbackDto $duoCallbackDto, + string $token + ): AuthenticationToken { + if (!Validation::uuid($token)) { + throw new \InvalidArgumentException('The authentication token should be a valid UUID.'); + } + $authenticationTokenType = AuthenticationToken::TYPE_MFA_VERIFY; + $authenticationToken = (new MfaDuoCallbackAuthenticationTokenService()) + ->consumeAndVerifyAuthenticationToken( + $uac, + $authenticationTokenType, + $token, + $duoCallbackDto->state + ); + (new MfaDuoVerifyDuoCodeService($authenticationTokenType, $this->duoClient)) + ->verify($uac, $duoCallbackDto->duoCode); + + return $authenticationToken; + } +} diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoStartDuoAuthenticationService.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoStartDuoAuthenticationService.php index 674bc10581..6427434998 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoStartDuoAuthenticationService.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoStartDuoAuthenticationService.php @@ -51,7 +51,8 @@ public function __construct(?Client $client = null) { try { $this->duoClient = $client ?? (new MfaDuoGetSdkClientService())->getOrFail( - new MfaOrgSettingsDuoService(MfaOrgSettings::get()->getSettings()) + new MfaOrgSettingsDuoService(MfaOrgSettings::get()->getSettings()), + AuthenticationToken::TYPE_MFA_SETUP ); } catch (\Throwable $th) { throw new InternalErrorException(__('Could not enable Duo MFA provider.'), null, $th); @@ -120,7 +121,7 @@ private function assertDuoCallbackAuthenticationTokenType(string $authentication ); if (!$isValid) { $readableAllowedTokenTypes = implode(', ', MfaDuoCallbackAuthenticationTokenService::$ALLOWED_TOKEN_TYPES); - $msg = __('The authentication token type should be one of the following: {0}.', $readableAllowedTokenTypes); + $msg = 'The authentication token type should be one of the following: ' . $readableAllowedTokenTypes . '.'; throw new \InvalidArgumentException($msg); } } diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoVerifyDuoCodeService.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoVerifyDuoCodeService.php index 25cb87cd01..8ff13c2c97 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoVerifyDuoCodeService.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoVerifyDuoCodeService.php @@ -39,14 +39,16 @@ class MfaDuoVerifyDuoCodeService /** * MfaDuoVerifyService constructor. * + * @param string $authTokenType The authentication token type, which determines which flow this is for * @param \Duo\DuoUniversal\Client|null $client Duo SDK Client * @throws \Cake\Http\Exception\InternalErrorException If it cannot create the Duo Sdk Client */ - public function __construct(?Client $client = null) + public function __construct(string $authTokenType, ?Client $client = null) { try { $this->duoClient = $client ?? (new MfaDuoGetSdkClientService())->getOrFail( - new MfaOrgSettingsDuoService(MfaOrgSettings::get()->getSettings()) + new MfaOrgSettingsDuoService(MfaOrgSettings::get()->getSettings()), + $authTokenType ); } catch (\Throwable $th) { throw new InternalErrorException(__('Could not enable Duo MFA provider.'), null, $th); diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/MfaOrgSettings/MfaOrgSettingsDuoService.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/MfaOrgSettings/MfaOrgSettingsDuoService.php index ae554c5185..b6719a90c8 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Service/MfaOrgSettings/MfaOrgSettingsDuoService.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Service/MfaOrgSettings/MfaOrgSettingsDuoService.php @@ -17,6 +17,7 @@ namespace Passbolt\MultiFactorAuthentication\Service\MfaOrgSettings; use App\Error\Exception\CustomValidationException; +use App\Model\Entity\AuthenticationToken; use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Http\Exception\InternalErrorException; use Cake\Validation\Validation; @@ -123,7 +124,10 @@ public function validateDuoSettings(?Client $client = null): void if (empty($errors[MfaSettings::PROVIDER_DUO])) { try { - $duoClient = $client ?? (new MfaDuoGetSdkClientService())->getOrFail($this); + $duoClient = $client ?? (new MfaDuoGetSdkClientService())->getOrFail( + $this, + AuthenticationToken::TYPE_MFA_SETUP + ); $duoClient->healthCheck(); } catch (DuoException | InternalErrorException $e) { $msg = __('Cannot verify Duo settings.') . ' ' . $e->getMessage(); diff --git a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Controllers/Duo/DuoVerifyCallbackGetControllerTest.php b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Controllers/Duo/DuoVerifyCallbackGetControllerTest.php new file mode 100644 index 0000000000..c5d128ce7a --- /dev/null +++ b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Controllers/Duo/DuoVerifyCallbackGetControllerTest.php @@ -0,0 +1,200 @@ +get('/mfa/verify/duo/callback'); + $this->assertRedirect(); + $this->assertRedirectContains('/auth/login?redirect=%2Fmfa%2Fverify%2Fduo%2Fcallback'); + } + + public function testDuoVerifyCallbackGetController_Error_JsonNotAllowed() + { + $user = $this->logInAsUser(); + $this->mockMfaCookieValid($this->makeUac($user), MfaSettings::PROVIDER_DUO); + $this->get('/mfa/verify/duo/callback.json'); + $this->assertResponseError('You need to login to access this location.'); + } + + public function testDuoVerifyCallbackGetController_Error_AlreadyVerified() + { + $user = $this->logInAsUser(); + $this->loadFixtureScenario(MfaDuoScenario::class, $user); + $this->mockMfaCookieValid($this->makeUac($user), MfaSettings::PROVIDER_DUO); + $this->get('/mfa/verify/duo/callback'); + $this->assertResponseError('The multi-factor authentication is not required.'); + $this->assertSame(1, OrganizationSettingFactory::count()); + } + + public function testDuoVerifyCallbackGetController_Error_InvalidOrgSettings() + { + $this->logInAsUser(); + $this->get('/mfa/verify/duo/callback'); + $this->assertRedirect(); + $this->assertRedirectContains('/'); + } + + public function testDuoVerifyCallbackGetController_Error_DuoStateCookieMissing() + { + $user = $this->logInAsUser(); + $this->loadFixtureScenario(MfaDuoScenario::class, $user); + $duoState = UuidFactory::uuid(); + $userId = $user->get('id'); + $this->mockService(Client::class, function () use ($user) { + return DuoSdkClientMock::createDefault($this, $user)->getClient(); + }); + + AuthenticationTokenFactory::make()->active()->data([ + 'provider' => 'duo', + 'state' => $duoState, + 'redirect' => '', + 'user_agent' => 'PassboltUA', + ])->userId($userId)->type(AuthenticationToken::TYPE_MFA_VERIFY)->persist(); + + $this->get('/mfa/verify/duo/callback?state=' . $duoState . '&duo_code=' . UuidFactory::uuid()); + $this->assertResponseCode(400); + $this->assertResponseContains('A Duo state cookie is required.'); + + $this->assertCookieNotSet(MfaVerifiedCookie::MFA_COOKIE_ALIAS); + $this->assertCookieNotSet(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE); + } + + public function testDuoVerifyCallbackGetController_Error_UnableToAuthenticateToDuo() + { + $user = $this->logInAsUser(); + $this->loadFixtureScenario(MfaDuoScenario::class, $user); + $duoState = UuidFactory::uuid(); + $userId = $user->get('id'); + $this->mockService(Client::class, function () use ($user) { + return DuoSdkClientMock::createDefault($this, $user)->getClient(); + }); + + $authToken = AuthenticationTokenFactory::make()->active()->data([ + 'provider' => 'duo', + 'state' => $duoState, + 'redirect' => '', + 'user_agent' => 'PassboltUA', + ])->userId($userId)->type(AuthenticationToken::TYPE_MFA_SETUP)->persist(); + $token = $authToken->token; + + $this->cookie(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE, $token); + + $this->get('/mfa/verify/duo/callback?error=DuoCallbackError&DuoCallbackErrorDescription'); + $this->assertResponseCode(400); + $this->assertResponseContains('Unable to authenticate to Duo.'); + + $this->assertCookieNotSet(MfaVerifiedCookie::MFA_COOKIE_ALIAS); + $this->assertCookieNotSet(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE); + } + + public function testDuoVerifyCallbackGetController_Error_CouldNotValidateDuoCallbackData() + { + $user = $this->logInAsUser(); + $this->loadFixtureScenario(MfaDuoScenario::class, $user); + $duoState = UuidFactory::uuid(); + $userId = $user->get('id'); + $this->mockService(Client::class, function () use ($user) { + return DuoSdkClientMock::createDefault($this, $user)->getClient(); + }); + + $authToken = AuthenticationTokenFactory::make()->active()->data([ + 'provider' => 'duo', + 'state' => $duoState, + 'redirect' => '', + 'user_agent' => 'PassboltUA', + ])->userId($userId)->type(AuthenticationToken::TYPE_MFA_VERIFY)->persist(); + $token = $authToken->token; + + $this->cookie(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE, $token); + + $this->get('/mfa/verify/duo/callback'); + $this->assertResponseCode(400); + $this->assertResponseContains('Unable to validate the Duo callback data.'); + + $this->assertCookieNotSet(MfaVerifiedCookie::MFA_COOKIE_ALIAS); + $this->assertCookieNotSet(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE); + } + + public function testDuoVerifyCallbackGetController_Success() + { + $user = $this->logInAsUser(); + $this->loadFixtureScenario(MfaDuoScenario::class, $user); + $duoState = UuidFactory::uuid(); + $userId = $user->get('id'); + $this->mockService(Client::class, function () use ($user) { + return DuoSdkClientMock::createDefault($this, $user)->getClient(); + }); + + $authToken = AuthenticationTokenFactory::make()->active()->data([ + 'provider' => 'duo', + 'state' => $duoState, + 'redirect' => '', + 'user_agent' => 'PassboltUA', + ])->userId($userId)->type(AuthenticationToken::TYPE_MFA_VERIFY)->persist(); + $token = $authToken->token; + + $this->cookie(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE, $token); + + $this->get('/mfa/verify/duo/callback?state=' . $duoState . '&duo_code=' . UuidFactory::uuid()); + $this->assertResponseOk(); + + $this->assertCookieSet(MfaVerifiedCookie::MFA_COOKIE_ALIAS); + $this->assertCookieNotSet(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE); + } + + public function testDuoVerifyCallbackGetController_Success_Redirect() + { + $user = $this->logInAsUser(); + $this->loadFixtureScenario(MfaDuoScenario::class, $user); + $redirectPath = '/app/settings/mfa'; + $duoState = UuidFactory::uuid(); + $userId = $user->get('id'); + $this->mockService(Client::class, function () use ($user) { + return DuoSdkClientMock::createDefault($this, $user)->getClient(); + }); + + $authToken = AuthenticationTokenFactory::make()->active()->data([ + 'provider' => 'duo', + 'state' => $duoState, + 'redirect' => $redirectPath, + 'user_agent' => 'PassboltUA', + ])->userId($userId)->type(AuthenticationToken::TYPE_MFA_VERIFY)->persist(); + + $this->cookie(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE, $authToken->token); + + $this->get('/mfa/verify/duo/callback?state=' . $duoState . '&duo_code=' . UuidFactory::uuid()); + $this->assertResponseCode(302); + + $this->assertCookieSet(MfaVerifiedCookie::MFA_COOKIE_ALIAS); + $this->assertCookieNotSet(MfaDuoStateCookieService::MFA_COOKIE_DUO_STATE); + } +} diff --git a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Service/Duo/MfaDuoGetSdkClientServiceTest.php b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Service/Duo/MfaDuoGetSdkClientServiceTest.php new file mode 100644 index 0000000000..0cbe80d8f2 --- /dev/null +++ b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Service/Duo/MfaDuoGetSdkClientServiceTest.php @@ -0,0 +1,76 @@ + [ + MfaOrgSettings::DUO_CLIENT_ID => '', + MfaOrgSettings::DUO_CLIENT_SECRET => '', + MfaOrgSettings::DUO_API_HOSTNAME => '', + ], + ]); + $service = new MfaDuoGetSdkClientService(); + + $this->expectException(InternalErrorException::class); + $this->expectExceptionMessage('Could not validate the Duo settings.'); + $service->getOrFail($settings, AuthenticationToken::TYPE_MFA_SETUP); + } + + public function testMfaDuoGetSdkClientService_getCallbackUrl_Success_Setup() + { + $service = new MfaDuoGetSdkClientService(); + $expectedUrl = Router::url('/mfa/setup/duo/callback', true); + $url = $service->getCallbackRedirectUrl(AuthenticationToken::TYPE_MFA_SETUP); + + $this->assertEquals($expectedUrl, $url); + } + + public function testMfaDuoGetSdkClientService_getCallbackUrl_Success_Verify() + { + $service = new MfaDuoGetSdkClientService(); + $expectedUrl = Router::url('/mfa/verify/duo/callback', true); + $url = $service->getCallbackRedirectUrl(AuthenticationToken::TYPE_MFA_VERIFY); + + $this->assertEquals($expectedUrl, $url); + } + + public function testMfaDuoGetSdkClientService_getCallbackUrl_Error_Invalid_Token_Type() + { + $service = new MfaDuoGetSdkClientService(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The authentication token type should be one of the following: mfa_setup, mfa_verify.'); + $service->getCallbackRedirectUrl('invalid_token_type'); + } +} diff --git a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Service/Duo/MfaDuoVerifyDuoCodeServiceTest.php b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Service/Duo/MfaDuoVerifyDuoCodeServiceTest.php index 5ff44a7623..0d29743667 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Service/Duo/MfaDuoVerifyDuoCodeServiceTest.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Service/Duo/MfaDuoVerifyDuoCodeServiceTest.php @@ -17,6 +17,7 @@ namespace Passbolt\MultiFactorAuthentication\Test\TestCase\Service\Duo; +use App\Model\Entity\AuthenticationToken; use App\Model\Entity\Role; use App\Test\Factory\UserFactory; use App\Utility\UserAccessControl; @@ -42,7 +43,7 @@ public function testMfaDuoVerifyDuoCodeService_Success() $duoCode = 'not-so-random-duo-code'; $duoSdkClientMock = DuoSdkClientMock::createDefault($this, $user)->getClient(); - $service = new MfaDuoVerifyDuoCodeService($duoSdkClientMock); + $service = new MfaDuoVerifyDuoCodeService(AuthenticationToken::TYPE_MFA_VERIFY, $duoSdkClientMock); $verified = $service->verify($uac, $duoCode); $this->assertTrue($verified); @@ -57,7 +58,7 @@ public function testMfaDuoVerifyDuoCodeService_Error_CannotRetrieveAuthenticatio $duoCode = 'not-so-random-duo-code'; $duoSdkClientMock = DuoSdkClientMock::createWithExchangeAuthorizationCodeFor2FAResultThrowingException($this); - $service = new MfaDuoVerifyDuoCodeService($duoSdkClientMock->getClient()); + $service = new MfaDuoVerifyDuoCodeService(AuthenticationToken::TYPE_MFA_VERIFY, $duoSdkClientMock->getClient()); try { $service->verify($uac, $duoCode); @@ -77,7 +78,7 @@ public function testMfaDuoVerifyDuoCodeService_Error_AuthenticationDetailsDuoExc $duoCode = 'not-so-random-duo-code'; $duoSdkClientMock = (new DuoSdkClientMock($this))->mockInvalidExchangeAuthorizationCodeFor2FAResult(); - $service = new MfaDuoVerifyDuoCodeService($duoSdkClientMock->getClient()); + $service = new MfaDuoVerifyDuoCodeService(AuthenticationToken::TYPE_MFA_VERIFY, $duoSdkClientMock->getClient()); try { $service->verify($uac, $duoCode); @@ -97,7 +98,7 @@ public function testMfaDuoVerifyDuoCodeService_Error_WrongAuthenticationDetailsT $duoCode = 'not-so-random-duo-code'; $duoSdkClientMock = (new DuoSdkClientMock($this))->mockInvalidExchangeAuthorizationCodeFor2FAResult(); - $service = new MfaDuoVerifyDuoCodeService($duoSdkClientMock->getClient()); + $service = new MfaDuoVerifyDuoCodeService(AuthenticationToken::TYPE_MFA_VERIFY, $duoSdkClientMock->getClient()); try { $service->verify($uac, $duoCode); @@ -117,7 +118,7 @@ public function testMfaDuoVerifyDuoCodeService_Error_CallbackWrongIss() $duoCode = 'not-so-random-duo-code'; $mock = DuoSdkClientMock::createWithWrongExchangeAuthorizationCodeFor2FAResultIss($this, $user); - $service = new MfaDuoVerifyDuoCodeService($mock->getClient()); + $service = new MfaDuoVerifyDuoCodeService(AuthenticationToken::TYPE_MFA_VERIFY, $mock->getClient()); try { $service->verify($uac, $duoCode); @@ -137,7 +138,7 @@ public function testMfaDuoVerifyDuoCodeService_Error_CallbackWrongUsername() $duoCode = 'not-so-random-duo-code'; $duoSdkClientMock = DuoSdkClientMock::createWithWrongExchangeAuthorizationCodeFor2FAResultSub($this); - $service = new MfaDuoVerifyDuoCodeService($duoSdkClientMock->getClient()); + $service = new MfaDuoVerifyDuoCodeService(AuthenticationToken::TYPE_MFA_VERIFY, $duoSdkClientMock->getClient()); try { $service->verify($uac, $duoCode);