Completed
Push — master ( f0158e...2060ff )
by Blizzz
50:16 queued 36:57
created

Manager   B

Complexity

Total Complexity 34

Size/Duplication

Total Lines 292
Duplicated Lines 5.14 %

Coupling/Cohesion

Components 1
Dependencies 15

Importance

Changes 0
Metric Value
dl 15
loc 292
rs 8.4332
c 0
b 0
f 0
wmc 34
lcom 1
cbo 15

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 15 15 1
A isTwoFactorAuthenticated() 0 4 2
A disableTwoFactorAuthentication() 0 3 1
A enableTwoFactorAuthentication() 0 3 1
A getProvider() 0 4 2
A getBackupProvider() 0 7 2
C getProviders() 0 30 7
A loadTwoFactorApp() 0 5 2
B verifyChallenge() 0 32 4
A publishEvent() 0 14 2
C needsSecondFactor() 0 50 9
A prepareTwoFactorLogin() 0 8 1

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
3
/**
4
 * @copyright Copyright (c) 2016, ownCloud, Inc.
5
 *
6
 * @author Christoph Wurst <[email protected]>
7
 *
8
 * @license AGPL-3.0
9
 *
10
 * This code is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License, version 3,
12
 * as published by the Free Software Foundation.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU Affero General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU Affero General Public License, version 3,
20
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21
 *
22
 */
