Completed
Push — master ( e36d4a...8177fd )
by Morris
16:38
created
settings/Controller/TwoFactorSettingsController.php 1 patch
Indentation   +17 added lines, -17 removed lines patch added patch discarded remove patch
@@ -34,27 +34,27 @@
 block discarded – undo
34 34
 
35 35
 class TwoFactorSettingsController extends Controller {
36 36
 
37
-	/** @var MandatoryTwoFactor */
38
-	private $mandatoryTwoFactor;
37
+    /** @var MandatoryTwoFactor */
38
+    private $mandatoryTwoFactor;
39 39
 
40
-	public function __construct(string $appName,
41
-								IRequest $request,
42
-								MandatoryTwoFactor $mandatoryTwoFactor) {
43
-		parent::__construct($appName, $request);
40
+    public function __construct(string $appName,
41
+                                IRequest $request,
42
+                                MandatoryTwoFactor $mandatoryTwoFactor) {
43
+        parent::__construct($appName, $request);
44 44
 
45
-		$this->mandatoryTwoFactor = $mandatoryTwoFactor;
46
-	}
45
+        $this->mandatoryTwoFactor = $mandatoryTwoFactor;
46
+    }
47 47
 
48
-	public function index(): JSONResponse {
49
-		return new JSONResponse($this->mandatoryTwoFactor->getState());
50
-	}
48
+    public function index(): JSONResponse {
49
+        return new JSONResponse($this->mandatoryTwoFactor->getState());
50
+    }
51 51
 
52
-	public function update(bool $enforced, array $enforcedGroups = [], array $excludedGroups = []): JSONResponse {
53
-		$this->mandatoryTwoFactor->setState(
54
-			new EnforcementState($enforced, $enforcedGroups, $excludedGroups)
55
-		);
52
+    public function update(bool $enforced, array $enforcedGroups = [], array $excludedGroups = []): JSONResponse {
53
+        $this->mandatoryTwoFactor->setState(
54
+            new EnforcementState($enforced, $enforcedGroups, $excludedGroups)
55
+        );
56 56
 
57
-		return new JSONResponse($this->mandatoryTwoFactor->getState());
58
-	}
57
+        return new JSONResponse($this->mandatoryTwoFactor->getState());
58
+    }
59 59
 
60 60
 }
61 61
\ No newline at end of file
Please login to merge, or discard this patch.
lib/private/Authentication/TwoFactorAuth/EnforcementState.php 1 patch
Indentation   +45 added lines, -45 removed lines patch added patch discarded remove patch
@@ -29,57 +29,57 @@
 block discarded – undo
29 29
 
30 30
 class EnforcementState implements JsonSerializable {
31 31
 
32
-	/** @var bool */
33
-	private $enforced;
32
+    /** @var bool */
33
+    private $enforced;
34 34
 
35
-	/** @var array */
36
-	private $enforcedGroups;
35
+    /** @var array */
36
+    private $enforcedGroups;
37 37
 
38
-	/** @var array */
39
-	private $excludedGroups;
38
+    /** @var array */
39
+    private $excludedGroups;
40 40
 
41
-	/**
42
-	 * EnforcementState constructor.
43
-	 *
44
-	 * @param bool $enforced
45
-	 * @param string[] $enforcedGroups
46
-	 * @param string[] $excludedGroups
47
-	 */
48
-	public function __construct(bool $enforced,
49
-								array $enforcedGroups = [],
50
-								array $excludedGroups = []) {
51
-		$this->enforced = $enforced;
52
-		$this->enforcedGroups = $enforcedGroups;
53
-		$this->excludedGroups = $excludedGroups;
54
-	}
41
+    /**
42
+     * EnforcementState constructor.
43
+     *
44
+     * @param bool $enforced
45
+     * @param string[] $enforcedGroups
46
+     * @param string[] $excludedGroups
47
+     */
48
+    public function __construct(bool $enforced,
49
+                                array $enforcedGroups = [],
50
+                                array $excludedGroups = []) {
51
+        $this->enforced = $enforced;
52
+        $this->enforcedGroups = $enforcedGroups;
53
+        $this->excludedGroups = $excludedGroups;
54
+    }
55 55
 
56
-	/**
57
-	 * @return string[]
58
-	 */
59
-	public function isEnforced(): bool {
60
-		return $this->enforced;
61
-	}
56
+    /**
57
+     * @return string[]
58
+     */
59
+    public function isEnforced(): bool {
60
+        return $this->enforced;
61
+    }
62 62
 
63
-	/**
64
-	 * @return string[]
65
-	 */
66
-	public function getEnforcedGroups(): array {
67
-		return $this->enforcedGroups;
68
-	}
63
+    /**
64
+     * @return string[]
65
+     */
66
+    public function getEnforcedGroups(): array {
67
+        return $this->enforcedGroups;
68
+    }
69 69
 
70
-	/**
71
-	 * @return string[]
72
-	 */
73
-	public function getExcludedGroups(): array {
74
-		return $this->excludedGroups;
75
-	}
70
+    /**
71
+     * @return string[]
72
+     */
73
+    public function getExcludedGroups(): array {
74
+        return $this->excludedGroups;
75
+    }
76 76
 
77
-	public function jsonSerialize(): array {
78
-		return [
79
-			'enforced' => $this->enforced,
80
-			'enforcedGroups' => $this->enforcedGroups,
81
-			'excludedGroups' => $this->excludedGroups,
82
-		];
83
-	}
77
+    public function jsonSerialize(): array {
78
+        return [
79
+            'enforced' => $this->enforced,
80
+            'enforcedGroups' => $this->enforcedGroups,
81
+            'excludedGroups' => $this->excludedGroups,
82
+        ];
83
+    }
84 84
 
85 85
 }
