diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7061901 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.bat] +end_of_line = crlf + +[*.yml] +indent_style = space +indent_size = 2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19bb238..e1824a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,11 @@ on: jobs: testsuite: - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - php-version: ['7.2', '7.3', '7.4', '8.0', '8.1'] + php-version: ['7.4', '8.0', '8.1'] db-type: [sqlite, mysql, pgsql] prefer-lowest: [''] @@ -77,7 +77,7 @@ jobs: cs-stan: name: Coding Standard & Static Analysis - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v2 @@ -85,7 +85,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: mbstring, intl, apcu, memcached, redis tools: cs2pr coverage: none diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e8a79..8de07b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [9.0.0] - 2024-02-09 +- Two factor processors introduced. Prvided way to add new two factor processors on client level. + ## [7.0.0] - 2021-10-30 -- upgrade to cakephp 4.3.0 +- upgrade to cakephp 4.3.0 - upgrade to phpunit 9.5 ## [6.1.0] - 2021-05-14 diff --git a/Docs/Documentation/TwoFactor.md b/Docs/Documentation/TwoFactor.md new file mode 100644 index 0000000..97fa518 --- /dev/null +++ b/Docs/Documentation/TwoFactor.md @@ -0,0 +1,17 @@ +Two Factor (2FA) +================ + +Two-factor authentication (2FA) is an identity and access management security method that requires two forms of identification to access resources and data. 2FA gives businesses the ability to monitor and help safeguard their most vulnerable information and networks. + +Configuration +------------- + +Processors defined as Configure storage with key `TwoFactorProcessors` + + +Processors +------------- + +* `U2FProcessor` - *deprecated*. Universal 2nd Factor (U2F) is an open standard that strengthens and simplifies two-factor authentication (2FA) using specialized Universal Serial Bus (USB) or near-field communication (NFC) devices based on similar security technology found in smart cards. +* `OneTimePassword` - Authenticator is an authenticator app used as part of a two-factor/multi-factor authentication (2FA/MFA) scheme. It acts as an example of a “something you have” factor by generating one-time passwords (OTPs) on a smartphone or other mobile device. +* `Webauthn2fa` - WebAuthn is a browser-based API that allows for web applications to simplify and secure user authentication by using registered devices (phones, laptops, etc) as factors. It uses public key cryptography to protect users from advanced phishing attacks. diff --git a/Docs/Home.md b/Docs/Home.md index 9782d32..2aeb858 100644 --- a/Docs/Home.md +++ b/Docs/Home.md @@ -1,6 +1,7 @@ Home ==== * [Authentication](Documentation/Authentication.md) + * [Two Factor](Documentation/TwoFactor.md) * [Authorization](Documentation/Authorization.md) * [Social](Documentation/Social.md) * [SimpleRbacAuthorize](Documentation/SimpleRbacAuthorize.md) RBAC Authorize based on configuration diff --git a/config/auth.php b/config/auth.php index 82fbef5..0264716 100644 --- a/config/auth.php +++ b/config/auth.php @@ -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' => [ diff --git a/src/Authentication/AuthenticationService.php b/src/Authentication/AuthenticationService.php index 9cdf7ea..742f2ef 100644 --- a/src/Authentication/AuthenticationService.php +++ b/src/Authentication/AuthenticationService.php @@ -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; @@ -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} * @@ -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; diff --git a/src/Authentication/TwoFactorProcessor/OneTimePasswordProcessor.php b/src/Authentication/TwoFactorProcessor/OneTimePasswordProcessor.php new file mode 100644 index 0000000..9ecd070 --- /dev/null +++ b/src/Authentication/TwoFactorProcessor/OneTimePasswordProcessor.php @@ -0,0 +1,114 @@ +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(); + } +} diff --git a/src/Authentication/TwoFactorProcessor/U2FProcessor.php b/src/Authentication/TwoFactorProcessor/U2FProcessor.php new file mode 100644 index 0000000..2486d23 --- /dev/null +++ b/src/Authentication/TwoFactorProcessor/U2FProcessor.php @@ -0,0 +1,120 @@ +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(); + } +} diff --git a/src/Authentication/TwoFactorProcessor/Webauthn2faProcessor.php b/src/Authentication/TwoFactorProcessor/Webauthn2faProcessor.php new file mode 100644 index 0000000..d8d1064 --- /dev/null +++ b/src/Authentication/TwoFactorProcessor/Webauthn2faProcessor.php @@ -0,0 +1,114 @@ +getWebauthn2fAuthenticationChecker()->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('Webauthn2fa.startAction'); + } + + return null; + } + + /** + * Get the configured u2f authentication checker + * + * @return \CakeDC\Auth\Authentication\Webauthn2fAuthenticationCheckerInterface + */ + protected function getWebauthn2fAuthenticationChecker() + { + return (new Webauthn2fAuthenticationCheckerFactory())->build(); + } +} diff --git a/src/Authentication/TwoFactorProcessorCollection.php b/src/Authentication/TwoFactorProcessorCollection.php new file mode 100644 index 0000000..a75653e --- /dev/null +++ b/src/Authentication/TwoFactorProcessorCollection.php @@ -0,0 +1,110 @@ + + */ + protected $processors = []; + + /** + * Constructor + * + * @param array<\CakeDC\Auth\Authentication\TwoFactorProcessorInterface> $processors The list of processors to add to the collection. + */ + public function __construct(array $processors = []) + { + $this->addMany($processors); + } + + /** + * Remove all processors from the collection + * + * @return $this + */ + public function clear() + { + $this->processors = []; + + return $this; + } + + /** + * Add multiple processors at once. + * + * @param array<\CakeDC\Auth\Authentication\TwoFactorProcessorInterface|string> $processors The list of processors to add to the collection. + * @return $this + */ + public function addMany(array $processors) + { + foreach ($processors as $processor) { + if (is_string($processor) && is_subclass_of($processor, TwoFactorProcessorInterface::class)) { + $processor = new $processor(); + } + $this->add($processor); + } + + return $this; + } + + /** + * Add a processor to the collection + * + * Processor will be keyed by their names. + * + * @param \CakeDC\Auth\Authentication\TwoFactorProcessorInterface $processor The processor to load. + * @return $this + * @throws \InvalidArgumentException + */ + public function add(TwoFactorProcessorInterface $processor) + { + $this->processors[] = $processor; + + return $this; + } + + /** + * Implementation of IteratorAggregate. + * + * @return \Traversable + * @psalm-return \Traversable + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->processors); + } + + /** + * Implementation of Countable. + * + * Get the number of processors in the collection. + * + * @return int + */ + public function count(): int + { + return count($this->processors); + } +} diff --git a/src/Authentication/TwoFactorProcessorInterface.php b/src/Authentication/TwoFactorProcessorInterface.php new file mode 100644 index 0000000..0a1809a --- /dev/null +++ b/src/Authentication/TwoFactorProcessorInterface.php @@ -0,0 +1,53 @@ +addMany($processors); + + return $collection; + } +} diff --git a/src/Authentication/Webauthn2fAuthenticationCheckerFactory.php b/src/Authentication/Webauthn2fAuthenticationCheckerFactory.php index 3d92573..3af4e2b 100644 --- a/src/Authentication/Webauthn2fAuthenticationCheckerFactory.php +++ b/src/Authentication/Webauthn2fAuthenticationCheckerFactory.php @@ -24,13 +24,13 @@ class Webauthn2fAuthenticationCheckerFactory /** * Get the two factor authentication checker * - * @return \CakeDC\Auth\Authentication\Webauthn2FAuthenticationCheckerInterface + * @return \CakeDC\Auth\Authentication\Webauthn2fAuthenticationCheckerInterface */ public function build() { $className = Configure::read('Webauthn2fa.checker'); $interfaces = class_implements($className); - $required = Webauthn2FAuthenticationCheckerInterface::class; + $required = Webauthn2fAuthenticationCheckerInterface::class; if (in_array($required, $interfaces)) { return new $className(); diff --git a/src/Authentication/Webauthn2fAuthenticationCheckerInterface.php b/src/Authentication/Webauthn2fAuthenticationCheckerInterface.php index f118a1b..e78d8f6 100644 --- a/src/Authentication/Webauthn2fAuthenticationCheckerInterface.php +++ b/src/Authentication/Webauthn2fAuthenticationCheckerInterface.php @@ -12,7 +12,7 @@ */ namespace CakeDC\Auth\Authentication; -interface Webauthn2FAuthenticationCheckerInterface +interface Webauthn2fAuthenticationCheckerInterface { /** * Check if two factor authentication is enabled diff --git a/src/Exception/InvalidProviderException.php b/src/Exception/InvalidProviderException.php index 9e7d851..e603f21 100644 --- a/src/Exception/InvalidProviderException.php +++ b/src/Exception/InvalidProviderException.php @@ -18,10 +18,10 @@ class InvalidProviderException extends CakeException { protected $_messageTemplate = 'Invalid provider or missing class (%s)'; - - /** - * @var int - */ + + /** + * @var int + */ protected $code = 500; /** diff --git a/src/Exception/InvalidSettingsException.php b/src/Exception/InvalidSettingsException.php index 38e9d04..c5571c5 100644 --- a/src/Exception/InvalidSettingsException.php +++ b/src/Exception/InvalidSettingsException.php @@ -19,9 +19,9 @@ class InvalidSettingsException extends CakeException { protected $_messageTemplate = 'Invalid settings for key (%s)'; - /** - * @var int - */ + /** + * @var int + */ protected $code = 500; /** diff --git a/src/Middleware/TwoFactorMiddleware.php b/src/Middleware/TwoFactorMiddleware.php index 677754f..172376d 100644 --- a/src/Middleware/TwoFactorMiddleware.php +++ b/src/Middleware/TwoFactorMiddleware.php @@ -13,10 +13,9 @@ namespace CakeDC\Auth\Middleware; -use Cake\Core\Configure; use Cake\Http\Response; use Cake\Routing\Router; -use CakeDC\Auth\Authentication\AuthenticationService; +use CakeDC\Auth\Authentication\TwoFactorProcessorLoader; use CakeDC\Auth\Authenticator\CookieAuthenticator; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -32,19 +31,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { $service = $request->getAttribute('authentication'); $status = $service->getResult() ? $service->getResult()->getStatus() : null; - switch ($status) { - case AuthenticationService::NEED_TWO_FACTOR_VERIFY: - $url = Configure::read('OneTimePasswordAuthenticator.verifyAction'); + $processors = TwoFactorProcessorLoader::processors(); + $url = null; + foreach ($processors as $processor) { + $url = $processor->getUrlByType($status); + if ($url !== null) { break; - case AuthenticationService::NEED_U2F_VERIFY: - $url = Configure::read('U2f.startAction'); - break; - case AuthenticationService::NEED_WEBAUTHN_2FA_VERIFY: - $url = Configure::read('Webauthn2fa.startAction'); - break; - default: - return $handler->handle($request); + } } + if ($url === null) { + return $handler->handle($request); + } + /** * @var \Cake\Http\Session $session */ diff --git a/src/Plugin.php b/src/Plugin.php index 87557af..7166fa2 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -21,4 +21,5 @@ */ class Plugin extends BasePlugin { + public const DEPRECATED_MESSAGE_U2F = 'U2F is no longer supported by chrome, we suggest using Webauthn as a replacement'; } diff --git a/tests/TestCase/Authentication/AuthenticationServiceTest.php b/tests/TestCase/Authentication/AuthenticationServiceTest.php index e8750ac..7594fbd 100644 --- a/tests/TestCase/Authentication/AuthenticationServiceTest.php +++ b/tests/TestCase/Authentication/AuthenticationServiceTest.php @@ -20,6 +20,9 @@ use Cake\TestSuite\TestCase; use CakeDC\Auth\Authentication\AuthenticationService; use CakeDC\Auth\Authentication\Failure; +use CakeDC\Auth\Authentication\TwoFactorProcessor\OneTimePasswordProcessor; +use CakeDC\Auth\Authentication\TwoFactorProcessor\U2FProcessor; +use CakeDC\Auth\Authentication\TwoFactorProcessor\Webauthn2faProcessor; use CakeDC\Auth\Authenticator\FormAuthenticator; class AuthenticationServiceTest extends TestCase @@ -49,6 +52,7 @@ public function testAuthenticateEmptyAuthenticators() $response = new Response(); $service = new AuthenticationService([ + 'processors' => [], 'identifiers' => [ 'Authentication.Password', ], @@ -79,6 +83,7 @@ public function testAuthenticateFail() $response = new Response(); $service = new AuthenticationService([ + 'processors' => [], 'identifiers' => [ 'Authentication.Password', ], @@ -131,6 +136,11 @@ public function testAuthenticate() ); $service = new AuthenticationService([ + 'processors' => [ + new OneTimePasswordProcessor(), + new U2FProcessor(), + new Webauthn2faProcessor(), + ], 'identifiers' => [ 'Authentication.Password', ], @@ -180,6 +190,11 @@ public function testAuthenticateShouldDoGoogleVerifyEnabled() ); $service = new AuthenticationService([ + 'processors' => [ + new OneTimePasswordProcessor(), + new U2FProcessor(), + new Webauthn2faProcessor(), + ], 'identifiers' => [ 'Authentication.Password' => [], ], @@ -232,6 +247,11 @@ public function testAuthenticateShouldDoGoogleVerifyDisabled() ); $service = new AuthenticationService([ + 'processors' => [ + new OneTimePasswordProcessor(), + new U2FProcessor(), + new Webauthn2faProcessor(), + ], 'identifiers' => [ 'Authentication.Password' => [], ], @@ -285,6 +305,11 @@ public function testAuthenticateShouldDoWebauthn2faEnabled() $response = new Response(); $service = new AuthenticationService([ + 'processors' => [ + new OneTimePasswordProcessor(), + new U2FProcessor(), + new Webauthn2faProcessor(), + ], 'identifiers' => [ 'Authentication.Password' => [], ], @@ -337,6 +362,11 @@ public function testAuthenticateShouldDoU2fEnabled() $response = new Response(); $service = new AuthenticationService([ + 'processors' => [ + new OneTimePasswordProcessor(), + new U2FProcessor(), + new Webauthn2faProcessor(), + ], 'identifiers' => [ 'Authentication.Password' => [], ], @@ -389,6 +419,11 @@ public function testAuthenticateShouldDoU2fDisabled() $response = new Response(); $service = new AuthenticationService([ + 'processors' => [ + new OneTimePasswordProcessor(), + new U2FProcessor(), + new Webauthn2faProcessor(), + ], 'identifiers' => [ 'Authentication.Password' => [], ], diff --git a/tests/TestCase/Authentication/Webauthn2fAuthenticationCheckerFactoryTest.php b/tests/TestCase/Authentication/Webauthn2fAuthenticationCheckerFactoryTest.php index 7c63fa8..5f117bb 100644 --- a/tests/TestCase/Authentication/Webauthn2fAuthenticationCheckerFactoryTest.php +++ b/tests/TestCase/Authentication/Webauthn2fAuthenticationCheckerFactoryTest.php @@ -39,7 +39,7 @@ public function testGetCheckerInvalidInterface() { Configure::write('Webauthn2fa.checker', \stdClass::class); $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage("Invalid config for 'Webauthn2fa.checker', 'stdClass' does not implement 'CakeDC\Auth\Authentication\Webauthn2FAuthenticationCheckerInterface'"); + $this->expectExceptionMessage("Invalid config for 'Webauthn2fa.checker', 'stdClass' does not implement 'CakeDC\Auth\Authentication\Webauthn2fAuthenticationCheckerInterface'"); (new Webauthn2fAuthenticationCheckerFactory())->build(); } }