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 @@ +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/Code2fTimeBasedAuthenticationChecker.php b/src/Authentication/Code2fTimeBasedAuthenticationChecker.php new file mode 100644 index 0000000..575de4e --- /dev/null +++ b/src/Authentication/Code2fTimeBasedAuthenticationChecker.php @@ -0,0 +1,59 @@ +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(); + } +} diff --git a/src/Authentication/DefaultCode2fAuthenticationChecker.php b/src/Authentication/DefaultCode2fAuthenticationChecker.php new file mode 100644 index 0000000..d4f20f2 --- /dev/null +++ b/src/Authentication/DefaultCode2fAuthenticationChecker.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;