Please login to merge, or discard this patch.
lib/private/Authentication/TwoFactorAuth/MandatoryTwoFactor.php 1 patch
Indentation   +75 added lines, -75 removed lines patch added patch discarded remove patch
@@ -32,84 +32,84 @@
 block discarded – undo
32 32
 
33 33
 class MandatoryTwoFactor {
34 34
 
35
-	/** @var IConfig */
36
-	private $config;
37
-
38
-	/** @var IGroupManager */
39
-	private $groupManager;
40
-
41
-	public function __construct(IConfig $config, IGroupManager $groupManager) {
42
-		$this->config = $config;
43
-		$this->groupManager = $groupManager;
44
-	}
45
-
46
-	/**
47
-	 * Get the state of enforced two-factor auth
48
-	 */
49
-	public function getState(): EnforcementState {
50
-		return new EnforcementState(
51
-			$this->config->getSystemValue('twofactor_enforced', 'false') === 'true',
52
-			$this->config->getSystemValue('twofactor_enforced_groups', []),
53
-			$this->config->getSystemValue('twofactor_enforced_excluded_groups', [])
54
-		);
55
-	}
56
-
57
-	/**
58
-	 * Set the state of enforced two-factor auth
59
-	 */
60
-	public function setState(EnforcementState $state) {
61
-		$this->config->setSystemValue('twofactor_enforced', $state->isEnforced() ? 'true' : 'false');
62
-		$this->config->setSystemValue('twofactor_enforced_groups', $state->getEnforcedGroups());
63
-		$this->config->setSystemValue('twofactor_enforced_excluded_groups', $state->getExcludedGroups());
64
-	}
65
-
66
-	/**
67
-	 * Check if two-factor auth is enforced for a specific user
68
-	 *
69
-	 * The admin(s) can enforce two-factor auth system-wide, for certain groups only
70
-	 * and also have the option to exclude users of certain groups. This method will
71
-	 * check their membership of those groups.
72
-	 *
73
-	 * @param IUser $user
74
-	 *
75
-	 * @return bool
76
-	 */
77
-	public function isEnforcedFor(IUser $user): bool {
78
-		$state = $this->getState();
79
-		if (!$state->isEnforced()) {
80
-			return false;
81
-		}
82
-		$uid = $user->getUID();
83
-
84
-		/*
35
+    /** @var IConfig */
36
+    private $config;
37
+
38
+    /** @var IGroupManager */
39
+    private $groupManager;
40
+
41
+    public function __construct(IConfig $config, IGroupManager $groupManager) {
42
+        $this->config = $config;
43
+        $this->groupManager = $groupManager;
44
+    }
45
+
46
+    /**
47
+     * Get the state of enforced two-factor auth
48
+     */
49
+    public function getState(): EnforcementState {
50
+        return new EnforcementState(
51
+            $this->config->getSystemValue('twofactor_enforced', 'false') === 'true',
52
+            $this->config->getSystemValue('twofactor_enforced_groups', []),
53
+            $this->config->getSystemValue('twofactor_enforced_excluded_groups', [])
54
+        );
55
+    }
56
+
57
+    /**
58
+     * Set the state of enforced two-factor auth
59
+     */
60
+    public function setState(EnforcementState $state) {
61
+        $this->config->setSystemValue('twofactor_enforced', $state->isEnforced() ? 'true' : 'false');
62
+        $this->config->setSystemValue('twofactor_enforced_groups', $state->getEnforcedGroups());
63
+        $this->config->setSystemValue('twofactor_enforced_excluded_groups', $state->getExcludedGroups());
64
+    }
65
+
66
+    /**
67
+     * Check if two-factor auth is enforced for a specific user
68
+     *
69
+     * The admin(s) can enforce two-factor auth system-wide, for certain groups only
70
+     * and also have the option to exclude users of certain groups. This method will
71
+     * check their membership of those groups.
72
+     *
73
+     * @param IUser $user
74
+     *
75
+     * @return bool
76
+     */
77
+    public function isEnforcedFor(IUser $user): bool {
78
+        $state = $this->getState();
79
+        if (!$state->isEnforced()) {
80
+            return false;
81
+        }
82
+        $uid = $user->getUID();
83
+
84
+        /*
85 85
 		 * If there is a list of enforced groups, we only enforce 2FA for members of those groups.
86 86
 		 * For all the other users it is not enforced (overruling the excluded groups list).
87 87
 		 */
88
-		if (!empty($state->getEnforcedGroups())) {
89
-			foreach ($state->getEnforcedGroups() as $group) {
90
-				if ($this->groupManager->isInGroup($uid, $group)) {
91
-					return true;
92
-				}
93
-			}
94
-			// Not a member of any of these groups -> no 2FA enforced
95
-			return false;
96
-		}
97
-
98
-		/**
99
-		 * If the user is member of an excluded group, 2FA won't be enforced.
100
-		 */
101
-		foreach ($state->getExcludedGroups() as $group) {
102
-			if ($this->groupManager->isInGroup($uid, $group)) {
103
-				return false;
104
-			}
105
-		}
106
-
107
-		/**
108
-		 * No enforced groups configured and user not member of an excluded groups,
109
-		 * so 2FA is enforced.
110
-		 */
111
-		return true;
112
-	}
88
+        if (!empty($state->getEnforcedGroups())) {
89
+            foreach ($state->getEnforcedGroups() as $group) {
90
+                if ($this->groupManager->isInGroup($uid, $group)) {
91
+                    return true;
92
+                }
93
+            }
94
+            // Not a member of any of these groups -> no 2FA enforced
95
+            return false;
96
+        }
97
+
98
+        /**
99
+         * If the user is member of an excluded group, 2FA won't be enforced.
100
+         */
101
+        foreach ($state->getExcludedGroups() as $group) {
102
+            if ($this->groupManager->isInGroup($uid, $group)) {
103
+                return false;
104
+            }
105
+        }
106
+
107
+        /**
108
+         * No enforced groups configured and user not member of an excluded groups,
109
+         * so 2FA is enforced.
110
+         */
111
+        return true;
112
+    }
113 113
 
114 114
 
115 115
 }
