aboutsummaryrefslogtreecommitdiffstats
path: root/core/Middleware/TwoFactorMiddleware.php
blob: 0dea402f127abc98029ad817810cf847bb084bdd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<?php

declare(strict_types=1);

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OC\Core\Middleware;

use Exception;
use OC\Authentication\Exceptions\TwoFactorAuthRequiredException;
use OC\Authentication\Exceptions\UserAlreadyLoggedInException;
use OC\Authentication\TwoFactorAuth\Manager;
use OC\Core\Controller\LoginController;
use OC\Core\Controller\TwoFactorChallengeController;
use OC\User\Session;
use OCA\TwoFactorNextcloudNotification\Controller\APIController;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Middleware;
use OCP\AppFramework\Utility\IControllerMethodReflector;
use OCP\Authentication\TwoFactorAuth\ALoginSetupController;
use OCP\IRequest;
use OCP\ISession;
use OCP\IURLGenerator;
use OCP\IUser;

class TwoFactorMiddleware extends Middleware {
	public function __construct(
		private Manager $twoFactorManager,
		private Session $userSession,
		private ISession $session,
		private IURLGenerator $urlGenerator,
		private IControllerMethodReflector $reflector,
		private IRequest $request,
	) {
	}

	/**
	 * @param Controller $controller
	 * @param string $methodName
	 */
	public function beforeController($controller, $methodName) {
		if ($this->reflector->hasAnnotation('NoTwoFactorRequired')) {
			// Route handler explicitly marked to work without finished 2FA are
			// not blocked
			return;
		}

		if ($controller instanceof APIController && $methodName === 'poll') {
			// Allow polling the twofactor nextcloud notifications state
			return;
		}

		if ($controller instanceof TwoFactorChallengeController
			&& $this->userSession->getUser() !== null
			&& !$this->reflector->hasAnnotation('TwoFactorSetUpDoneRequired')) {
			$providers = $this->twoFactorManager->getProviderSet($this->userSession->getUser());

			if (!($providers->getPrimaryProviders() === [] && !$providers->isProviderMissing())) {
				throw new TwoFactorAuthRequiredException();
			}
		}

		if ($controller instanceof ALoginSetupController
			&& $this->userSession->getUser() !== null
			&& $this->twoFactorManager->needsSecondFactor($this->userSession->getUser())) {
			$providers = $this->twoFactorManager->getProviderSet($this->userSession->getUser());

			if ($providers->getPrimaryProviders() === [] && !$providers->isProviderMissing()) {
				return;
			}
		}

		if ($controller instanceof LoginController && $methodName === 'logout') {
			// Don't block the logout page, to allow canceling the 2FA
			return;
		}

		if ($this->userSession->isLoggedIn()) {
			$user = $this->userSession->getUser();

			if ($this->session->exists('app_password')  // authenticated using an app password
				|| $this->session->exists('app_api')  // authenticated using an AppAPI Auth
				|| $this->twoFactorManager->isTwoFactorAuthenticated($user)) {

				$this->checkTwoFactor($controller, $methodName, $user);
			} elseif ($controller instanceof TwoFactorChallengeController) {
				// Allow access to the two-factor controllers only if two-factor authentication
				// is in progress.
				throw new UserAlreadyLoggedInException();
			}
		}
		// TODO: dont check/enforce 2FA if a auth token is used
	}

	private function checkTwoFactor(Controller $controller, $methodName, IUser $user) {
		// If two-factor auth is in progress disallow access to any controllers
		// defined within "LoginController".
		$needsSecondFactor = $this->twoFactorManager->needsSecondFactor($user);
		$twoFactor = $controller instanceof TwoFactorChallengeController;

		// Disallow access to any controller if 2FA needs to be checked
		if ($needsSecondFactor && !$twoFactor) {
			throw new TwoFactorAuthRequiredException();
		}

		// Allow access to the two-factor controllers only if two-factor authentication
		// is in progress.
		if (!$needsSecondFactor && $twoFactor) {
			throw new UserAlreadyLoggedInException();
		}
	}

	public function afterException($controller, $methodName, Exception $exception) {
		if ($exception instanceof TwoFactorAuthRequiredException) {
			$params = [
				'redirect_url' => $this->request->getParam('redirect_url'),
			];
			if (!isset($params['redirect_url']) && isset($this->request->server['REQUEST_URI'])) {
				$params['redirect_url'] = $this->request->server['REQUEST_URI'];
			}
			return new RedirectResponse($this->urlGenerator->linkToRoute('core.TwoFactorChallenge.selectChallenge', $params));
		}
		if ($exception instanceof UserAlreadyLoggedInException) {
			return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index'));
		}

		throw $exception;
	}
}