diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 4467e4e6179c2..b95d8ba26fe4b 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -233,6 +233,7 @@ 'OCP\\Authentication\\TwoFactorAuth\\ALoginSetupController' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php', 'OCP\\Authentication\\TwoFactorAuth\\IActivatableAtLogin' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php', 'OCP\\Authentication\\TwoFactorAuth\\IActivatableByAdmin' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IActivatableByAdmin.php', + 'OCP\\Authentication\\TwoFactorAuth\\IChallengeRequester' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IChallengeRequester.php', 'OCP\\Authentication\\TwoFactorAuth\\IDeactivatableByAdmin' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IDeactivatableByAdmin.php', 'OCP\\Authentication\\TwoFactorAuth\\ILoginSetupProvider' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IPersonalProviderSettings' => $baseDir . '/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php', @@ -1355,6 +1356,7 @@ 'OC\\Authentication\\Token\\RemoteWipe' => $baseDir . '/lib/private/Authentication/Token/RemoteWipe.php', 'OC\\Authentication\\Token\\TokenCleanupJob' => $baseDir . '/lib/private/Authentication/Token/TokenCleanupJob.php', 'OC\\Authentication\\Token\\TokenScopes' => $baseDir . '/lib/private/Authentication/Token/TokenScopes.php', + 'OC\\Authentication\\TwoFactorAuth\\ChallengeRequester' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/ChallengeRequester.php', 'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php', 'OC\\Authentication\\TwoFactorAuth\\EnforcementState' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/EnforcementState.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => $baseDir . '/lib/private/Authentication/TwoFactorAuth/Manager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index dac59af04087a..6f728d6d6825d 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -274,6 +274,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Authentication\\TwoFactorAuth\\ALoginSetupController' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/ALoginSetupController.php', 'OCP\\Authentication\\TwoFactorAuth\\IActivatableAtLogin' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IActivatableAtLogin.php', 'OCP\\Authentication\\TwoFactorAuth\\IActivatableByAdmin' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IActivatableByAdmin.php', + 'OCP\\Authentication\\TwoFactorAuth\\IChallengeRequester' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IChallengeRequester.php', 'OCP\\Authentication\\TwoFactorAuth\\IDeactivatableByAdmin' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IDeactivatableByAdmin.php', 'OCP\\Authentication\\TwoFactorAuth\\ILoginSetupProvider' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/ILoginSetupProvider.php', 'OCP\\Authentication\\TwoFactorAuth\\IPersonalProviderSettings' => __DIR__ . '/../../..' . '/lib/public/Authentication/TwoFactorAuth/IPersonalProviderSettings.php', @@ -1396,6 +1397,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Authentication\\Token\\RemoteWipe' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/RemoteWipe.php', 'OC\\Authentication\\Token\\TokenCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/TokenCleanupJob.php', 'OC\\Authentication\\Token\\TokenScopes' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/TokenScopes.php', + 'OC\\Authentication\\TwoFactorAuth\\ChallengeRequester' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/ChallengeRequester.php', 'OC\\Authentication\\TwoFactorAuth\\Db\\ProviderUserAssignmentDao' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Db/ProviderUserAssignmentDao.php', 'OC\\Authentication\\TwoFactorAuth\\EnforcementState' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/EnforcementState.php', 'OC\\Authentication\\TwoFactorAuth\\Manager' => __DIR__ . '/../../..' . '/lib/private/Authentication/TwoFactorAuth/Manager.php', diff --git a/lib/private/Authentication/TwoFactorAuth/ChallengeRequester.php b/lib/private/Authentication/TwoFactorAuth/ChallengeRequester.php new file mode 100644 index 0000000000000..8781a90c00fa1 --- /dev/null +++ b/lib/private/Authentication/TwoFactorAuth/ChallengeRequester.php @@ -0,0 +1,45 @@ +session->exists('app_password') + && $this->session->get('app_api') !== true + && $this->manager->isTwoFactorAuthenticated($user); + } + + #[\Override] + public function requestChallenge(IUser $user, string $redirectUrl): string { + if (!$this->isAvailable($user)) { + throw new \LogicException('The second factor cannot be requested for this session'); + } + + $this->session->remove(Manager::SESSION_UID_DONE); + $this->manager->prepareTwoFactorLogin($user, false); + + return $this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge', [ + 'redirect_url' => $redirectUrl, + ]); + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index f1dff3b0693df..b21f62a46e9fa 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -469,6 +469,7 @@ public function __construct( $this->registerAlias(IUserSession::class, Session::class); $this->registerAlias(IRegistry::class, Registry::class); + $this->registerAlias(\OCP\Authentication\TwoFactorAuth\IChallengeRequester::class, \OC\Authentication\TwoFactorAuth\ChallengeRequester::class); $this->registerAlias(INavigationManager::class, NavigationManager::class); diff --git a/lib/public/Authentication/TwoFactorAuth/IChallengeRequester.php b/lib/public/Authentication/TwoFactorAuth/IChallengeRequester.php new file mode 100644 index 0000000000000..7daa8ac6b1556 --- /dev/null +++ b/lib/public/Authentication/TwoFactorAuth/IChallengeRequester.php @@ -0,0 +1,41 @@ +manager = $this->createMock(Manager::class); + $this->session = $this->createMock(ISession::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + $this->user = $this->createMock(IUser::class); + $this->requester = new ChallengeRequester($this->manager, $this->session, $this->urlGenerator); + } + + private function withSession(bool $appPassword, bool $appApi, bool $hasSecondFactor): void { + $this->session->method('exists') + ->willReturnCallback(static fn (string $key): bool => $key === 'app_password' && $appPassword); + $this->session->method('get') + ->willReturnCallback(static fn (string $key): mixed => $key === 'app_api' ? $appApi : null); + $this->manager->method('isTwoFactorAuthenticated') + ->with($this->user) + ->willReturn($hasSecondFactor); + } + + public static function dataIsAvailable(): array { + return [ + 'second factor' => [false, false, true, true], + 'no second factor' => [false, false, false, false], + 'app password session' => [true, false, true, false], + 'AppAPI session' => [false, true, true, false], + ]; + } + + #[\PHPUnit\Framework\Attributes\DataProvider(methodName: 'dataIsAvailable')] + public function testIsAvailable(bool $appPassword, bool $appApi, bool $hasSecondFactor, bool $expected): void { + $this->withSession($appPassword, $appApi, $hasSecondFactor); + + $this->assertSame($expected, $this->requester->isAvailable($this->user)); + } + + public function testRequestChallengeMarksTheSessionAndReturnsTheChallengeUrl(): void { + $this->withSession(false, false, true); + $this->session->expects($this->once()) + ->method('remove') + ->with(Manager::SESSION_UID_DONE); + $this->manager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with($this->user, false); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with('core.TwoFactorChallenge.selectChallenge', ['redirect_url' => '/apps/settings/user/security']) + ->willReturn('/index.php/login/selectchallenge?redirect_url=%2Fapps%2Fsettings%2Fuser%2Fsecurity'); + + $this->assertSame( + '/index.php/login/selectchallenge?redirect_url=%2Fapps%2Fsettings%2Fuser%2Fsecurity', + $this->requester->requestChallenge($this->user, '/apps/settings/user/security'), + ); + } + + public function testRequestChallengeRefusesSessionsThatCannotBeChallenged(): void { + $this->withSession(true, false, true); + $this->manager->expects($this->never()) + ->method('prepareTwoFactorLogin'); + + $this->expectException(\LogicException::class); + $this->requester->requestChallenge($this->user, '/'); + } +}