Please login to merge, or discard this patch.
lib/private/Authentication/TwoFactorAuth/Manager.php 1 patch
Indentation   +314 added lines, -314 removed lines patch added patch discarded remove patch
@@ -46,322 +46,322 @@
 block discarded – undo
46 46
 
47 47
 class Manager {
48 48
 
49
-	const SESSION_UID_KEY = 'two_factor_auth_uid';
50
-	const SESSION_UID_DONE = 'two_factor_auth_passed';
51
-	const REMEMBER_LOGIN = 'two_factor_remember_login';
52
-	const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
53
-
54
-	/** @var ProviderLoader */
55
-	private $providerLoader;
56
-
57
-	/** @var IRegistry */
58
-	private $providerRegistry;
59
-
60
-	/** @var MandatoryTwoFactor */
61
-	private $mandatoryTwoFactor;
62
-
63
-	/** @var ISession */
64
-	private $session;
65
-
66
-	/** @var IConfig */
67
-	private $config;
68
-
69
-	/** @var IManager */
70
-	private $activityManager;
71
-
72
-	/** @var ILogger */
73
-	private $logger;
74
-
75
-	/** @var TokenProvider */
76
-	private $tokenProvider;
77
-
78
-	/** @var ITimeFactory */
79
-	private $timeFactory;
80
-
81
-	/** @var EventDispatcherInterface */
82
-	private $dispatcher;
83
-
84
-	public function __construct(ProviderLoader $providerLoader,
85
-								IRegistry $providerRegistry,
86
-								MandatoryTwoFactor $mandatoryTwoFactor,
87
-								ISession $session, IConfig $config,
88
-								IManager $activityManager, ILogger $logger, TokenProvider $tokenProvider,
89
-								ITimeFactory $timeFactory, EventDispatcherInterface $eventDispatcher) {
90
-		$this->providerLoader = $providerLoader;
91
-		$this->providerRegistry = $providerRegistry;
92
-		$this->mandatoryTwoFactor = $mandatoryTwoFactor;
93
-		$this->session = $session;
94
-		$this->config = $config;
95
-		$this->activityManager = $activityManager;
96
-		$this->logger = $logger;
97
-		$this->tokenProvider = $tokenProvider;
98
-		$this->timeFactory = $timeFactory;
99
-		$this->dispatcher = $eventDispatcher;
100
-	}
101
-
102
-	/**
103
-	 * Determine whether the user must provide a second factor challenge
104
-	 *
105
-	 * @param IUser $user
106
-	 * @return boolean
107
-	 */
108
-	public function isTwoFactorAuthenticated(IUser $user): bool {
109
-		if ($this->mandatoryTwoFactor->isEnforcedFor($user)) {
110
-			return true;
111
-		}
112
-
113
-		$providerStates = $this->providerRegistry->getProviderStates($user);
114
-		$providers = $this->providerLoader->getProviders($user);
115
-		$fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
116
-		$enabled = array_filter($fixedStates);
117
-		$providerIds = array_keys($enabled);
118
-		$providerIdsWithoutBackupCodes = array_diff($providerIds, [self::BACKUP_CODES_PROVIDER_ID]);
119
-
120
-		return !empty($providerIdsWithoutBackupCodes);
121
-	}
122
-
123
-	/**
124
-	 * Get a 2FA provider by its ID
125
-	 *
126
-	 * @param IUser $user
127
-	 * @param string $challengeProviderId
128
-	 * @return IProvider|null
129
-	 */
130
-	public function getProvider(IUser $user, string $challengeProviderId) {
131
-		$providers = $this->getProviderSet($user)->getProviders();
132
-		return $providers[$challengeProviderId] ?? null;
133
-	}
134
-
135
-	/**
136
-	 * Check if the persistant mapping of enabled/disabled state of each available
137
-	 * provider is missing an entry and add it to the registry in that case.
138
-	 *
139
-	 * @todo remove in Nextcloud 17 as by then all providers should have been updated
140
-	 *
141
-	 * @param string[] $providerStates
142
-	 * @param IProvider[] $providers
143
-	 * @param IUser $user
144
-	 * @return string[] the updated $providerStates variable
145
-	 */
146
-	private function fixMissingProviderStates(array $providerStates,
147
-		array $providers, IUser $user): array {
148
-
149
-		foreach ($providers as $provider) {
150
-			if (isset($providerStates[$provider->getId()])) {
151
-				// All good
152
-				continue;
153
-			}
154
-
155
-			$enabled = $provider->isTwoFactorAuthEnabledForUser($user);
156
-			if ($enabled) {
157
-				$this->providerRegistry->enableProviderFor($provider, $user);
158
-			} else {
159
-				$this->providerRegistry->disableProviderFor($provider, $user);
160
-			}
161
-			$providerStates[$provider->getId()] = $enabled;
162
-		}
163
-
164
-		return $providerStates;
165
-	}
166
-
167
-	/**
168
-	 * @param array $states
169
-	 * @param IProvider $providers
170
-	 */
171
-	private function isProviderMissing(array $states, array $providers): bool {
172
-		$indexed = [];
173
-		foreach ($providers as $provider) {
174
-			$indexed[$provider->getId()] = $provider;
175
-		}
176
-
177
-		$missing = [];
178
-		foreach ($states as $providerId => $enabled) {
179
-			if (!$enabled) {
180
-				// Don't care
181
-				continue;
182
-			}
183
-
184
-			if (!isset($indexed[$providerId])) {
185
-				$missing[] = $providerId;
186
-				$this->logger->alert("two-factor auth provider '$providerId' failed to load",
187
-					[
188
-					'app' => 'core',
189
-				]);
190
-			}
191
-		}
192
-
193
-		if (!empty($missing)) {
194
-			// There was at least one provider missing
195
-			$this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']);
196
-
197
-			return true;
198
-		}
199
-
200
-		// If we reach this, there was not a single provider missing
201
-		return false;
202
-	}
203
-
204
-	/**
205
-	 * Get the list of 2FA providers for the given user
206
-	 *
207
-	 * @param IUser $user
208
-	 * @throws Exception
209
-	 */
210
-	public function getProviderSet(IUser $user): ProviderSet {
211
-		$providerStates = $this->providerRegistry->getProviderStates($user);
212
-		$providers = $this->providerLoader->getProviders($user);
213
-
214
-		$fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
215
-		$isProviderMissing = $this->isProviderMissing($fixedStates, $providers);
216
-
217
-		$enabled = array_filter($providers, function (IProvider $provider) use ($fixedStates) {
218
-			return $fixedStates[$provider->getId()];
219
-		});
220
-		return new ProviderSet($enabled, $isProviderMissing);
221
-	}
222
-
223
-	/**
224
-	 * Verify the given challenge
225
-	 *
226
-	 * @param string $providerId
227
-	 * @param IUser $user
228
-	 * @param string $challenge
229
-	 * @return boolean
230
-	 */
231
-	public function verifyChallenge(string $providerId, IUser $user, string $challenge): bool {
232
-		$provider = $this->getProvider($user, $providerId);
233
-		if ($provider === null) {
234
-			return false;
235
-		}
236
-
237
-		$passed = $provider->verifyChallenge($user, $challenge);
238
-		if ($passed) {
239
-			if ($this->session->get(self::REMEMBER_LOGIN) === true) {
240
-				// TODO: resolve cyclic dependency and use DI
241
-				\OC::$server->getUserSession()->createRememberMeToken($user);
242
-			}
243
-			$this->session->remove(self::SESSION_UID_KEY);
244
-			$this->session->remove(self::REMEMBER_LOGIN);
245
-			$this->session->set(self::SESSION_UID_DONE, $user->getUID());
246
-
247
-			// Clear token from db
248
-			$sessionId = $this->session->getId();
249
-			$token = $this->tokenProvider->getToken($sessionId);
250
-			$tokenId = $token->getId();
251
-			$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $tokenId);
252
-
253
-			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
254
-			$this->dispatcher->dispatch(IProvider::EVENT_SUCCESS, $dispatchEvent);
255
-
256
-			$this->publishEvent($user, 'twofactor_success', [
257
-				'provider' => $provider->getDisplayName(),
258
-			]);
259
-		} else {
260
-			$dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
261
-			$this->dispatcher->dispatch(IProvider::EVENT_FAILED, $dispatchEvent);
262
-
263
-			$this->publishEvent($user, 'twofactor_failed', [
264
-				'provider' => $provider->getDisplayName(),
265
-			]);
266
-		}
267
-		return $passed;
268
-	}
269
-
270
-	/**
271
-	 * Push a 2fa event the user's activity stream
272
-	 *
273
-	 * @param IUser $user
274
-	 * @param string $event
275
-	 * @param array $params
276
-	 */
277
-	private function publishEvent(IUser $user, string $event, array $params) {
278
-		$activity = $this->activityManager->generateEvent();
279
-		$activity->setApp('core')
280
-			->setType('security')
281
-			->setAuthor($user->getUID())
282
-			->setAffectedUser($user->getUID())
283
-			->setSubject($event, $params);
284
-		try {
285
-			$this->activityManager->publish($activity);
286
-		} catch (BadMethodCallException $e) {
287
-			$this->logger->warning('could not publish activity', ['app' => 'core']);
288
-			$this->logger->logException($e, ['app' => 'core']);
289
-		}
290
-	}
291
-
292
-	/**
293
-	 * Check if the currently logged in user needs to pass 2FA
294
-	 *
295
-	 * @param IUser $user the currently logged in user
296
-	 * @return boolean
297
-	 */
298
-	public function needsSecondFactor(IUser $user = null): bool {
299
-		if ($user === null) {
300
-			return false;
301
-		}
302
-
303
-		// If we are authenticated using an app password skip all this
304
-		if ($this->session->exists('app_password')) {
305
-			return false;
306
-		}
307
-
308
-		// First check if the session tells us we should do 2FA (99% case)
309
-		if (!$this->session->exists(self::SESSION_UID_KEY)) {
310
-
311
-			// Check if the session tells us it is 2FA authenticated already
312
-			if ($this->session->exists(self::SESSION_UID_DONE) &&
313
-				$this->session->get(self::SESSION_UID_DONE) === $user->getUID()) {
314
-				return false;
315
-			}
316
-
317
-			/*
49
+    const SESSION_UID_KEY = 'two_factor_auth_uid';
50
+    const SESSION_UID_DONE = 'two_factor_auth_passed';
51
+    const REMEMBER_LOGIN = 'two_factor_remember_login';
52
+    const BACKUP_CODES_PROVIDER_ID = 'backup_codes';
53
+
54
+    /** @var ProviderLoader */
55
+    private $providerLoader;
56
+
57
+    /** @var IRegistry */
58
+    private $providerRegistry;
59
+
60
+    /** @var MandatoryTwoFactor */
61
+    private $mandatoryTwoFactor;
62
+
63
+    /** @var ISession */
64
+    private $session;
65
+
66
+    /** @var IConfig */
67
+    private $config;
68
+
69
+    /** @var IManager */
70
+    private $activityManager;
71
+
72
+    /** @var ILogger */
73
+    private $logger;
74
+
75
+    /** @var TokenProvider */
76
+    private $tokenProvider;
77
+
78
+    /** @var ITimeFactory */
79
+    private $timeFactory;
80
+
81
+    /** @var EventDispatcherInterface */
82
+    private $dispatcher;
83
+
84
+    public function __construct(ProviderLoader $providerLoader,
85
+                                IRegistry $providerRegistry,
86
+                                MandatoryTwoFactor $mandatoryTwoFactor,
87
+                                ISession $session, IConfig $config,
88
+                                IManager $activityManager, ILogger $logger, TokenProvider $tokenProvider,
89
+                                ITimeFactory $timeFactory, EventDispatcherInterface $eventDispatcher) {
90
+        $this->providerLoader = $providerLoader;
91
+        $this->providerRegistry = $providerRegistry;
92
+        $this->mandatoryTwoFactor = $mandatoryTwoFactor;
93
+        $this->session = $session;
94
+        $this->config = $config;
95
+        $this->activityManager = $activityManager;
96
+        $this->logger = $logger;
97
+        $this->tokenProvider = $tokenProvider;
98
+        $this->timeFactory = $timeFactory;
99
+        $this->dispatcher = $eventDispatcher;
100
+    }
101
+
102
+    /**
103
+     * Determine whether the user must provide a second factor challenge
104
+     *
105
+     * @param IUser $user
106
+     * @return boolean
107
+     */
108
+    public function isTwoFactorAuthenticated(IUser $user): bool {
109
+        if ($this->mandatoryTwoFactor->isEnforcedFor($user)) {
110
+            return true;
111
+        }
112
+
113
+        $providerStates = $this->providerRegistry->getProviderStates($user);
114
+        $providers = $this->providerLoader->getProviders($user);
115
+        $fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
116
+        $enabled = array_filter($fixedStates);
117
+        $providerIds = array_keys($enabled);
118
+        $providerIdsWithoutBackupCodes = array_diff($providerIds, [self::BACKUP_CODES_PROVIDER_ID]);
119
+
120
+        return !empty($providerIdsWithoutBackupCodes);
121
+    }
122
+
123
+    /**
124
+     * Get a 2FA provider by its ID
125
+     *
126
+     * @param IUser $user
127
+     * @param string $challengeProviderId
128
+     * @return IProvider|null
129
+     */
130
+    public function getProvider(IUser $user, string $challengeProviderId) {
131
+        $providers = $this->getProviderSet($user)->getProviders();
132
+        return $providers[$challengeProviderId] ?? null;
133
+    }
134
+
135
+    /**
136
+     * Check if the persistant mapping of enabled/disabled state of each available
137
+     * provider is missing an entry and add it to the registry in that case.
138
+     *
139
+     * @todo remove in Nextcloud 17 as by then all providers should have been updated
140
+     *
141
+     * @param string[] $providerStates
142
+     * @param IProvider[] $providers
143
+     * @param IUser $user
144
+     * @return string[] the updated $providerStates variable
145
+     */
146
+    private function fixMissingProviderStates(array $providerStates,
147
+        array $providers, IUser $user): array {
148
+
149
+        foreach ($providers as $provider) {
150
+            if (isset($providerStates[$provider->getId()])) {
151
+                // All good
152
+                continue;
153
+            }
154
+
155
+            $enabled = $provider->isTwoFactorAuthEnabledForUser($user);
156
+            if ($enabled) {
157
+                $this->providerRegistry->enableProviderFor($provider, $user);
158
+            } else {
159
+                $this->providerRegistry->disableProviderFor($provider, $user);
160
+            }
161
+            $providerStates[$provider->getId()] = $enabled;
162
+        }
163
+
164
+        return $providerStates;
165
+    }
166
+
167
+    /**
168
+     * @param array $states
169
+     * @param IProvider $providers
170
+     */
171
+    private function isProviderMissing(array $states, array $providers): bool {
172
+        $indexed = [];
173
+        foreach ($providers as $provider) {
174
+            $indexed[$provider->getId()] = $provider;
175
+        }
176
+
177
+        $missing = [];
178
+        foreach ($states as $providerId => $enabled) {
179
+            if (!$enabled) {
180
+                // Don't care
181
+                continue;
182
+            }
183
+
184
+            if (!isset($indexed[$providerId])) {
185
+                $missing[] = $providerId;
186
+                $this->logger->alert("two-factor auth provider '$providerId' failed to load",
187
+                    [
188
+                    'app' => 'core',
189
+                ]);
190
+            }
191
+        }
192
+
193
+        if (!empty($missing)) {
194
+            // There was at least one provider missing
195
+            $this->logger->alert(count($missing) . " two-factor auth providers failed to load", ['app' => 'core']);
196
+
197
+            return true;
198
+        }
199
+
200
+        // If we reach this, there was not a single provider missing
201
+        return false;
202
+    }
203
+
204
+    /**
205
+     * Get the list of 2FA providers for the given user
206
+     *
207
+     * @param IUser $user
208
+     * @throws Exception
209
+     */
210
+    public function getProviderSet(IUser $user): ProviderSet {
211
+        $providerStates = $this->providerRegistry->getProviderStates($user);
212
+        $providers = $this->providerLoader->getProviders($user);
213
+
214
+        $fixedStates = $this->fixMissingProviderStates($providerStates, $providers, $user);
215
+        $isProviderMissing = $this->isProviderMissing($fixedStates, $providers);
216
+
217
+        $enabled = array_filter($providers, function (IProvider $provider) use ($fixedStates) {
218
+            return $fixedStates[$provider->getId()];
219
+        });
220
+        return new ProviderSet($enabled, $isProviderMissing);
221
+    }
222
+
223
+    /**
224
+     * Verify the given challenge
225
+     *
226
+     * @param string $providerId
227
+     * @param IUser $user
228
+     * @param string $challenge
229
+     * @return boolean
230
+     */
231
+    public function verifyChallenge(string $providerId, IUser $user, string $challenge): bool {
232
+        $provider = $this->getProvider($user, $providerId);
233
+        if ($provider === null) {
234
+            return false;
235
+        }
236
+
237
+        $passed = $provider->verifyChallenge($user, $challenge);
238
+        if ($passed) {
239
+            if ($this->session->get(self::REMEMBER_LOGIN) === true) {
240
+                // TODO: resolve cyclic dependency and use DI
241
+                \OC::$server->getUserSession()->createRememberMeToken($user);
242
+            }
243
+            $this->session->remove(self::SESSION_UID_KEY);
244
+            $this->session->remove(self::REMEMBER_LOGIN);
245
+            $this->session->set(self::SESSION_UID_DONE, $user->getUID());
246
+
247
+            // Clear token from db
248
+            $sessionId = $this->session->getId();
249
+            $token = $this->tokenProvider->getToken($sessionId);
250
+            $tokenId = $token->getId();
251
+            $this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $tokenId);
252
+
253
+            $dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
254
+            $this->dispatcher->dispatch(IProvider::EVENT_SUCCESS, $dispatchEvent);
255
+
256
+            $this->publishEvent($user, 'twofactor_success', [
257
+                'provider' => $provider->getDisplayName(),
258
+            ]);
259
+        } else {
260
+            $dispatchEvent = new GenericEvent($user, ['provider' => $provider->getDisplayName()]);
261
+            $this->dispatcher->dispatch(IProvider::EVENT_FAILED, $dispatchEvent);
262
+
263
+            $this->publishEvent($user, 'twofactor_failed', [
264
+                'provider' => $provider->getDisplayName(),
265
+            ]);
266
+        }
267
+        return $passed;
268
+    }
269
+
270
+    /**
271
+     * Push a 2fa event the user's activity stream
272
+     *
273
+     * @param IUser $user
274
+     * @param string $event
275
+     * @param array $params
276
+     */
277
+    private function publishEvent(IUser $user, string $event, array $params) {
278
+        $activity = $this->activityManager->generateEvent();
279
+        $activity->setApp('core')
280
+            ->setType('security')
281
+            ->setAuthor($user->getUID())
282
+            ->setAffectedUser($user->getUID())
283
+            ->setSubject($event, $params);
284
+        try {
285
+            $this->activityManager->publish($activity);
286
+        } catch (BadMethodCallException $e) {
287
+            $this->logger->warning('could not publish activity', ['app' => 'core']);
288
+            $this->logger->logException($e, ['app' => 'core']);
289
+        }
290
+    }
291
+
292
+    /**
293
+     * Check if the currently logged in user needs to pass 2FA
294
+     *
295
+     * @param IUser $user the currently logged in user
296
+     * @return boolean
297
+     */
298
+    public function needsSecondFactor(IUser $user = null): bool {
299
+        if ($user === null) {
300
+            return false;
301
+        }
302
+
303
+        // If we are authenticated using an app password skip all this
304
+        if ($this->session->exists('app_password')) {
305
+            return false;
306
+        }
307
+
308
+        // First check if the session tells us we should do 2FA (99% case)
309
+        if (!$this->session->exists(self::SESSION_UID_KEY)) {
310
+
311
+            // Check if the session tells us it is 2FA authenticated already
312
+            if ($this->session->exists(self::SESSION_UID_DONE) &&
313
+                $this->session->get(self::SESSION_UID_DONE) === $user->getUID()) {
314
+                return false;
315
+            }
316
+
317
+            /*
318 318
 			 * If the session is expired check if we are not logged in by a token
319 319
 			 * that still needs 2FA auth
320 320
 			 */
321
-			try {
322
-				$sessionId = $this->session->getId();
323
-				$token = $this->tokenProvider->getToken($sessionId);
324
-				$tokenId = $token->getId();
325
-				$tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
326
-
327
-				if (!\in_array($tokenId, $tokensNeeding2FA, true)) {
328
-					$this->session->set(self::SESSION_UID_DONE, $user->getUID());
329
-					return false;
330
-				}
331
-			} catch (InvalidTokenException $e) {
332
-			}
333
-		}
334
-
335
-		if (!$this->isTwoFactorAuthenticated($user)) {
336
-			// There is no second factor any more -> let the user pass
337
-			//   This prevents infinite redirect loops when a user is about
338
-			//   to solve the 2FA challenge, and the provider app is
339
-			//   disabled the same time
340
-			$this->session->remove(self::SESSION_UID_KEY);
341
-
342
-			$keys = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
343
-			foreach ($keys as $key) {
344
-				$this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $key);
345
-			}
346
-			return false;
347
-		}
348
-
349
-		return true;
350
-	}
351
-
352
-	/**
353
-	 * Prepare the 2FA login
354
-	 *
355
-	 * @param IUser $user
356
-	 * @param boolean $rememberMe
357
-	 */
358
-	public function prepareTwoFactorLogin(IUser $user, bool $rememberMe) {
359
-		$this->session->set(self::SESSION_UID_KEY, $user->getUID());
360
-		$this->session->set(self::REMEMBER_LOGIN, $rememberMe);
361
-
362
-		$id = $this->session->getId();
363
-		$token = $this->tokenProvider->getToken($id);
364
-		$this->config->setUserValue($user->getUID(), 'login_token_2fa', $token->getId(), $this->timeFactory->getTime());
365
-	}
321
+            try {
322
+                $sessionId = $this->session->getId();
323
+                $token = $this->tokenProvider->getToken($sessionId);
324
+                $tokenId = $token->getId();
325
+                $tokensNeeding2FA = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
326
+
327
+                if (!\in_array($tokenId, $tokensNeeding2FA, true)) {
328
+                    $this->session->set(self::SESSION_UID_DONE, $user->getUID());
329
+                    return false;
330
+                }
331
+            } catch (InvalidTokenException $e) {
332
+            }
333
+        }
334
+
335
+        if (!$this->isTwoFactorAuthenticated($user)) {
336
+            // There is no second factor any more -> let the user pass
337
+            //   This prevents infinite redirect loops when a user is about
338
+            //   to solve the 2FA challenge, and the provider app is
339
+            //   disabled the same time
340
+            $this->session->remove(self::SESSION_UID_KEY);
341
+
342
+            $keys = $this->config->getUserKeys($user->getUID(), 'login_token_2fa');
343
+            foreach ($keys as $key) {
344
+                $this->config->deleteUserValue($user->getUID(), 'login_token_2fa', $key);
345
+            }
346
+            return false;
347
+        }
348
+
349
+        return true;
350
+    }
351
+
352
+    /**
353
+     * Prepare the 2FA login
354
+     *
355
+     * @param IUser $user
356
+     * @param boolean $rememberMe
357
+     */
358
+    public function prepareTwoFactorLogin(IUser $user, bool $rememberMe) {
359
+        $this->session->set(self::SESSION_UID_KEY, $user->getUID());
360
+        $this->session->set(self::REMEMBER_LOGIN, $rememberMe);
361
+
362
+        $id = $this->session->getId();
363
+        $token = $this->tokenProvider->getToken($id);
364
+        $this->config->setUserValue($user->getUID(), 'login_token_2fa', $token->getId(), $this->timeFactory->getTime());
365
+    }
366 366
 
