Completed
Push — master ( e3be9e...9444a3 )
by Morris
73:40 queued 52:27
created

Manager::loadTwoFactorApp()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
/**
5
 * @copyright Copyright (c) 2016, ownCloud, Inc.
6
 *
7
 * @author Christoph Wurst <[email protected]>
8
 * @author Lukas Reschke <[email protected]>
9
 * @author Robin Appelman <[email protected]>
10
 * @author Roeland Jago Douma <[email protected]>
11
 *
12
 * @license AGPL-3.0
13
 *
14
 * This code is free software: you can redistribute it and/or modify
15
 * it under the terms of the GNU Affero General Public License, version 3,
16
 * as published by the Free Software Foundation.
17
 *
18
 * This program is distributed in the hope that it will be useful,
19
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21
 * GNU Affero General Public License for more details.
22
 *
23
 * You should have received a copy of the GNU Affero General Public License, version 3,
24
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
25
 *
26
 */
27
28
namespace OC\Authentication\TwoFactorAuth;
29
30
use BadMethodCallException;
31
use Exception;
32
use OC\Authentication\Exceptions\InvalidTokenException;
33
use OC\Authentication\Token\IProvider as TokenProvider;
34
use OCP\Activity\IManager;
35
use OCP\AppFramework\Utility\ITimeFactory;
36
use OCP\Authentication\TwoFactorAuth\IProvider;
37
use OCP\Authentication\TwoFactorAuth\IRegistry;
38
use OCP\IConfig;
39
use OCP\ILogger;
40
use OCP\ISession;
41
use OCP\IUser;
42
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
43
use Symfony\Component\EventDispatcher\GenericEvent;
44
45
class Manager {
46
47
	const SESSION_UID_KEY = 'two_factor_auth_uid';
48
	const SESSION_UID_DONE = 'two_factor_auth_passed';
49
	const REMEMBER_LOGIN = 'two_factor_remember_login';
50
51
	/** @var ProviderLoader */
52
	private $providerLoader;
53
54
	/** @var IRegistry */
55
	private $providerRegistry;
56
57
	/** @var ISession */
58
	private $session;
59
60
	/** @var IConfig */
61
	private $config;
62
63
	/** @var IManager */
64
	private $activityManager;
65
66
	/** @var ILogger */
67
	private $logger;
68
69
	/** @var TokenProvider */
70
	private $tokenProvider;
71
72
	/** @var ITimeFactory */
73
	private $timeFactory;
74
75
	/** @var EventDispatcherInterface */
76
	private $dispatcher;
77
78 View Code Duplication
	public function __construct(ProviderLoader $providerLoader,
79
		IRegistry $providerRegistry, ISession $session, IConfig $config,
80
		IManager $activityManager, ILogger $logger, TokenProvider $tokenProvider,
81
		ITimeFactory $timeFactory, EventDispatcherInterface $eventDispatcher) {
82
		$this->providerLoader = $providerLoader;
83
		$this->session = $session;
84
		$this->config = $config;
85
		$this->activityManager = $activityManager;
86
		$this->logger = $logger;
87
		$this->tokenProvider = $tokenProvider;
88
		$this->timeFactory = $timeFactory;
89
		$this->dispatcher = $eventDispatcher;
90
		$this->providerRegistry = $providerRegistry;
91
	}
92
93
	/**
94
	 * Determine whether the user must provide a second factor challenge
95
	 *
96
	 * @param IUser $user
97
	 * @return boolean
98
	 */
99
	public function isTwoFactorAuthenticated(IUser $user): bool {
100
		$twoFactorEnabled = ((int) $this->config->getUserValue($user->getUID(), 'core', 'two_factor_auth_disabled', 0)) === 0;
101
102
		if (!$twoFactorEnabled) {
103
			return false;
104
		}
105
106
		$providerStates = $this->providerRegistry->getProviderStates($user);
107
		$enabled = array_filter($providerStates);
108
109
		return $twoFactorEnabled && !empty($enabled);
110
	}
111
112
	/**
113
	 * Disable 2FA checks for the given user
114
	 *
115
	 * @param IUser $user
116
	 */
117
	public function disableTwoFactorAuthentication(IUser $user) {
118
		$this->config->setUserValue($user->getUID(), 'core', 'two_factor_auth_disabled', 1);
119
	}
120
121
	/**
122
	 * Enable all 2FA checks for the given user
123
	 *
124
	 * @param IUser $user
125
	 */
126
	public function enableTwoFactorAuthentication(IUser $user) {
127
		$this->config->deleteUserValue($user->getUID(), 'core', 'two_factor_auth_disabled');
128
	}
129
130
	/**
131
	 * Get a 2FA provider by its ID
132
	 *
133
	 * @param IUser $user
134
	 * @param string $challengeProviderId
135
	 * @return IProvider|null
136
	 */
137
	public function getProvider(IUser $user, string $challengeProviderId) {
138
		$providers = $this->getProviderSet($user)->getProviders();
139
		return $providers[$challengeProviderId] ?? null;
140
	}
141
142
	/**
143
	 * Check if the persistant mapping of enabled/disabled state of each available
144
	 * provider is missing an entry and add it to the registry in that case.
145
	 *
146
	 * @todo remove in Nextcloud 17 as by then all providers should have been updated
147
	 *
148
	 * @param string[] $providerStates
149
	 * @param IProvider[] $providers
150
	 * @param IUser $user
151
	 * @return string[] the updated $providerStates variable
152
	 */
153
	private function fixMissingProviderStates(array $providerStates,
154
		array $providers, IUser $user): array {
155
156
		foreach ($providers as $provider) {
157
			if (isset($providerStates[$provider->getId()])) {
158
				// All good
159
				continue;
160
			}
161
162
			$enabled = $provider->isTwoFactorAuthEnabledForUser($user);
163
			if ($enabled) {
164
				$this->providerRegistry->enableProviderFor($provider, $user);
165
			} else {
166
				$this->providerRegistry->disableProviderFor($provider, $user);
167
			}
168
			$providerStates[$provider->getId()] = $enabled;
169
		}
170
171
		return $providerStates;
172
	}
173
174
	/**
175
	 * @param array $states
176
	 * @param IProvider $providers
177
	 */
178
	private function isProviderMissing(array $states, array $providers): bool {
179
		$indexed = [];
180
		foreach ($providers as $provider) {
181
			$indexed[$provider->getId()] = $provider;
182
		}
183
184
		$missing = [];
185
		foreach ($states as $providerId => $enabled) {
186
			if (!$enabled) {
187
				// Don't care
188
				continue;
189
			}
190
191
			if (!isset($indexed[$providerId])) {
192
				$missing[] = $providerId;
193
				$this->logger->alert("two-factor auth provider '$providerId' failed to load",
194
					[
195
					'app' => 'core',
196
				]);
197
			}
198
		}
199
200
		if (!empty($missing)) {
201
			// There was at least one provider missing
202
			$this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']);
203
204
			return true;
205
		}
206
207
		// If we reach this, there was not a single provider missing
208
		return false;
209
	}
210
211
	/**
212
	 * Get the list of 2FA providers for the given user
213
	 *
214
	 * @param IUser $user
215
	 * @throws Exception
216
	 */
217
	public function getProviderSet(IUser $user): ProviderSet {
218
		$providerStates = $this->providerRegistry->getProviderStates($user);
219
		$providers = $this->providerLoader->getProviders($user);
220
221
		$fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
222
		$isProviderMissing = $this->isProviderMissing($fixedStates, $providers);
223
224
		$enabled = array_filter($providers, function (IProvider $provider) use ($fixedStates) {
225
			return $fixedStates[$provider->getId()];
226
		});
227
		return new ProviderSet($enabled, $isProviderMissing);
228
	}
229
230
	/**
231
	 * Verify the given challenge
232
	 *
233
	 * @param string $providerId
234
	 * @param IUser $user
235
	 * @param string $challenge
236
	 * @return boolean
237
	 */
238
	public function verifyChallenge(string $providerId, IUser $user, string $challenge): bool {
239
		$provider = $this->getProvider($user, $providerId);
240
		if ($provider === null) {
241
			return false;
242
		}
243
244
		$passed = $provider->verifyChallenge($user, $challenge);
245
		if ($passed) {
246
			if ($this->session->get(self::REMEMBER_LOGIN) === true) {
247
				// TODO: resolve cyclic dependency and use DI
248
				\OC::$server->getUserSession()->createRememberMeToken($user);
249
			}
250
			$this->session->remove(self::SESSION_UID_KEY);
251
			$this->session->remove(self::REMEMBER_LOGIN);
252
			$this->session->set(self::SESSION_UID_DONE, $user->getUID());
253
254
			// Clear token from db
255
			$sessionId = $this->session->getId();
256
			$token = $this->tokenProvider->getToken($sessionId);
257
			$tokenId = $token->getId();
258
			$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $tokenId);
259
260
			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
261
			$this->dispatcher->dispatch(IProvider::EVENT_SUCCESS, $dispatchEvent);
262
263
			$this->publishEvent($user, 'twofactor_success', [
264
				'provider' => $provider->getDisplayName(),
265
			]);
266
		} else {
267
			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
268
			$this->dispatcher->dispatch(IProvider::EVENT_FAILED, $dispatchEvent);
269
270
			$this->publishEvent($user, 'twofactor_failed', [
271
				'provider' => $provider->getDisplayName(),
272
			]);
273
		}
274
		return $passed;
275
	}
