Passed
Push — master ( 050b91...deb7d2 )
by Roeland
10:46 queued 11s
created

Manager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 10
nc 1
nop 10
dl 0
loc 16
rs 9.9332
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 function array_diff;
31
use function array_filter;
32
use BadMethodCallException;
33
use Exception;
34
use OC\Authentication\Exceptions\ExpiredTokenException;
35
use OC\Authentication\Exceptions\InvalidTokenException;
36
use OC\Authentication\Token\IProvider as TokenProvider;
37
use OCP\Activity\IManager;
38
use OCP\AppFramework\Utility\ITimeFactory;
39
use OCP\Authentication\TwoFactorAuth\IProvider;
40
use OCP\Authentication\TwoFactorAuth\IRegistry;
41
use OCP\IConfig;
42
use OCP\ILogger;
43
use OCP\ISession;
44
use OCP\IUser;
45
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
46
use Symfony\Component\EventDispatcher\GenericEvent;
47
48
class Manager {
49
50
	const SESSION_UID_KEY = 'two_factor_auth_uid';
51
	const SESSION_UID_DONE = 'two_factor_auth_passed';
52
	const REMEMBER_LOGIN = 'two_factor_remember_login';
53
	const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
54
55
	/** @var ProviderLoader */
56
	private $providerLoader;
57
58
	/** @var IRegistry */
59
	private $providerRegistry;
60
61
	/** @var MandatoryTwoFactor */
62
	private $mandatoryTwoFactor;
63
64
	/** @var ISession */
65
	private $session;
66
67
	/** @var IConfig */
68
	private $config;
69
70
	/** @var IManager */
71
	private $activityManager;
72
73
	/** @var ILogger */
74
	private $logger;
75
76
	/** @var TokenProvider */
77
	private $tokenProvider;
78
79
	/** @var ITimeFactory */
80
	private $timeFactory;
81
82
	/** @var EventDispatcherInterface */
83
	private $dispatcher;
84
85
	public function __construct(ProviderLoader $providerLoader,
86
								IRegistry $providerRegistry,
87
								MandatoryTwoFactor $mandatoryTwoFactor,
88
								ISession $session, IConfig $config,
89
								IManager $activityManager, ILogger $logger, TokenProvider $tokenProvider,
90
								ITimeFactory $timeFactory, EventDispatcherInterface $eventDispatcher) {
91
		$this->providerLoader = $providerLoader;
92
		$this->providerRegistry = $providerRegistry;
93
		$this->mandatoryTwoFactor = $mandatoryTwoFactor;
94
		$this->session = $session;
95
		$this->config = $config;
96
		$this->activityManager = $activityManager;
97
		$this->logger = $logger;
98
		$this->tokenProvider = $tokenProvider;
99
		$this->timeFactory = $timeFactory;
100
		$this->dispatcher = $eventDispatcher;
101
	}
102
103
	/**
104
	 * Determine whether the user must provide a second factor challenge
105
	 *
106
	 * @param IUser $user
107
	 * @return boolean
108
	 */
109
	public function isTwoFactorAuthenticated(IUser $user): bool {
110
		if ($this->mandatoryTwoFactor->isEnforcedFor($user)) {
111
			return true;
112
		}
113
114
		$providerStates = $this->providerRegistry->getProviderStates($user);
115
		$providers = $this->providerLoader->getProviders($user);
116
		$fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
117
		$enabled = array_filter($fixedStates);
118
		$providerIds = array_keys($enabled);
119
		$providerIdsWithoutBackupCodes = array_diff($providerIds, [self::BACKUP_CODES_PROVIDER_ID]);
120
121
		return !empty($providerIdsWithoutBackupCodes);
122
	}
123
124
	/**
125
	 * Get a 2FA provider by its ID
126
	 *
127
	 * @param IUser $user
128
	 * @param string $challengeProviderId
129
	 * @return IProvider|null
130
	 */
131
	public function getProvider(IUser $user, string $challengeProviderId) {
132
		$providers = $this->getProviderSet($user)->getProviders();
133
		return $providers[$challengeProviderId] ?? null;
134
	}
135
136
	/**
137
	 * Check if the persistant mapping of enabled/disabled state of each available
138
	 * provider is missing an entry and add it to the registry in that case.
139
	 *
140
	 * @todo remove in Nextcloud 17 as by then all providers should have been updated
141
	 *
142
	 * @param string[] $providerStates
143
	 * @param IProvider[] $providers
144
	 * @param IUser $user
145
	 * @return string[] the updated $providerStates variable
146
	 */
147
	private function fixMissingProviderStates(array $providerStates,
148
		array $providers, IUser $user): array {
149
150
		foreach ($providers as $provider) {
151
			if (isset($providerStates[$provider->getId()])) {
152
				// All good
153
				continue;
154
			}
155
156
			$enabled = $provider->isTwoFactorAuthEnabledForUser($user);
157
			if ($enabled) {
158
				$this->providerRegistry->enableProviderFor($provider, $user);
159
			} else {
160
				$this->providerRegistry->disableProviderFor($provider, $user);
161
			}
162
			$providerStates[$provider->getId()] = $enabled;
163
		}
164
165
		return $providerStates;
166
	}
167
168
	/**
169
	 * @param array $states
170
	 * @param IProvider $providers
171
	 */
172
	private function isProviderMissing(array $states, array $providers): bool {
173
		$indexed = [];
174
		foreach ($providers as $provider) {
175
			$indexed[$provider->getId()] = $provider;
176
		}
177
178
		$missing = [];
179
		foreach ($states as $providerId => $enabled) {
180
			if (!$enabled) {
181
				// Don't care
182
				continue;
183
			}
184
185
			if (!isset($indexed[$providerId])) {
186
				$missing[] = $providerId;
187
				$this->logger->alert("two-factor auth provider '$providerId' failed to load",
188
					[
189
					'app' => 'core',
190
				]);
191
			}
192
		}
193
194
		if (!empty($missing)) {
195
			// There was at least one provider missing
196
			$this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']);
197
198
			return true;
199
		}
200
201
		// If we reach this, there was not a single provider missing
202
		return false;
203
	}
204
205
	/**
206
	 * Get the list of 2FA providers for the given user
207
	 *
208
	 * @param IUser $user
209
	 * @throws Exception
210
	 */
211
	public function getProviderSet(IUser $user): ProviderSet {
212
		$providerStates = $this->providerRegistry->getProviderStates($user);
213
		$providers = $this->providerLoader->getProviders($user);
214
215
		$fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
216
		$isProviderMissing = $this->isProviderMissing($fixedStates, $providers);
217
218
		$enabled = array_filter($providers, function (IProvider $provider) use ($fixedStates) {
219
			return $fixedStates[$provider->getId()];
220
		});
221
		return new ProviderSet($enabled, $isProviderMissing);
222
	}
223
224
	/**
225
	 * Verify the given challenge
226
	 *
227
	 * @param string $providerId
228
	 * @param IUser $user
229
	 * @param string $challenge
230
	 * @return boolean
231
	 */
232
	public function verifyChallenge(string $providerId, IUser $user, string $challenge): bool {
233
		$provider = $this->getProvider($user, $providerId);
234
		if ($provider === null) {
235
			return false;
236
		}
237
238
		$passed = $provider->verifyChallenge($user, $challenge);
239
		if ($passed) {
240
			if ($this->session->get(self::REMEMBER_LOGIN) === true) {
241
				// TODO: resolve cyclic dependency and use DI
242
				\OC::$server->getUserSession()->createRememberMeToken($user);
243
			}
244
			$this->session->remove(self::SESSION_UID_KEY);
245
			$this->session->remove(self::REMEMBER_LOGIN);
246
			$this->session->set(self::SESSION_UID_DONE, $user->getUID());
247
248
			// Clear token from db
249
			$sessionId = $this->session->getId();
250
			$token = $this->tokenProvider->getToken($sessionId);
251
			$tokenId = $token->getId();
252
			$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $tokenId);
253
254
			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
255
			$this->dispatcher->dispatch(IProvider::EVENT_SUCCESS, $dispatchEvent);
256
257
			$this->publishEvent($user, 'twofactor_success', [
258
				'provider' => $provider->getDisplayName(),
259
			]);
260
		} else {
261
			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
262
			$this->dispatcher->dispatch(IProvider::EVENT_FAILED, $dispatchEvent);
263
264
			$this->publishEvent($user, 'twofactor_failed', [
265
				'provider' => $provider->getDisplayName(),
266
			]);
267
		}
268
		return $passed;
269
	}