367 367
 }
Please login to merge, or discard this patch.
core/Command/TwoFactorAuth/Enforce.php 2 patches
Indentation   +69 added lines, -69 removed lines patch added patch discarded remove patch
@@ -36,81 +36,81 @@
 block discarded – undo
36 36
 
37 37
 class Enforce extends Command {
38 38
 
39
-	/** @var MandatoryTwoFactor */
40
-	private $mandatoryTwoFactor;
39
+    /** @var MandatoryTwoFactor */
40
+    private $mandatoryTwoFactor;
41 41
 
42
-	public function __construct(MandatoryTwoFactor $mandatoryTwoFactor) {
43
-		parent::__construct();
42
+    public function __construct(MandatoryTwoFactor $mandatoryTwoFactor) {
43
+        parent::__construct();
44 44
 
45
-		$this->mandatoryTwoFactor = $mandatoryTwoFactor;
46
-	}
45
+        $this->mandatoryTwoFactor = $mandatoryTwoFactor;
46
+    }
47 47
 
48
-	protected function configure() {
49
-		$this->setName('twofactorauth:enforce');
50
-		$this->setDescription('Enabled/disable enforced two-factor authentication');
51
-		$this->addOption(
52
-			'on',
53
-			null,
54
-			InputOption::VALUE_NONE,
55
-			'enforce two-factor authentication'
56
-		);
57
-		$this->addOption(
58
-			'off',
59
-			null,
60
-			InputOption::VALUE_NONE,
61
-			'don\'t enforce two-factor authenticaton'
62
-		);
63
-		$this->addOption(
64
-			'group',
65
-			null,
66
-			InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
67
-			'enforce only for the given group(s)'
68
-		);
69
-		$this->addOption(
70
-			'exclude',
71
-			null,
72
-			InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
73
-			'exclude mandatory two-factor auth for the given group(s)'
74
-		);
75
-	}
48
+    protected function configure() {
49
+        $this->setName('twofactorauth:enforce');
50
+        $this->setDescription('Enabled/disable enforced two-factor authentication');
51
+        $this->addOption(
52
+            'on',
53
+            null,
54
+            InputOption::VALUE_NONE,
55
+            'enforce two-factor authentication'
56
+        );
57
+        $this->addOption(
58
+            'off',
59
+            null,
60
+            InputOption::VALUE_NONE,
61
+            'don\'t enforce two-factor authenticaton'
62
+        );
63
+        $this->addOption(
64
+            'group',
65
+            null,
66
+            InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
67
+            'enforce only for the given group(s)'
68
+        );
69
+        $this->addOption(
70
+            'exclude',
71
+            null,
72
+            InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
73
+            'exclude mandatory two-factor auth for the given group(s)'
74
+        );
75
+    }
76 76
 
77
-	protected function execute(InputInterface $input, OutputInterface $output) {
78
-		if ($input->getOption('on')) {
79
-			$enforcedGroups = $input->getOption('group');
80
-			$excludedGroups = $input->getOption('exclude');
81
-			$this->mandatoryTwoFactor->setState(new EnforcementState(true, $enforcedGroups, $excludedGroups));
82
-		} elseif ($input->getOption('off')) {
83
-			$this->mandatoryTwoFactor->setState(new EnforcementState(false));
84
-		}
77
+    protected function execute(InputInterface $input, OutputInterface $output) {
78
+        if ($input->getOption('on')) {
79
+            $enforcedGroups = $input->getOption('group');
80
+            $excludedGroups = $input->getOption('exclude');
81
+            $this->mandatoryTwoFactor->setState(new EnforcementState(true, $enforcedGroups, $excludedGroups));
82
+        } elseif ($input->getOption('off')) {
83
+            $this->mandatoryTwoFactor->setState(new EnforcementState(false));
84
+        }
85 85
 
86
-		$state = $this->mandatoryTwoFactor->getState();
87
-		if ($state->isEnforced()) {
88
-			$this->writeEnforced($output, $state);
89
-		} else {
90
-			$this->writeNotEnforced($output);
91
-		}
92
-	}
86
+        $state = $this->mandatoryTwoFactor->getState();
87
+        if ($state->isEnforced()) {
88
+            $this->writeEnforced($output, $state);
89
+        } else {
90
+            $this->writeNotEnforced($output);
91
+        }
92
+    }
93 93
 
94
-	/**
95
-	 * @param OutputInterface $output
96
-	 */
97
-	protected function writeEnforced(OutputInterface $output, EnforcementState $state) {
98
-		if (empty($state->getEnforcedGroups())) {
99
-			$message = 'Two-factor authentication is enforced for all users';
100
-		} else {
101
-			$message = 'Two-factor authentication is enforced for members of the group(s) ' . implode(', ', $state->getEnforcedGroups());
102
-		}
103
-		if (!empty($state->getExcludedGroups())) {
104
-			$message .= ', except members of ' . implode(', ', $state->getExcludedGroups());
105
-		}
106
-		$output->writeln($message);
107
-	}
94
+    /**
95
+     * @param OutputInterface $output
96
+     */
97
+    protected function writeEnforced(OutputInterface $output, EnforcementState $state) {
98
+        if (empty($state->getEnforcedGroups())) {
99
+            $message = 'Two-factor authentication is enforced for all users';
100
+        } else {
101
+            $message = 'Two-factor authentication is enforced for members of the group(s) ' . implode(', ', $state->getEnforcedGroups());
102
+        }
103
+        if (!empty($state->getExcludedGroups())) {
104
+            $message .= ', except members of ' . implode(', ', $state->getExcludedGroups());
105
+        }
106
+        $output->writeln($message);
107
+    }
108 108
 
109
-	/**
110
-	 * @param OutputInterface $output
111
-	 */
112
-	protected function writeNotEnforced(OutputInterface $output) {
113
-		$output->writeln('Two-factor authentication is not enforced');
114
-	}
109
+    /**
110
+     * @param OutputInterface $output
111
+     */
112
+    protected function writeNotEnforced(OutputInterface $output) {
113
+        $output->writeln('Two-factor authentication is not enforced');
114
+    }
115 115
 
116 116
 }
Please login to merge, or discard this patch.
Spacing   +2 added lines, -2 removed lines patch added patch discarded remove patch
@@ -98,10 +98,10 @@
 block discarded – undo
98 98
 		if (empty($state->getEnforcedGroups())) {
99 99
 			$message = 'Two-factor authentication is enforced for all users';
100 100
 		} else {
101
-			$message = 'Two-factor authentication is enforced for members of the group(s) ' . implode(', ', $state->getEnforcedGroups());
101
+			$message = 'Two-factor authentication is enforced for members of the group(s) '.implode(', ', $state->getEnforcedGroups());
102 102
 		}
103 103
 		if (!empty($state->getExcludedGroups())) {
104
-			$message .= ', except members of ' . implode(', ', $state->getExcludedGroups());
104
+			$message .= ', except members of '.implode(', ', $state->getExcludedGroups());
105 105
 		}
106 106
 		$output->writeln($message);
107 107
 	}
Please login to merge, or discard this patch.