From f9c37ca9c6af589a33c7f72bd0727d68be6a1668 Mon Sep 17 00:00:00 2001 From: Alejandro Ibarra Date: Fri, 18 Feb 2022 11:02:42 +0100 Subject: [PATCH 1/2] Initial OTP implementation with phone and email --- config/permissions.php | 5 +- src/Authentication/AuthenticationService.php | 45 +++++++++++++++-- .../Code2fAuthenticationCheckerFactory.php | 40 ++++++++++++++++ .../Code2fAuthenticationCheckerInterface.php | 37 ++++++++++++++ .../DefaultCode2fAuthenticationChecker.php | 48 +++++++++++++++++++ ...FingerprintCode2fAuthenticationChecker.php | 48 +++++++++++++++++++ src/Middleware/TwoFactorMiddleware.php | 3 ++ 7 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 src/Authentication/Code2fAuthenticationCheckerFactory.php create mode 100644 src/Authentication/Code2fAuthenticationCheckerInterface.php create mode 100644 src/Authentication/DefaultCode2fAuthenticationChecker.php create mode 100644 src/Authentication/FingerprintCode2fAuthenticationChecker.php diff --git a/config/permissions.php b/config/permissions.php index 3b64d09..b863a45 100644 --- a/config/permissions.php +++ b/config/permissions.php @@ -70,7 +70,10 @@ 'requestResetPassword', // UserValidationTrait used in PasswordManagementTrait 'resendTokenValidation', - 'linkSocial' + 'linkSocial', + 'code2f', + 'code2fRegister', + 'code2fAuthenticate' ], 'bypassAuth' => true, ], diff --git a/src/Authentication/AuthenticationService.php b/src/Authentication/AuthenticationService.php index 379c241..65d36c2 100644 --- a/src/Authentication/AuthenticationService.php +++ b/src/Authentication/AuthenticationService.php @@ -30,6 +30,10 @@ class AuthenticationService extends BaseService public const NEED_U2F_VERIFY = 'NEED_U2F_VERIFY'; + public const CODE2F_SESSION_KEY = 'Code2f.User'; + + public const NEED_CODE2F_VERIFY = 'NEED_CODE2F_VERIFY'; + public const NEED_WEBAUTHN_2FA_VERIFY = 'NEED_WEBAUTHN2FA_VERIFY'; public const WEBAUTHN_2FA_SESSION_KEY = 'Webauthn2fa.User'; @@ -42,7 +46,7 @@ class AuthenticationService extends BaseService protected $failures = []; /** - * Proceed to google verify action after a valid result result + * Proceed to google verify action after a valid result * * @param \Psr\Http\Message\ServerRequestInterface $request The request. * @param \Authentication\Authenticator\ResultInterface $result The original result @@ -61,7 +65,7 @@ protected function proceedToGoogleVerify(ServerRequestInterface $request, Result return $this->_result = $result; } /** - * Proceed to webauthn2fa flow after a valid result result + * Proceed to webauthn2fa flow after a valid result * * @param \Psr\Http\Message\ServerRequestInterface $request response to manipulate * @param \Authentication\Authenticator\ResultInterface $result valid result @@ -81,7 +85,7 @@ protected function proceedToWebauthn2fa(ServerRequestInterface $request, ResultI } /** - * Proceed to U2f flow after a valid result result + * Proceed to U2f flow after a valid result * * @param \Psr\Http\Message\ServerRequestInterface $request response to manipulate * @param \Authentication\Authenticator\ResultInterface $result valid result @@ -100,6 +104,26 @@ protected function proceedToU2f(ServerRequestInterface $request, ResultInterface return $this->_result = $result; } + /** + * Proceed to Code2f flow after a valid result + * + * @param \Psr\Http\Message\ServerRequestInterface $request response to manipulate + * @param \Authentication\Authenticator\ResultInterface $result valid result + * @return \Authentication\Authenticator\ResultInterface with result, request and response keys + */ + protected function proceedToCode2f(ServerRequestInterface $request, ResultInterface $result) + { + /** + * @var \Cake\Http\Session $session + */ + $session = $request->getAttribute('session'); + $session->write(self::CODE2F_SESSION_KEY, $result->getData()); + $result = new Result(null, self::NEED_CODE2F_VERIFY); + $this->_successfulAuthenticator = null; + + return $this->_result = $result; + } + /** * Get the configured one-time password authentication checker * @@ -130,6 +154,16 @@ protected function getU2fAuthenticationChecker() return (new U2fAuthenticationCheckerFactory())->build(); } + /** + * Get the configured one-time password authentication checker + * + * @return \CakeDC\Auth\Authentication\OneTimePasswordAuthenticationCheckerInterface + */ + protected function getCode2fAuthenticationChecker() + { + return (new Code2fAuthenticationCheckerFactory())->build(); + } + /** * {@inheritDoc} * @@ -164,6 +198,11 @@ public function authenticate(ServerRequestInterface $request): ResultInterface return $this->proceedToGoogleVerify($request, $result); } + $code2fCheck = $this->getCode2fAuthenticationChecker(); + if ($skipTwoFactorVerify !== true && $code2fCheck->isRequired($userData, $request)) { + return $this->proceedToCode2f($request, $result); + } + $this->_successfulAuthenticator = $authenticator; $this->_result = $result; diff --git a/src/Authentication/Code2fAuthenticationCheckerFactory.php b/src/Authentication/Code2fAuthenticationCheckerFactory.php new file mode 100644 index 0000000..eff0dc8 --- /dev/null +++ b/src/Authentication/Code2fAuthenticationCheckerFactory.php @@ -0,0 +1,40 @@ +isEnabled(); + } +} diff --git a/src/Authentication/FingerprintCode2fAuthenticationChecker.php b/src/Authentication/FingerprintCode2fAuthenticationChecker.php new file mode 100644 index 0000000..6e68887 --- /dev/null +++ b/src/Authentication/FingerprintCode2fAuthenticationChecker.php @@ -0,0 +1,48 @@ +isEnabled(); + } +} diff --git a/src/Middleware/TwoFactorMiddleware.php b/src/Middleware/TwoFactorMiddleware.php index 677754f..89ea17a 100644 --- a/src/Middleware/TwoFactorMiddleware.php +++ b/src/Middleware/TwoFactorMiddleware.php @@ -39,6 +39,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface case AuthenticationService::NEED_U2F_VERIFY: $url = Configure::read('U2f.startAction'); break; + case AuthenticationService::NEED_CODE2F_VERIFY: + $url = Configure::read('Code2f.verifyAction'); + break; case AuthenticationService::NEED_WEBAUTHN_2FA_VERIFY: $url = Configure::read('Webauthn2fa.startAction'); break; From a18bf722aa5268c8a51a1763520314438551ad1f Mon Sep 17 00:00:00 2001 From: Alejandro Ibarra Date: Mon, 28 Feb 2022 13:34:51 +0100 Subject: [PATCH 2/2] Implement time based checker and fingerprint checker for code2f --- ...Code2fFingerprintAuthenticationChecker.php | 61 +++++++++++++++++++ ... Code2fTimeBasedAuthenticationChecker.php} | 15 ++++- 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/Authentication/Code2fFingerprintAuthenticationChecker.php rename src/Authentication/{FingerprintCode2fAuthenticationChecker.php => Code2fTimeBasedAuthenticationChecker.php} (65%) diff --git a/src/Authentication/Code2fFingerprintAuthenticationChecker.php b/src/Authentication/Code2fFingerprintAuthenticationChecker.php new file mode 100644 index 0000000..4219fe6 --- /dev/null +++ b/src/Authentication/Code2fFingerprintAuthenticationChecker.php @@ -0,0 +1,61 @@ +get('CakeDC/Users.OtpCodes')->find() + ->where(['user_id' => $user['id'], 'validated IS NOT' => null])->orderDesc('validated')->first(); + $fingerprint = Security::hash($request->clientIp() . $request->getHeaderLine('User-Agent')); + $request->getSession()->write('Code2f.fingerprint', $fingerprint); + return !empty($user) && + ( + empty($latestOtpCode) || + $latestOtpCode->fingerprint !== $fingerprint + ) && + $this->isEnabled(); + } +} diff --git a/src/Authentication/FingerprintCode2fAuthenticationChecker.php b/src/Authentication/Code2fTimeBasedAuthenticationChecker.php similarity index 65% rename from src/Authentication/FingerprintCode2fAuthenticationChecker.php rename to src/Authentication/Code2fTimeBasedAuthenticationChecker.php index 6e68887..575de4e 100644 --- a/src/Authentication/FingerprintCode2fAuthenticationChecker.php +++ b/src/Authentication/Code2fTimeBasedAuthenticationChecker.php @@ -13,6 +13,9 @@ namespace CakeDC\Auth\Authentication; use Cake\Core\Configure; +use Cake\I18n\FrozenTime; +use Cake\ORM\TableRegistry; +use CakeDC\Users\Model\Entity\OtpCode; use Psr\Http\Message\ServerRequestInterface; /** @@ -20,7 +23,7 @@ * * @package CakeDC\Auth\Auth */ -class FingerprintCode2fAuthenticationChecker implements Code2fAuthenticationCheckerInterface +class Code2fTimeBasedAuthenticationChecker implements Code2fAuthenticationCheckerInterface { /** * Check if two factor authentication is enabled @@ -43,6 +46,14 @@ public function isEnabled() */ public function isRequired(?array $user = null, ServerRequestInterface $request) { - return !empty($user) && $this->isEnabled(); + /** @var OtpCode $latestOtpCode */ + $latestOtpCode = TableRegistry::getTableLocator()->get('CakeDC/Users.OtpCodes')->find() + ->where(['user_id' => $user['id'], 'validated IS NOT' => null])->orderDesc('validated')->first(); + return !empty($user) && + ( + empty($latestOtpCode) || + ((new FrozenTime())->diffInDays($latestOtpCode->validated) >= Configure::read('Code2f.daysBeforeVerifyAgain', 15)) + ) && + $this->isEnabled(); } }