Skip to content

Commit

Permalink
extract two factor logic into separate classes processing each type o…
Browse files Browse the repository at this point in the history
…f 2fa.
  • Loading branch information
skie committed Feb 8, 2024
1 parent 6abfdc9 commit 41fd79f
Show file tree
Hide file tree
Showing 15 changed files with 616 additions and 117 deletions.
5 changes: 5 additions & 0 deletions config/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@
]
],
],
'TwoFactorProcessors' => [
\CakeDC\Auth\Authentication\TwoFactorProcessor\OneTimePasswordProcessor::class,
\CakeDC\Auth\Authentication\TwoFactorProcessor\U2FProcessor::class,
\CakeDC\Auth\Authentication\TwoFactorProcessor\Webauthn2faProcessor::class,
],
'OneTimePasswordAuthenticator' => [
'checker' => \CakeDC\Auth\Authentication\DefaultOneTimePasswordAuthenticationChecker::class,
'verifyAction' => [
Expand Down
102 changes: 9 additions & 93 deletions src/Authentication/AuthenticationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
namespace CakeDC\Auth\Authentication;

use Authentication\AuthenticationService as BaseService;
use Authentication\Authenticator\Result;
use Authentication\Authenticator\ResultInterface;
use Authentication\Authenticator\StatelessInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand Down Expand Up @@ -42,95 +41,21 @@ class AuthenticationService extends BaseService
protected $failures = [];

/**
* Proceed to google verify action after a valid result result
* Proceed to 2fa processor after a valid result result
*
* @param \CakeDC\Auth\Authentication\TwoFactorProcessorInterface $processor The processor.
* @param \Psr\Http\Message\ServerRequestInterface $request The request.
* @param \Authentication\Authenticator\ResultInterface $result The original result
* @return \Authentication\Authenticator\ResultInterface The result object.
*/
protected function proceedToGoogleVerify(ServerRequestInterface $request, ResultInterface $result)
protected function proceed2FA(TwoFactorProcessorInterface $processor, ServerRequestInterface $request, ResultInterface $result)
{
/**
* @var \Cake\Http\Session $session
*/
$session = $request->getAttribute('session');
$session->write(self::TWO_FACTOR_VERIFY_SESSION_KEY, $result->getData());
$result = new Result(null, self::NEED_TWO_FACTOR_VERIFY);
$result = $processor->proceed($request, $result);
$this->_successfulAuthenticator = null;

return $this->_result = $result;
}

/**
* Proceed to webauthn2fa flow after a valid result 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 proceedToWebauthn2fa(ServerRequestInterface $request, ResultInterface $result)
{
/**
* @var \Cake\Http\Session $session
*/
$session = $request->getAttribute('session');
$session->write(self::WEBAUTHN_2FA_SESSION_KEY, $result->getData());
$result = new Result(null, self::NEED_WEBAUTHN_2FA_VERIFY);
$this->_successfulAuthenticator = null;

return $this->_result = $result;
}

/**
* Proceed to U2f flow after a valid result 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 proceedToU2f(ServerRequestInterface $request, ResultInterface $result)
{
/**
* @var \Cake\Http\Session $session
*/
$session = $request->getAttribute('session');
$session->write(self::U2F_SESSION_KEY, $result->getData());
$result = new Result(null, self::NEED_U2F_VERIFY);
$this->_successfulAuthenticator = null;

return $this->_result = $result;
}

/**
* Get the configured one-time password authentication checker
*
* @return \CakeDC\Auth\Authentication\OneTimePasswordAuthenticationCheckerInterface
*/
protected function getOneTimePasswordAuthenticationChecker()
{
return (new OneTimePasswordAuthenticationCheckerFactory())->build();
}

/**
* Get the configured u2f authentication checker
*
* @return \CakeDC\Auth\Authentication\Webauthn2FAuthenticationCheckerInterface
*/
protected function getWebauthn2fAuthenticationChecker()
{
return (new Webauthn2fAuthenticationCheckerFactory())->build();
}

/**
* Get the configured u2f authentication checker
*
* @return \CakeDC\Auth\Authentication\U2fAuthenticationCheckerInterface
*/
protected function getU2fAuthenticationChecker()
{
return (new U2fAuthenticationCheckerFactory())->build();
}