270
271
	/**
272
	 * Push a 2fa event the user's activity stream
273
	 *
274
	 * @param IUser $user
275
	 * @param string $event
276
	 * @param array $params
277
	 */
278
	private function publishEvent(IUser $user, string $event, array $params) {
279
		$activity = $this->activityManager->generateEvent();
280
		$activity->setApp('core')
281
			->setType('security')
282
			->setAuthor($user->getUID())
283
			->setAffectedUser($user->getUID())
284
			->setSubject($event, $params);
285
		try {
286
			$this->activityManager->publish($activity);
287
		} catch (BadMethodCallException $e) {
288
			$this->logger->warning('could not publish activity', ['app' => 'core']);
289
			$this->logger->logException($e, ['app' => 'core']);
290
		}
291
	}
292
293
	/**
294
	 * Check if the currently logged in user needs to pass 2FA
295
	 *
296
	 * @param IUser $user the currently logged in user
297
	 * @return boolean
298
	 */
299
	public function needsSecondFactor(IUser $user = null): bool {
300
		if ($user === null) {
301
			return false;
302
		}
303
304
		// If we are authenticated using an app password skip all this
305
		if ($this->session->exists('app_password')) {
306
			return false;
307
		}
308
309
		// First check if the session tells us we should do 2FA (99% case)
310
		if (!$this->session->exists(self::SESSION_UID_KEY)) {
311
312
			// Check if the session tells us it is 2FA authenticated already
313
			if ($this->session->exists(self::SESSION_UID_DONE) &&
314
				$this->session->get(self::SESSION_UID_DONE) === $user->getUID()) {
315
				return false;
316
			}
317
318
			/*
319
			 * If the session is expired check if we are not logged in by a token
320
			 * that still needs 2FA auth
321
			 */
322
			try {
323
				$sessionId = $this->session->getId();
324
				$token = $this->tokenProvider->getToken($sessionId);
325
				$tokenId = $token->getId();
326
				$tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
327
328
				if (!\in_array($tokenId, $tokensNeeding2FA, true)) {
329
					$this->session->set(self::SESSION_UID_DONE, $user->getUID());
330
					return false;
331
				}
332
			} catch (InvalidTokenException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
333
			}
334
		}
335
336
		if (!$this->isTwoFactorAuthenticated($user)) {
337
			// There is no second factor any more -> let the user pass
338
			//   This prevents infinite redirect loops when a user is about
339
			//   to solve the 2FA challenge, and the provider app is
340
			//   disabled the same time
341
			$this->session->remove(self::SESSION_UID_KEY);
342
343
			$keys = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
344
			foreach ($keys as $key) {
345
				$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $key);
346
			}
347
			return false;
348
		}
