Completed
Push — stable13 ( 9b96db...7edc8c )
by Morris
13:16
created

Manager::disableTwoFactorAuthentication()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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...
344
		}
345
	}
346
347
}
348