276
277
	/**
278
	 * Push a 2fa event the user's activity stream
279
	 *
280
	 * @param IUser $user
281
	 * @param string $event
282
	 * @param array $params
283
	 */
284
	private function publishEvent(IUser $user, string $event, array $params) {
285
		$activity = $this->activityManager->generateEvent();
286
		$activity->setApp('core')
287
			->setType('security')
288
			->setAuthor($user->getUID())
289
			->setAffectedUser($user->getUID())
290
			->setSubject($event, $params);
291
		try {
292
			$this->activityManager->publish($activity);
293
		} catch (BadMethodCallException $e) {
294
			$this->logger->warning('could not publish activity', ['app' => 'core']);
295
			$this->logger->logException($e, ['app' => 'core']);
0 ignored issues
show
Documentation introduced by
$e is of type object<BadMethodCallException>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
296
		}
297
	}
298
299
	/**
300
	 * Check if the currently logged in user needs to pass 2FA
301
	 *
302
	 * @param IUser $user the currently logged in user
303
	 * @return boolean
304
	 */
305
	public function needsSecondFactor(IUser $user = null): bool {
306
		if ($user === null) {
307
			return false;
308
		}
309
310
		// If we are authenticated using an app password skip all this
311
		if ($this->session->exists('app_password')) {
312
			return false;
313
		}
314
315
		// First check if the session tells us we should do 2FA (99% case)
316
		if (!$this->session->exists(self::SESSION_UID_KEY)) {
317
318
			// Check if the session tells us it is 2FA authenticated already
319
			if ($this->session->exists(self::SESSION_UID_DONE) &&
320
				$this->session->get(self::SESSION_UID_DONE) === $user->getUID()) {
321
				return false;
322
			}
323
324
			/*
325
			 * If the session is expired check if we are not logged in by a token
326
			 * that still needs 2FA auth
327
			 */
328
			try {
329
				$sessionId = $this->session->getId();
330
				$token = $this->tokenProvider->getToken($sessionId);
331
				$tokenId = $token->getId();
332
				$tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
333
334
				if (!\in_array($tokenId, $tokensNeeding2FA, true)) {
335
					$this->session->set(self::SESSION_UID_DONE, $user->getUID());
336
					return false;
337
				}
338
			} catch (InvalidTokenException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
339
			}
340
		}
341
342
		if (!$this->isTwoFactorAuthenticated($user)) {
343
			// There is no second factor any more -> let the user pass
344
			//   This prevents infinite redirect loops when a user is about
345
			//   to solve the 2FA challenge, and the provider app is
346
			//   disabled the same time
347
			$this->session->remove(self::SESSION_UID_KEY);
348
349
			$keys = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
350
			foreach ($keys as $key) {
351
				$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $key);
352
			}
353
			return false;
354
		}
355
356
		return true;
357
	}
358
359
	/**
360
	 * Prepare the 2FA login
361
	 *
362
	 * @param IUser $user
363
	 * @param boolean $rememberMe
364
	 */
365
	public function prepareTwoFactorLogin(IUser $user, bool $rememberMe) {
366
		$this->session->set(self::SESSION_UID_KEY, $user->getUID());
367
		$this->session->set(self::REMEMBER_LOGIN, $rememberMe);
368
369
		$id = $this->session->getId();
370
		$token = $this->tokenProvider->getToken($id);
371
		$this->config->setUserValue($user->getUID(), 'login_token_2fa', $token->getId(), $this->timeFactory->getTime());
372
	}
373
374
}
375