diff --git a/AUTHORS b/AUTHORS index fe478401fddb4..a816c2d1698be 100644 --- a/AUTHORS +++ b/AUTHORS @@ -86,6 +86,7 @@ - Carlos Cerrillo - Carlos Ferreira - Carsten Wiedmann + - Charles Taborin - Chih-Hsuan Yen - Christian <16852529+cviereck@users.noreply.github.com> - Christian Berendt diff --git a/apps/settings/composer/composer/autoload_classmap.php b/apps/settings/composer/composer/autoload_classmap.php index 511bcf3c938aa..d3609b91d10f1 100644 --- a/apps/settings/composer/composer/autoload_classmap.php +++ b/apps/settings/composer/composer/autoload_classmap.php @@ -38,6 +38,7 @@ 'OCA\\Settings\\Controller\\TwoFactorSettingsController' => $baseDir . '/../lib/Controller/TwoFactorSettingsController.php', 'OCA\\Settings\\Controller\\UsersController' => $baseDir . '/../lib/Controller/UsersController.php', 'OCA\\Settings\\Controller\\WebAuthnController' => $baseDir . '/../lib/Controller/WebAuthnController.php', + 'OCA\\Settings\\Events\\AfterAuthTokenCreatedEvent' => $baseDir . '/../lib/Events/AfterAuthTokenCreatedEvent.php', 'OCA\\Settings\\Events\\BeforeTemplateRenderedEvent' => $baseDir . '/../lib/Events/BeforeTemplateRenderedEvent.php', 'OCA\\Settings\\Hooks' => $baseDir . '/../lib/Hooks.php', 'OCA\\Settings\\Listener\\AppPasswordCreatedActivityListener' => $baseDir . '/../lib/Listener/AppPasswordCreatedActivityListener.php', diff --git a/apps/settings/composer/composer/autoload_static.php b/apps/settings/composer/composer/autoload_static.php index e23da014bd81a..551786c91340a 100644 --- a/apps/settings/composer/composer/autoload_static.php +++ b/apps/settings/composer/composer/autoload_static.php @@ -53,6 +53,7 @@ class ComposerStaticInitSettings 'OCA\\Settings\\Controller\\TwoFactorSettingsController' => __DIR__ . '/..' . '/../lib/Controller/TwoFactorSettingsController.php', 'OCA\\Settings\\Controller\\UsersController' => __DIR__ . '/..' . '/../lib/Controller/UsersController.php', 'OCA\\Settings\\Controller\\WebAuthnController' => __DIR__ . '/..' . '/../lib/Controller/WebAuthnController.php', + 'OCA\\Settings\\Events\\AfterAuthTokenCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/AfterAuthTokenCreatedEvent.php', 'OCA\\Settings\\Events\\BeforeTemplateRenderedEvent' => __DIR__ . '/..' . '/../lib/Events/BeforeTemplateRenderedEvent.php', 'OCA\\Settings\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php', 'OCA\\Settings\\Listener\\AppPasswordCreatedActivityListener' => __DIR__ . '/..' . '/../lib/Listener/AppPasswordCreatedActivityListener.php', diff --git a/apps/settings/lib/Controller/AuthSettingsController.php b/apps/settings/lib/Controller/AuthSettingsController.php index 8652a49fb1d6e..3873d0da90228 100644 --- a/apps/settings/lib/Controller/AuthSettingsController.php +++ b/apps/settings/lib/Controller/AuthSettingsController.php @@ -14,6 +14,7 @@ use OC\Authentication\Token\IProvider; use OC\Authentication\Token\RemoteWipe; use OCA\Settings\Activity\Provider; +use OCA\Settings\Events\AfterAuthTokenCreatedEvent; use OCP\Activity\IManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -24,6 +25,7 @@ use OCP\Authentication\Exceptions\InvalidTokenException; use OCP\Authentication\Exceptions\WipeTokenException; use OCP\Authentication\Token\IToken; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; @@ -47,6 +49,7 @@ class AuthSettingsController extends Controller { * @param string|null $userId * @param IUserSession $userSession * @param IManager $activityManager + * @param IEventDispatcher $eventDispatcher * @param RemoteWipe $remoteWipe * @param LoggerInterface $logger */ @@ -59,6 +62,7 @@ public function __construct( private ?string $userId, private IUserSession $userSession, private IManager $activityManager, + private IEventDispatcher $eventDispatcher, RemoteWipe $remoteWipe, private LoggerInterface $logger, ) { @@ -106,6 +110,12 @@ public function create($name) { } $token = $this->generateRandomDeviceToken(); + + // Allow apps to post-process the generated token before persisting it + $event = new AfterAuthTokenCreatedEvent($token); + $this->eventDispatcher->dispatchTyped($event); + $token = $event->getToken(); + $deviceToken = $this->tokenProvider->generateToken($token, $this->userId, $loginName, $password, $name, IToken::PERMANENT_TOKEN); $tokenData = $deviceToken->jsonSerialize(); $tokenData['canDelete'] = true; diff --git a/apps/settings/lib/Events/AfterAuthTokenCreatedEvent.php b/apps/settings/lib/Events/AfterAuthTokenCreatedEvent.php new file mode 100644 index 0000000000000..88d4d1554d758 --- /dev/null +++ b/apps/settings/lib/Events/AfterAuthTokenCreatedEvent.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Settings\Events; + +use OCP\EventDispatcher\Event; + +class AfterAuthTokenCreatedEvent extends Event { + public function __construct( + private string $token, + ) { + } + + public function getToken(): string { + return $this->token; + } + + public function setToken(string $token): void { + $this->token = $token; + } +} diff --git a/apps/settings/tests/Controller/AuthSettingsControllerTest.php b/apps/settings/tests/Controller/AuthSettingsControllerTest.php index d195dbf83d394..5fe4e8b1526be 100644 --- a/apps/settings/tests/Controller/AuthSettingsControllerTest.php +++ b/apps/settings/tests/Controller/AuthSettingsControllerTest.php @@ -16,9 +16,11 @@ use OC\Authentication\Token\PublicKeyToken; use OC\Authentication\Token\RemoteWipe; use OCA\Settings\Controller\AuthSettingsController; +use OCA\Settings\Events\AfterAuthTokenCreatedEvent; use OCP\Activity\IEvent; use OCP\Activity\IManager; use OCP\AppFramework\Http\JSONResponse; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; use OCP\ISession; use OCP\IUserSession; @@ -36,6 +38,7 @@ class AuthSettingsControllerTest extends TestCase { private ISecureRandom&MockObject $secureRandom; private IManager&MockObject $activityManager; private RemoteWipe&MockObject $remoteWipe; + private IEventDispatcher&MockObject $eventDispatcher; private string $uid = 'jane'; private AuthSettingsController $controller; @@ -49,6 +52,7 @@ protected function setUp(): void { $this->secureRandom = $this->createMock(ISecureRandom::class); $this->activityManager = $this->createMock(IManager::class); $this->remoteWipe = $this->createMock(RemoteWipe::class); + $this->eventDispatcher = $this->createMock(IEventDispatcher::class); /** @var LoggerInterface&MockObject $logger */ $logger = $this->createMock(LoggerInterface::class); @@ -61,6 +65,7 @@ protected function setUp(): void { $this->uid, $this->userSession, $this->activityManager, + $this->eventDispatcher, $this->remoteWipe, $logger ); @@ -93,6 +98,13 @@ public function testCreate(): void { ->willReturn('XXXXX'); $newToken = 'XXXXX-XXXXX-XXXXX-XXXXX-XXXXX'; + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with($this->callback(function (AfterAuthTokenCreatedEvent $event) use ($newToken) { + $this->assertSame($newToken, $event->getToken()); + return true; + })); + $this->tokenProvider->expects($this->once()) ->method('generateToken') ->with($newToken, $this->uid, 'User13', $password, $name, IToken::PERMANENT_TOKEN) @@ -115,6 +127,56 @@ public function testCreate(): void { $this->assertEquals($expected, $response->getData()); } + public function testCreateTokenModifiedByEvent(): void { + $name = 'Pixel 8'; + $sessionToken = $this->createMock(IToken::class); + $deviceToken = $this->createMock(IToken::class); + + $this->session->expects($this->once()) + ->method('getId') + ->willReturn('sessionid'); + $this->tokenProvider->expects($this->once()) + ->method('getToken') + ->with('sessionid') + ->willReturn($sessionToken); + $this->tokenProvider->expects($this->once()) + ->method('getPassword') + ->with($sessionToken, 'sessionid') + ->willReturn('secret'); + $sessionToken->expects($this->once()) + ->method('getLoginName') + ->willReturn('User99'); + + $this->secureRandom->expects($this->exactly(5)) + ->method('generate') + ->with(5, ISecureRandom::CHAR_HUMAN_READABLE) + ->willReturnOnConsecutiveCalls('AAAAA', 'BBBBB', 'CCCCC', 'DDDDD', 'EEEEE'); + $initialToken = 'AAAAA-BBBBB-CCCCC-DDDDD-EEEEE'; + + $this->eventDispatcher->expects($this->once()) + ->method('dispatchTyped') + ->with($this->callback(function (AfterAuthTokenCreatedEvent $event) use ($initialToken) { + $this->assertSame($initialToken, $event->getToken()); + $event->setToken('custom-token'); + return true; + })); + + $this->tokenProvider->expects($this->once()) + ->method('generateToken') + ->with('custom-token', $this->uid, 'User99', 'secret', $name, IToken::PERMANENT_TOKEN) + ->willReturn($deviceToken); + + $deviceToken->expects($this->once()) + ->method('jsonSerialize') + ->willReturn(['dummy' => 'dummy', 'canDelete' => true]); + + $this->mockActivityManager(); + + $response = $this->controller->create($name); + $this->assertInstanceOf(JSONResponse::class, $response); + $this->assertSame('custom-token', $response->getData()['token']); + } + public function testCreateSessionNotAvailable(): void { $name = 'personal phone';