349
350
		return true;
351
	}
352
353
	/**
354
	 * Prepare the 2FA login
355
	 *
356
	 * @param IUser $user
357
	 * @param boolean $rememberMe
358
	 */
359
	public function prepareTwoFactorLogin(IUser $user, bool $rememberMe) {
360
		$this->session->set(self::SESSION_UID_KEY, $user->getUID());
361
		$this->session->set(self::REMEMBER_LOGIN, $rememberMe);
362
363
		$id = $this->session->getId();
364
		$token = $this->tokenProvider->getToken($id);
365
		$this->config->setUserValue($user->getUID(), 'login_token_2fa', $token->getId(), $this->timeFactory->getTime());
366
	}
367
368
	public function clearTwoFactorPending(string $userId) {
369
		$tokensNeeding2FA = $this->config->getUserKeys($userId, 'login_token_2fa');
370
371
		foreach ($tokensNeeding2FA as $tokenId) {
372
			$this->tokenProvider->invalidateTokenById($userId, $tokenId);
0 ignored issues
show
Bug introduced by
$tokenId of type string is incompatible with the type integer expected by parameter $id of OC\Authentication\Token\...::invalidateTokenById(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

372
			$this->tokenProvider->invalidateTokenById($userId, /** @scrutinizer ignore-type */ $tokenId);
Loading history...
373
		}
374
	}
375
376
}
377