/**
* {@inheritDoc}
*
Expand All @@ -145,26 +70,17 @@ public function authenticate(ServerRequestInterface $request): ResultInterface
}

$result = null;
$processors = $this->getConfig('processors');
foreach ($this->authenticators() as $authenticator) {
$result = $authenticator->authenticate($request);
if ($result->isValid()) {
$skipTwoFactorVerify = $authenticator->getConfig('skipTwoFactorVerify');
$userData = $result->getData()->toArray();
$webauthn2faChecker = $this->getWebauthn2fAuthenticationChecker();
if ($skipTwoFactorVerify !== true && $webauthn2faChecker->isRequired($userData)) {
return $this->proceedToWebauthn2fa($request, $result);
foreach ($processors as $processor) {
if ($skipTwoFactorVerify !== true && $processor->isRequired($userData)) {
return $this->proceed2FA($processor, $request, $result);
}
}

$u2fCheck = $this->getU2fAuthenticationChecker();
if ($skipTwoFactorVerify !== true && $u2fCheck->isRequired($userData)) {
return $this->proceedToU2f($request, $result);
}

$otpCheck = $this->getOneTimePasswordAuthenticationChecker();
if ($skipTwoFactorVerify !== true && $otpCheck->isRequired($userData)) {
return $this->proceedToGoogleVerify($request, $result);
}

$this->_successfulAuthenticator = $authenticator;
$this->_result = $result;

Expand Down
114 changes: 114 additions & 0 deletions src/Authentication/TwoFactorProcessor/OneTimePasswordProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);

/**
* Copyright 2010 - 2024, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2010 - 2024, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace CakeDC\Auth\Authentication\TwoFactorProcessor;

use Authentication\Authenticator\Result;
use Authentication\Authenticator\ResultInterface;
use Cake\Core\Configure;
use CakeDC\Auth\Authentication\OneTimePasswordAuthenticationCheckerFactory;
use CakeDC\Auth\Authentication\TwoFactorProcessorInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* OneTimePasswordProcessor class
*/
class OneTimePasswordProcessor implements TwoFactorProcessorInterface
{
public const NEED_TWO_FACTOR_VERIFY = 'NEED_TWO_FACTOR_VERIFY';

public const TWO_FACTOR_VERIFY_SESSION_KEY = 'temporarySession';

/**
* Returns processor type.
*
* @return string
*/
public function getType(): string
{
return self::NEED_TWO_FACTOR_VERIFY;
}

/**
* Returns processor session key.
*
* @return string
*/
public function getSessionKey(): string
{
return self::TWO_FACTOR_VERIFY_SESSION_KEY;
}

/**
* Processor status detector.
*
* @return bool
*/
public function enabled(): bool
{
return Configure::read('OneTimePasswordAuthenticator.login') !== false;
}

/**
* Processor status detector.
*
* @return bool
*/
public function isRequired(array $userData): bool
{
return $this->getOneTimePasswordAuthenticationChecker()->isRequired($userData);
}

/**
* Proceed to 2fa processor after a valid result result.
*
* @param \Psr\Http\Message\ServerRequestInterface $request Request instance.
* @param \Authentication\Authenticator\ResultInterface $result Input result object.
* @return \Authentication\Authenticator\ResultInterface
*/
public function proceed(ServerRequestInterface $request, ResultInterface $result): ResultInterface
{
/**
* @var \Cake\Http\Session $session
*/
$session = $request->getAttribute('session');
$session->write($this->getSessionKey(), $result->getData());
$result = new Result(null, $this->getType());

return $result;
}

/**
* Generates 2fa url, if enable.
*
* @param string $type Processor type.
* @return array|null
*/
public function getUrlByType(string $type): ?array
{
if ($type == $this->getType()) {
return Configure::read('OneTimePasswordAuthenticator.verifyAction');
}

return null;
}

/**
* Get the configured one-time password authentication checker
*
* @return \CakeDC\Auth\Authentication\OneTimePasswordAuthenticationCheckerInterface
*/
protected function getOneTimePasswordAuthenticationChecker()
{
return (new OneTimePasswordAuthenticationCheckerFactory())->build();
}
}
119 changes: 119 additions & 0 deletions src/Authentication/TwoFactorProcessor/U2FProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);

/**
* Copyright 2010 - 2024, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2010 - 2024, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
namespace CakeDC\Auth\Authentication\TwoFactorProcessor;

use Authentication\Authenticator\Result;
use Authentication\Authenticator\ResultInterface;
use Cake\Core\Configure;
use CakeDC\Auth\Authentication\TwoFactorProcessorInterface;
use CakeDC\Auth\Authentication\U2fAuthenticationCheckerFactory;
use Psr\Http\Message\ServerRequestInterface;

/**
* U2FProcessor class
*/
class U2FProcessor implements TwoFactorProcessorInterface
{
public const U2F_SESSION_KEY = 'U2f.User';

public const NEED_U2F_VERIFY = 'NEED_U2F_VERIFY';

/**
* Returns processor type.
*
* @return string
*/
public function getType(): string
{
return self::NEED_U2F_VERIFY;
}

/**
* Returns processor session key.
*
* @return string
*/
public function getSessionKey(): string
{
return self::U2F_SESSION_KEY;
}

/**
* Processor status detector.
*
* @return bool
*/
public function enabled(): bool
{
$u2fEnabled = Configure::read('U2f.enabled') !== false;
if ($u2fEnabled) {
trigger_error(Plugin::DEPRECATED_MESSAGE_U2F, E_USER_DEPRECATED);
}

return $u2fEnabled;
}

/**
* Processor status detector.
*
* @return bool
*/
public function isRequired(array $userData): bool
{
return $this->getU2fAuthenticationChecker()->isRequired($userData);
}

/**
* Proceed to 2fa processor after a valid result result.
*
* @param \Psr\Http\Message\ServerRequestInterface $request Request instance.
* @param \Authentication\Authenticator\ResultInterface $result Input result object.
* @return \Authentication\Authenticator\ResultInterface
*/
public function proceed(ServerRequestInterface $request, ResultInterface $result): ResultInterface
{
/**
* @var \Cake\Http\Session $session
*/
$session = $request->getAttribute('session');
$session->write($this->getSessionKey(), $result->getData());
$result = new Result(null, $this->getType());

return $result;
}

/**
* Generates 2fa url, if enable.
*
* @param string $type Processor type.
* @return array|null
*/
public function getUrlByType(string $type): ?array
{
if ($type == $this->getType()) {
return Configure::read('U2f.startAction');
}

return null;
}

/**
* Get the configured u2f authentication checker
*
* @return \CakeDC\Auth\Authentication\U2fAuthenticationCheckerInterface
*/
protected function getU2fAuthenticationChecker()
{
return (new U2fAuthenticationCheckerFactory())->build();
}
}
Loading

0 comments on commit 41fd79f

Please sign in to comment.