diff --git a/.semver b/.semver index 66bf445c..8bf2191b 100644 --- a/.semver +++ b/.semver @@ -1,5 +1,5 @@ --- :major: 16 :minor: 0 -:patch: 0 +:patch: 1 :special: '' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c2784c4..d5e5b40d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ Changelog ========= Releases for CakePHP 5 ------------- +* 16.0.1 + * add event for skipping two-factor authentication verification * 16.0.0 * Require CakePHP ^5.3 * Added `src/Plugin.php` class which extends `UsersPlugin` for backward compatibility. diff --git a/Docs/Documentation/Two-Factor-Authenticator.md b/Docs/Documentation/Two-Factor-Authenticator.md index 022445a0..bcb976a9 100644 --- a/Docs/Documentation/Two-Factor-Authenticator.md +++ b/Docs/Documentation/Two-Factor-Authenticator.md @@ -52,3 +52,32 @@ the QR code shown (image 1). + +Skipping 2FA Verification +------------------------- +You can conditionally skip the Two-Factor Authentication verify step for specific users +or scenarios by listening to the `UsersPlugin::EVENT_2FA_SKIP_VERIFY` event. + +If the event returns `true`, the verification is skipped, and the user is logged in immediately. +The event receives the `user` data as a payload. + +**Example: Skipping 2FA based on User Role** + +In your `src/Application.php` or a dedicated Event Listener: + +```php +use Cake\Event\EventInterface; +use CakeDC\Users\UsersPlugin; + +// ... + +$this->getEventManager()->on(UsersPlugin::EVENT_2FA_SKIP_VERIFY, function (EventInterface $event) { + $user = $event->getData('user'); + + // Check if the user has the 'admin' role + if (!empty($user['role']) && $user['role'] === 'admin') { + // Return true to skip the 2FA verification for this user role + $event->setResult(true); + } +}); +``` \ No newline at end of file diff --git a/src/Controller/Traits/OneTimePasswordVerifyTrait.php b/src/Controller/Traits/OneTimePasswordVerifyTrait.php index d5f682dd..195805d2 100644 --- a/src/Controller/Traits/OneTimePasswordVerifyTrait.php +++ b/src/Controller/Traits/OneTimePasswordVerifyTrait.php @@ -16,6 +16,7 @@ use Cake\Core\Configure; use CakeDC\Auth\Authentication\AuthenticationService; use CakeDC\Auth\Authenticator\TwoFactorAuthenticator; +use CakeDC\Users\UsersPlugin; trait OneTimePasswordVerifyTrait { @@ -43,6 +44,15 @@ public function verify() $temporarySession = $this->getRequest()->getSession()->read( AuthenticationService::TWO_FACTOR_VERIFY_SESSION_KEY, ); + + $event = $this->dispatchEvent(UsersPlugin::EVENT_2FA_SKIP_VERIFY, ['user' => $temporarySession]); + if ($event->getResult() === true) { + $this->getRequest()->getSession()->delete(AuthenticationService::TWO_FACTOR_VERIFY_SESSION_KEY); + $this->getRequest()->getSession()->write(TwoFactorAuthenticator::USER_SESSION_KEY, $temporarySession); + + return $this->redirect($loginAction); + } + $secretVerified = $temporarySession['secret_verified'] ?? null; // showing QR-code until shared secret is verified if (!$secretVerified) { @@ -55,7 +65,10 @@ public function verify() $temporarySession['email'], $secret, ); - $this->set(['secretDataUri' => $secretDataUri]); + $this->set([ + 'secretDataUri' => $secretDataUri, + 'secret' => $secret, + ]); } if ($this->getRequest()->is('post')) { diff --git a/src/UsersPlugin.php b/src/UsersPlugin.php index 794e91e2..55b7a4af 100644 --- a/src/UsersPlugin.php +++ b/src/UsersPlugin.php @@ -48,6 +48,8 @@ class UsersPlugin extends BasePlugin public const EVENT_AFTER_RESEND_TOKEN_VALIDATION = 'Users.Global.afterResendTokenValidation'; public const EVENT_AFTER_EMAIL_TOKEN_VALIDATION = 'Users.Global.afterEmailTokenValidation'; + public const EVENT_2FA_SKIP_VERIFY = 'Users.TwoFactor.skipVerify'; + public const DEPRECATED_MESSAGE_U2F = 'U2F is no longer supported by chrome, we suggest using Webauthn as a replacement'; diff --git a/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php b/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php index 1ceae9d2..1a10d8b8 100644 --- a/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php +++ b/tests/TestCase/Controller/Traits/OneTimePasswordVerifyTraitTest.php @@ -14,9 +14,13 @@ namespace CakeDC\Users\Test\TestCase\Controller\Traits; use Cake\Core\Configure; +use Cake\Event\Event; use Cake\Http\ServerRequest; use Cake\ORM\TableRegistry; +use CakeDC\Auth\Authentication\AuthenticationService; +use CakeDC\Auth\Authenticator\TwoFactorAuthenticator; use CakeDC\Auth\Controller\Component\OneTimePasswordAuthenticatorComponent; +use CakeDC\Users\UsersPlugin; class OneTimePasswordVerifyTraitTest extends BaseTrait { @@ -147,7 +151,7 @@ public function testVerifyGetShowQR() ->will($this->returnValue('newDataUriGenerated')); $this->Trait->expects($this->once()) ->method('set') - ->with(['secretDataUri' => 'newDataUriGenerated']); + ->with(['secretDataUri' => 'newDataUriGenerated', 'secret' => 'newSecret']); $this->Trait->verify(); $user = $this->Trait->getUsersTable()->findById('00000000-0000-0000-0000-000000000001')->firstOrFail(); @@ -277,4 +281,49 @@ public function testVerifyGetDoesNotGenerateNewSecret() $session->read(), ); } + + /** + * testVerifySkipEventCheck + */ + public function testVerifySkipEventCheck() + { + Configure::write('OneTimePasswordAuthenticator.login', true); + $request = $this->getMockBuilder('Cake\Http\ServerRequest') + ->onlyMethods(['is', 'getData', 'getSession']) + ->addMethods(['allow']) + ->getMock(); + $this->Trait->setRequest($request); + + $userData = [ + 'id' => 1, + 'secret_verified' => 1, + 'email' => 'test@example.com', + ]; + $session = $this->_mockSession([ + 'temporarySession' => $userData, + ]); + + $eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->getMock(); + $eventMock->method('getResult')->willReturn(true); + + $this->Trait->expects($this->once()) + ->method('dispatchEvent') + ->with(UsersPlugin::EVENT_2FA_SKIP_VERIFY, ['user' => $userData]) + ->willReturn($eventMock); + + $this->Trait->expects($this->once()) + ->method('redirect'); + $this->Trait->verify(); + + $this->assertNull( + $session->read(AuthenticationService::TWO_FACTOR_VERIFY_SESSION_KEY), + ); + + $this->assertEquals( + $userData, + $session->read(TwoFactorAuthenticator::USER_SESSION_KEY), + ); + } }