23
24
namespace OC\Authentication\TwoFactorAuth;
25
26
use BadMethodCallException;
27
use Exception;
28
use OC;
29
use OC\App\AppManager;
30
use OC_App;
31
use OC\Authentication\Exceptions\InvalidTokenException;
32
use OC\Authentication\Token\IProvider as TokenProvider;
33
use OCP\Activity\IManager;
34
use OCP\AppFramework\QueryException;
35
use OCP\AppFramework\Utility\ITimeFactory;
36
use OCP\Authentication\TwoFactorAuth\IProvider;
37
use OCP\IConfig;
38
use OCP\ILogger;
39
use OCP\ISession;
40
use OCP\IUser;
41
42
class Manager {
43
44
	const SESSION_UID_KEY = 'two_factor_auth_uid';
45
	const SESSION_UID_DONE = 'two_factor_auth_passed';
46
	const BACKUP_CODES_APP_ID = 'twofactor_backupcodes';
47
	const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
48
	const REMEMBER_LOGIN = 'two_factor_remember_login';
49
50
	/** @var AppManager */
51
	private $appManager;
52
53
	/** @var ISession */
54
	private $session;
55
56
	/** @var IConfig */
57
	private $config;
58
59
	/** @var IManager */
60
	private $activityManager;
61
62
	/** @var ILogger */
63
	private $logger;
64
65
	/** @var TokenProvider */
66
	private $tokenProvider;
67
68
	/** @var ITimeFactory */
69
	private $timeFactory;
70
71
	/**
72
	 * @param AppManager $appManager
73
	 * @param ISession $session
74
	 * @param IConfig $config
75
	 * @param IManager $activityManager
76
	 * @param ILogger $logger
77
	 * @param TokenProvider $tokenProvider
78
	 * @param ITimeFactory $timeFactory
79
	 */
80 View Code Duplication
	public function __construct(AppManager $appManager,
81
								ISession $session,
82
								IConfig $config,
83
								IManager $activityManager,
84
								ILogger $logger,
85
								TokenProvider $tokenProvider,
86
								ITimeFactory $timeFactory) {
87
		$this->appManager = $appManager;
88
		$this->session = $session;
89
		$this->config = $config;
90
		$this->activityManager = $activityManager;
91
		$this->logger = $logger;
92
		$this->tokenProvider = $tokenProvider;
93
		$this->timeFactory = $timeFactory;
94
	}
95
96
	/**
97
	 * Determine whether the user must provide a second factor challenge
98
	 *
99
	 * @param IUser $user
100
	 * @return boolean
101
	 */
102
	public function isTwoFactorAuthenticated(IUser $user) {
103
		$twoFactorEnabled = ((int) $this->config->getUserValue($user->getUID(), 'core', 'two_factor_auth_disabled', 0)) === 0;
104
		return $twoFactorEnabled && count($this->getProviders($user)) > 0;
105
	}
106
107
	/**
108
	 * Disable 2FA checks for the given user
109
	 *
110
	 * @param IUser $user
111
	 */
112
	public function disableTwoFactorAuthentication(IUser $user) {
113
		$this->config->setUserValue($user->getUID(), 'core', 'two_factor_auth_disabled', 1);
114
	}
115
116
	/**
117
	 * Enable all 2FA checks for the given user
118
	 *
119
	 * @param IUser $user
120
	 */
121
	public function enableTwoFactorAuthentication(IUser $user) {
122
		$this->config->deleteUserValue($user->getUID(), 'core', 'two_factor_auth_disabled');
123
	}
124
125
	/**
126
	 * Get a 2FA provider by its ID
127
	 *
128
	 * @param IUser $user
129
	 * @param string $challengeProviderId
130
	 * @return IProvider|null
131
	 */
132
	public function getProvider(IUser $user, $challengeProviderId) {
133
		$providers = $this->getProviders($user, true);
134
		return isset($providers[$challengeProviderId]) ? $providers[$challengeProviderId] : null;
135
	}
136
137
	/**
138
	 * @param IUser $user
139
	 * @return IProvider|null the backup provider, if enabled for the given user
140
	 */
141
	public function getBackupProvider(IUser $user) {
142
		$providers = $this->getProviders($user, true);
143
		if (!isset($providers[self::BACKUP_CODES_PROVIDER_ID])) {
144
			return null;
145
		}
146
		return $providers[self::BACKUP_CODES_PROVIDER_ID];
147
	}
148
149
	/**
150
	 * Get the list of 2FA providers for the given user
151
	 *
152
	 * @param IUser $user
153
	 * @param bool $includeBackupApp
154
	 * @return IProvider[]
155
	 * @throws Exception
156
	 */
157
	public function getProviders(IUser $user, $includeBackupApp = false) {
158
		$allApps = $this->appManager->getEnabledAppsForUser($user);
159
		$providers = [];
160
161
		foreach ($allApps as $appId) {
162
			if (!$includeBackupApp && $appId === self::BACKUP_CODES_APP_ID) {
163
				continue;
164
			}
165
166
			$info = $this->appManager->getAppInfo($appId);
167
			if (isset($info['two-factor-providers'])) {
168
				$providerClasses = $info['two-factor-providers'];
169
				foreach ($providerClasses as $class) {
170
					try {
171
						$this->loadTwoFactorApp($appId);
172
						$provider = OC::$server->query($class);
173
						$providers[$provider->getId()] = $provider;
174
					} catch (QueryException $exc) {
175
						// Provider class can not be resolved
176
						throw new Exception("Could not load two-factor auth provider $class");
177
					}
178
				}
179
			}
180
		}
181
182
		return array_filter($providers, function ($provider) use ($user) {
183
			/* @var $provider IProvider */
184
			return $provider->isTwoFactorAuthEnabledForUser($user);
185
		});
186
	}
187
188
	/**
189
	 * Load an app by ID if it has not been loaded yet
190
	 *
191
	 * @param string $appId
192
	 */
193
	protected function loadTwoFactorApp($appId) {
194
		if (!OC_App::isAppLoaded($appId)) {
195
			OC_App::loadApp($appId);
196
		}
197
	}
198
199
	/**
200
	 * Verify the given challenge
201
	 *
202
	 * @param string $providerId
203
	 * @param IUser $user
204
	 * @param string $challenge
205
	 * @return boolean
206
	 */
207
	public function verifyChallenge($providerId, IUser $user, $challenge) {
208
		$provider = $this->getProvider($user, $providerId);
209
		if (is_null($provider)) {
210
			return false;
211
		}
212
213
		$passed = $provider->verifyChallenge($user, $challenge);
214
		if ($passed) {
215
			if ($this->session->get(self::REMEMBER_LOGIN) === true) {
216
				// TODO: resolve cyclic dependency and use DI
217
				\OC::$server->getUserSession()->createRememberMeToken($user);
218
			}
219
			$this->session->remove(self::SESSION_UID_KEY);
220
			$this->session->remove(self::REMEMBER_LOGIN);
221
			$this->session->set(self::SESSION_UID_DONE, $user->getUID());
222
223
			// Clear token from db
224
			$sessionId = $this->session->getId();
225
			$token = $this->tokenProvider->getToken($sessionId);
226
			$tokenId = $token->getId();
227
			$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $tokenId);
228
229
			$this->publishEvent($user, 'twofactor_success', [
230
				'provider' => $provider->getDisplayName(),
231
			]);
232
		} else {
233
			$this->publishEvent($user, 'twofactor_failed', [
234
				'provider' => $provider->getDisplayName(),
235
			]);
236
		}
237
		return $passed;
238
	}
