forked from passbolt/passbolt_api
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PB-21872 - As LU, I can verify Duo callback to login using Duo as MFA
Showing
14 changed files
with
592 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
...assboltCe/MultiFactorAuthentication/src/Controller/Duo/DuoVerifyCallbackGetController.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
/** | ||
* Passbolt ~ Open source password manager for teams | ||
* Copyright (c) Passbolt SA (https://www.passbolt.com) | ||
* | ||
* Licensed under GNU Affero General Public License version 3 of the or any later version. | ||
* For full copyright and license information, please see the LICENSE.txt | ||
* Redistributions of files must retain the above copyright notice. | ||
* | ||
* @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) | ||
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License | ||
* @link https://www.passbolt.com Passbolt(tm) | ||
* @since 3.11.0 | ||
*/ | ||
namespace Passbolt\MultiFactorAuthentication\Controller\Duo; | ||
|
||
use App\Authenticator\SessionIdentificationServiceInterface; | ||
use App\Error\Exception\FormValidationException; | ||
use App\Model\Entity\AuthenticationToken; | ||
use App\Utility\UserAccessControl; | ||
use Cake\Http\Cookie\Cookie; | ||
use Cake\Http\Exception\BadRequestException; | ||
use Cake\Http\Exception\InternalErrorException; | ||
use Cake\Validation\Validation; | ||
use Duo\DuoUniversal\Client; | ||
use Passbolt\MultiFactorAuthentication\Controller\MfaVerifyController; | ||
use Passbolt\MultiFactorAuthentication\Form\Duo\DuoCallbackForm; | ||
use Passbolt\MultiFactorAuthentication\Model\Dto\MfaDuoCallbackDto; | ||
use Passbolt\MultiFactorAuthentication\Service\Duo\MfaDuoLoginService; | ||
use Passbolt\MultiFactorAuthentication\Service\Duo\MfaDuoStateCookieService; | ||
use Passbolt\MultiFactorAuthentication\Service\MfaVerifiedCookieService; | ||
use Passbolt\MultiFactorAuthentication\Utility\MfaSettings; | ||
|
||
class DuoVerifyCallbackGetController extends MfaVerifyController | ||
{ | ||
/** | ||
* Handle Duo setup callback GET request. Redirect the user if the auth token associated to the callback | ||
* contains a redirect property. It is usually the case when a user authenticates to duo on the web application. | ||
* | ||
* @param \App\Authenticator\SessionIdentificationServiceInterface $sessionIdentificationService session ID service | ||
* @param \Duo\DuoUniversal\Client|null $duoSdkClient Duo SDK Client | ||
* @return \Cake\Http\Response|void | ||
*/ | ||
public function get( | ||
SessionIdentificationServiceInterface $sessionIdentificationService, | ||
?Client $duoSdkClient = null | ||
) { | ||
$this->_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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
plugins/PassboltCe/MultiFactorAuthentication/src/Service/Duo/MfaDuoLoginService.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
/** | ||
* Passbolt ~ Open source password manager for teams | ||
* Copyright (c) Passbolt SA (https://www.passbolt.com) | ||
* | ||
* Licensed under GNU Affero General Public License version 3 of the or any later version. | ||
* For full copyright and license information, please see the LICENSE.txt | ||
* Redistributions of files must retain the above copyright notice. | ||
* | ||
* @copyright Copyright (c) Passbolt SA (https://www.passbolt.com) | ||
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License | ||
* @link https://www.passbolt.com Passbolt(tm) | ||
* @since 3.11.0 | ||
*/ | ||
|
||
namespace Passbolt\MultiFactorAuthentication\Service\Duo; | ||
|
||
use App\Model\Entity\AuthenticationToken; | ||
use App\Utility\UserAccessControl; | ||
use Cake\Http\Exception\InternalErrorException; | ||
use Cake\Validation\Validation; | ||
use Duo\DuoUniversal\Client; | ||
use Passbolt\MultiFactorAuthentication\Model\Dto\MfaDuoCallbackDto; | ||
use Passbolt\MultiFactorAuthentication\Service\MfaOrgSettings\MfaOrgSettingsDuoService; | ||
use Passbolt\MultiFactorAuthentication\Utility\MfaOrgSettings; | ||
|
||
/** | ||
* Class MfaDuoLoginService | ||
*/ | ||
class MfaDuoLoginService | ||
{ | ||
/** | ||
* @var \Duo\DuoUniversal\Client | ||
*/ | ||
protected $duoClient; | ||
|
||
/** | ||
* MfaDuoLoginService constructor. | ||
* | ||
* @param \Duo\DuoUniversal\Client|null $client Duo SDK Client | ||
* @return void | ||
* @throws \Cake\Http\Exception\InternalErrorException If it cannot create the Duo Sdk Client | ||
*/ | ||
public function __construct(?Client $client = null) | ||
{ | ||
try { | ||
$this->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; | ||
} | ||
} |
Oops, something went wrong.