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

Manager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 14
Ratio 100 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 9
dl 14
loc 14
rs 9.7998
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 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