239
240
	/**
241
	 * Push a 2fa event the user's activity stream
242
	 *
243
	 * @param IUser $user
244
	 * @param string $event
245
	 */
246
	private function publishEvent(IUser $user, $event, array $params) {
247
		$activity = $this->activityManager->generateEvent();
248
		$activity->setApp('core')
249
			->setType('security')
250
			->setAuthor($user->getUID())
251
			->setAffectedUser($user->getUID())
252
			->setSubject($event, $params);
253
		try {
254
			$this->activityManager->publish($activity);
255
		} catch (BadMethodCallException $e) {
256
			$this->logger->warning('could not publish backup code creation activity', ['app' => 'core']);
257
			$this->logger->logException($e, ['app' => 'core']);
258
		}
259
	}
260
261
	/**
262
	 * Check if the currently logged in user needs to pass 2FA
263
	 *
264
	 * @param IUser $user the currently logged in user
265
	 * @return boolean
266
	 */
267
	public function needsSecondFactor(IUser $user = null) {
268
		if ($user === null) {
269
			return false;
270
		}
271
272
		// First check if the session tells us we should do 2FA (99% case)
273
		if (!$this->session->exists(self::SESSION_UID_KEY)) {
274
275
			// Check if the session tells us it is 2FA authenticated already
276
			if ($this->session->exists(self::SESSION_UID_DONE) &&
277
				$this->session->get(self::SESSION_UID_DONE) === $user->getUID()) {
278
				return false;
279
			}
280
281
			/*
282
			 * If the session is expired check if we are not logged in by a token
283
			 * that still needs 2FA auth
284
			 */
285
			try {
286
				$sessionId = $this->session->getId();
287
				$token = $this->tokenProvider->getToken($sessionId);
288
				$tokenId = $token->getId();
289
				$tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
290
291
				if (!in_array($tokenId, $tokensNeeding2FA, true)) {
292
					$this->session->set(self::SESSION_UID_DONE, $user->getUID());
293
					return false;
294
				}
295
			} catch (InvalidTokenException $e) {
296
				return true;
297
			}
298
		}
299
300
301
		if (!$this->isTwoFactorAuthenticated($user)) {
302
			// There is no second factor any more -> let the user pass
303
			//   This prevents infinite redirect loops when a user is about
304
			//   to solve the 2FA challenge, and the provider app is
305
			//   disabled the same time
306
			$this->session->remove(self::SESSION_UID_KEY);
307
308
			$keys = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
309
			foreach ($keys as $key) {
310
				$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $key);
311
			}
312
			return false;
313
		}
314
315
		return true;
316
	}
317
318
	/**
319
	 * Prepare the 2FA login
320
	 *
321
	 * @param IUser $user
322
	 * @param boolean $rememberMe
323
	 */
324
	public function prepareTwoFactorLogin(IUser $user, $rememberMe) {
325
		$this->session->set(self::SESSION_UID_KEY, $user->getUID());
326
		$this->session->set(self::REMEMBER_LOGIN, $rememberMe);
327
328
		$id = $this->session->getId();
329
		$token = $this->tokenProvider->getToken($id);
330
		$this->config->setUserValue($user->getUID(), 'login_token_2fa', $token->getId(), $this->timeFactory->getTime());
331
	}
332
333
}
334