Total Complexity | 42 |
Total Lines | 405 |
Duplicated Lines | 0 % |
Changes | 6 | ||
Bugs | 1 | Features | 0 |
Complex classes like Negotiate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Negotiate, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
17 | /** |
||
18 | * The Negotiate module. Allows for password-less, secure login by Kerberos and Negotiate. |
||
19 | * |
||
20 | * @package simplesamlphp/simplesamlphp-module-negotiate |
||
21 | */ |
||
22 | |||
23 | class Negotiate extends \SimpleSAML\Auth\Source |
||
24 | { |
||
25 | // Constants used in the module |
||
26 | public const STAGEID = '\SimpleSAML\Module\negotiateext\Auth\Source\Negotiate.StageId'; |
||
27 | |||
28 | /** @var \SimpleSAML\Module\ldap\Auth\Ldap */ |
||
29 | protected Ldap $ldap; |
||
30 | |||
31 | /** @var string */ |
||
32 | protected string $backend = ''; |
||
33 | |||
34 | /** @var string */ |
||
35 | protected string $hostname = ''; |
||
36 | |||
37 | /** @var int */ |
||
38 | protected int $port = 389; |
||
39 | |||
40 | /** @var bool */ |
||
41 | protected bool $referrals = true; |
||
42 | |||
43 | /** @var bool */ |
||
44 | protected bool $enableTLS = false; |
||
45 | |||
46 | /** @var bool */ |
||
47 | protected bool $debugLDAP = false; |
||
48 | |||
49 | /** @var int */ |
||
50 | protected int $timeout = 30; |
||
51 | |||
52 | /** @var string */ |
||
53 | protected string $keytab = ''; |
||
54 | |||
55 | /** @var array */ |
||
56 | protected array $base = []; |
||
57 | |||
58 | /** @var array */ |
||
59 | protected array $attr = ['uid']; |
||
60 | |||
61 | /** @var array|null */ |
||
62 | protected ?array $subnet = null; |
||
63 | |||
64 | /** @var string|null */ |
||
65 | protected ?string $admin_user = null; |
||
66 | |||
67 | /** @var string|null */ |
||
68 | protected ?string $admin_pw = null; |
||
69 | |||
70 | /** @var array|null */ |
||
71 | protected ?array $attributes = null; |
||
72 | |||
73 | /** @var array */ |
||
74 | protected array $binaryAttributes = []; |
||
75 | |||
76 | |||
77 | /** |
||
78 | * Constructor for this authentication source. |
||
79 | * |
||
80 | * @param array $info Information about this authentication source. |
||
81 | * @param array $config The configuration of the module |
||
82 | */ |
||
83 | public function __construct(array $info, array $config) |
||
84 | { |
||
85 | // call the parent constructor first, as required by the interface |
||
86 | parent::__construct($info, $config); |
||
87 | |||
88 | $cfg = \SimpleSAML\Configuration::loadFromArray($config); |
||
89 | |||
90 | $this->backend = $cfg->getString('fallback'); |
||
91 | $this->hostname = $cfg->getString('hostname'); |
||
92 | $this->port = $cfg->getInteger('port', 389); |
||
93 | $this->referrals = $cfg->getBoolean('referrals', true); |
||
94 | $this->enableTLS = $cfg->getBoolean('enable_tls', false); |
||
95 | $this->debugLDAP = $cfg->getBoolean('debugLDAP', false); |
||
96 | $this->timeout = $cfg->getInteger('timeout', 30); |
||
97 | $this->base = $cfg->getArrayizeString('base'); |
||
98 | $this->attr = $cfg->getArrayizeString('attr', 'uid'); |
||
99 | $this->subnet = $cfg->getArray('subnet', null); |
||
100 | $this->admin_user = $cfg->getString('adminUser', null); |
||
101 | $this->admin_pw = $cfg->getString('adminPassword', null); |
||
102 | $this->attributes = $cfg->getArray('attributes', null); |
||
103 | $this->binaryAttributes = $cfg->getArray('attributes.binary', []); |
||
104 | } |
||
105 | |||
106 | |||
107 | /** |
||
108 | * The inner workings of the module. |
||
109 | * |
||
110 | * Checks to see if client is in the defined subnets (if defined in config). Sends the client a 401 Negotiate and |
||
111 | * responds to the result. If the client fails to provide a proper Kerberos ticket, the login process is handed over |
||
112 | * to the 'fallback' module defined in the config. |
||
113 | * |
||
114 | * LDAP is used as a user metadata source. |
||
115 | * |
||
116 | * @param array &$state Information about the current authentication. |
||
117 | */ |
||
118 | public function authenticate(array &$state): void |
||
119 | { |
||
120 | // set the default backend to config |
||
121 | $state['LogoutState'] = [ |
||
122 | 'negotiate:backend' => $this->backend, |
||
123 | ]; |
||
124 | $state['negotiate:authId'] = $this->authId; |
||
125 | |||
126 | |||
127 | // check for disabled SPs. The disable flag is store in the SP metadata |
||
128 | if (array_key_exists('SPMetadata', $state) && $this->spDisabledInMetadata($state['SPMetadata'])) { |
||
129 | $this->fallBack($state); |
||
130 | } |
||
131 | /* Go straight to fallback if Negotiate is disabled or if you are sent back to the IdP directly from the SP |
||
132 | after having logged out. */ |
||
133 | $session = \SimpleSAML\Session::getSessionFromRequest(); |
||
134 | $disabled = $session->getData('negotiate:disable', 'session'); |
||
135 | |||
136 | if ( |
||
137 | $disabled || |
||
138 | (!empty($_REQUEST['negotiateext_auth']) && |
||
139 | $_REQUEST['negotiateext_auth'] == 'false') || |
||
140 | (!empty($_COOKIE['NEGOTIATE_AUTOLOGIN_DISABLE_PERMANENT']) && |
||
141 | $_COOKIE['NEGOTIATE_AUTOLOGIN_DISABLE_PERMANENT'] == 'True') |
||
142 | ) { |
||
143 | Logger::debug('Negotiate - session disabled. falling back'); |
||
144 | $this->fallBack($state); |
||
145 | // never executed |
||
146 | assert(false); |
||
147 | } |
||
148 | $mask = $this->checkMask(); |
||
149 | if (!$mask) { |
||
150 | $this->fallBack($state); |
||
151 | // never executed |
||
152 | assert(false); |
||
153 | } |
||
154 | |||
155 | // No auth token. Send it. |
||
156 | Logger::debug('Negotiate - authenticate(): Sending Negotiate.'); |
||
157 | // Save the $state array, so that we can restore if after a redirect |
||
158 | Logger::debug('Negotiate - fallback: ' . $state['LogoutState']['negotiate:backend']); |
||
159 | $id = \SimpleSAML\Auth\State::saveState($state, self::STAGEID); |
||
160 | $params = ['AuthState' => $id]; |
||
161 | |||
162 | $this->sendNegotiate($params); |
||
163 | exit; |
||
164 | } |
||
165 | |||
166 | |||
167 | /** |
||
168 | * @param array $spMetadata |
||
169 | * @return bool |
||
170 | */ |
||
171 | public function spDisabledInMetadata(array $spMetadata): bool |
||
172 | { |
||
173 | if (array_key_exists('negotiate:disable', $spMetadata)) { |
||
174 | if ($spMetadata['negotiate:disable'] == true) { |
||
175 | Logger::debug('Negotiate - SP disabled. falling back'); |
||
176 | return true; |
||
177 | } else { |
||
178 | Logger::debug('Negotiate - SP disable flag found but set to FALSE'); |
||
179 | } |
||
180 | } else { |
||
181 | Logger::debug('Negotiate - SP disable flag not found'); |
||
182 | } |
||
183 | return false; |
||
184 | } |
||
185 | |||
186 | |||
187 | /** |
||
188 | * checkMask() looks up the subnet config option and verifies |
||
189 | * that the client is within that range. |
||
190 | * |
||
191 | * Will return TRUE if no subnet option is configured. |
||
192 | * |
||
193 | * @return bool |
||
194 | */ |
||
195 | public function checkMask(): bool |
||
196 | { |
||
197 | // No subnet means all clients are accepted. |
||
198 | if ($this->subnet === null) { |
||
199 | return true; |
||
200 | } |
||
201 | $ip = $_SERVER['REMOTE_ADDR']; |
||
202 | $netUtils = new Utils\Net() |
||
203 | foreach ($this->subnet as $cidr) { |
||
|
|||
204 | $ret = $netUtils->ipCIDRcheck($cidr); |
||
205 | if ($ret) { |
||
206 | Logger::debug('Negotiate: Client "' . $ip . '" matched subnet.'); |
||
207 | return true; |
||
208 | } |
||
209 | } |
||
210 | Logger::debug('Negotiate: Client "' . $ip . '" did not match subnet.'); |
||
211 | return false; |
||
212 | } |
||
213 | |||
214 | |||
215 | /** |
||
216 | * Send the actual headers and body of the 401. Embedded in the body is a post that is triggered by JS if the client |
||
217 | * wants to show the 401 message. |
||
218 | * |
||
219 | * @param array $params additional parameters to the URL in the URL in the body. |
||
220 | */ |
||
221 | protected function sendNegotiate(array $params): void |
||
222 | { |
||
223 | $authPage = Module::getModuleURL('negotiateext/auth.php'); |
||
224 | $httpUtils = new Utils\HTTP(); |
||
225 | $httpUtils->redirectTrustedURL($authPage, $params); |
||
226 | } |
||
227 | |||
228 | |||
229 | /** |
||
230 | * Passes control of the login process to a different module. |
||
231 | * |
||
232 | * @param array $state Information about the current authentication. |
||
233 | * |
||
234 | * @throws \SimpleSAML\Error\Error If couldn't determine the auth source. |
||
235 | * @throws \SimpleSAML\Error\Exception |
||
236 | * @throws \Exception |
||
237 | */ |
||
238 | public static function fallBack(array &$state): void |
||
239 | { |
||
240 | $authId = $state['LogoutState']['negotiate:backend']; |
||
241 | |||
242 | if ($authId === null) { |
||
243 | throw new Error\Error([500, "Unable to determine auth source."]); |
||
244 | } |
||
245 | Logger::debug('Negotiate: fallBack to ' . $authId); |
||
246 | $source = Auth\Source::getById($authId); |
||
247 | |||
248 | if ($source === null) { |
||
249 | throw new Exception('Could not find authentication source with id ' . $authId); |
||
250 | } |
||
251 | |||
252 | try { |
||
253 | $source->authenticate($state); |
||
254 | } catch (Error\Exception $e) { |
||
255 | Auth\State::throwException($state, $e); |
||
256 | } catch (Exception $e) { |
||
257 | $e = new Error\UnserializableException($e); |
||
258 | Auth\State::throwException($state, $e); |
||
259 | } |
||
260 | // fallBack never returns after loginCompleted() |
||
261 | Logger::debug('Negotiate: backend returned'); |
||
262 | self::loginCompleted($state); |
||
263 | } |
||
264 | |||
265 | |||
266 | /** |
||
267 | * @param array $state Information about the current authentication. |
||
268 | */ |
||
269 | public function externalAuth(array &$state): void |
||
270 | { |
||
271 | Logger::debug('Negotiate - authenticate(): remote user found'); |
||
272 | $this->ldap = new Ldap( |
||
273 | $this->hostname, |
||
274 | $this->enableTLS, |
||
275 | $this->debugLDAP, |
||
276 | $this->timeout, |
||
277 | $this->port, |
||
278 | $this->referrals |
||
279 | ); |
||
280 | |||
281 | $user = $_SERVER['REMOTE_USER']; |
||
282 | Logger::info('Negotiate - authenticate(): ' . $user . ' authenticated.'); |
||
283 | $lookup = $this->lookupUserData($user); |
||
284 | if ($lookup) { |
||
285 | $state['Attributes'] = $lookup; |
||
286 | // Override the backend so logout will know what to look for |
||
287 | $state['LogoutState'] = [ |
||
288 | 'negotiate:backend' => null, |
||
289 | ]; |
||
290 | Logger::info('Negotiate - authenticate(): ' . $user . ' authorized.'); |
||
291 | Auth\Source::completeAuth($state); |
||
292 | // Never reached. |
||
293 | assert(false); |
||
294 | } |
||
295 | } |
||
296 | |||
297 | |||
298 | /** |
||
299 | * Passes control of the login process to a different module. |
||
300 | * |
||
301 | * @param string $state Information about the current authentication. |
||
302 | * |
||
303 | * @throws \SimpleSAML\Error\BadRequest If couldn't determine the auth source. |
||
304 | * @throws \SimpleSAML\Error\NoState |
||
305 | * @throws \SimpleSAML\Error\Exception |
||
306 | */ |
||
307 | public static function external(): void |
||
308 | { |
||
309 | if (!isset($_REQUEST['AuthState'])) { |
||
310 | throw new Error\BadRequest('Missing "AuthState" parameter.'); |
||
311 | } |
||
312 | Logger::debug('Negotiate: external returned'); |
||
313 | $sid = Auth\State::parseStateID($_REQUEST['AuthState']); |
||
314 | |||
315 | $state = Auth\State::loadState($_REQUEST['AuthState'], self::STAGEID, true); |
||
316 | if ($state === null) { |
||
317 | if ($sid['url'] === null) { |
||
318 | throw new Error\NoState(); |
||
319 | } |
||
320 | $httpUtils = new Utils\HTTP(); |
||
321 | $httpUtils->redirectUntrustedURL($sid['url'], ['negotiateext.auth' => 'false']); |
||
322 | assert(false); |
||
323 | } |
||
324 | |||
325 | Assert::isArray($state); |
||
326 | |||
327 | if (!empty($_SERVER['REMOTE_USER'])) { |
||
328 | $source = Auth\Source::getById($state['negotiate:authId']); |
||
329 | if ($source === null) { |
||
330 | /* |
||
331 | * The only way this should fail is if we remove or rename the authentication source |
||
332 | * while the user is at the login page. |
||
333 | */ |
||
334 | throw new Error\Exception( |
||
335 | 'Could not find authentication source with id ' . $state['negotiate:authId'] |
||
336 | ); |
||
337 | } |
||
338 | /* |
||
339 | * Make sure that we haven't switched the source type while the |
||
340 | * user was at the authentication page. This can only happen if we |
||
341 | * change config/authsources.php while an user is logging in. |
||
342 | */ |
||
343 | if (!($source instanceof self)) { |
||
344 | throw new Error\Exception('Authentication source type changed.'); |
||
345 | } |
||
346 | Logger::debug('Negotiate - authenticate(): looking for Negotate'); |
||
347 | $source->externalAuth($state); |
||
348 | } |
||
349 | |||
350 | self::fallBack($state); |
||
351 | assert(false); |
||
352 | } |
||
353 | |||
354 | |||
355 | /** |
||
356 | * Strips away the realm of the Kerberos identifier, looks up what attributes to fetch from SP metadata and |
||
357 | * searches the directory. |
||
358 | * |
||
359 | * @param string $user The Kerberos user identifier. |
||
360 | * |
||
361 | * @return array|null The attributes for the user or NULL if not found. |
||
362 | */ |
||
363 | protected function lookupUserData(string $user): ?array |
||
364 | { |
||
365 | // Kerberos user names include realm. Strip that away. |
||
366 | $pos = strpos($user, '@'); |
||
367 | if ($pos === false) { |
||
368 | return null; |
||
369 | } |
||
370 | $uid = substr($user, 0, $pos); |
||
371 | |||
372 | $this->adminBind(); |
||
373 | try { |
||
374 | /** @psalm-var string $dn */ |
||
375 | $dn = $this->ldap->searchfordn($this->base, $this->attr, $uid); |
||
376 | return $this->ldap->getAttributes($dn, $this->attributes, $this->binaryAttributes); |
||
377 | } catch (Error\Exception $e) { |
||
378 | Logger::debug('Negotiate - ldap lookup failed: ' . $e); |
||
379 | return null; |
||
380 | } |
||
381 | } |
||
382 | |||
383 | |||
384 | /** |
||
385 | * Elevates the LDAP connection to allow restricted lookups if |
||
386 | * so configured. Does nothing if not. |
||
387 | * |
||
388 | * @throws \SimpleSAML\Error\AuthSource |
||
389 | */ |
||
390 | protected function adminBind(): void |
||
391 | { |
||
392 | if ($this->admin_user === null || $this->admin_pw === null) { |
||
393 | // no admin user |
||
394 | return; |
||
395 | } |
||
396 | Logger::debug('Negotiate - authenticate(): Binding as system user ' . var_export($this->admin_user, true)); |
||
397 | |||
398 | if (!$this->ldap->bind($this->admin_user, $this->admin_pw)) { |
||
399 | $msg = 'Unable to authenticate system user (LDAP_INVALID_CREDENTIALS)'; |
||
400 | Logger::error('Negotiate - authenticate(): ' . $msg . ' ' . var_export($this->admin_user, true)); |
||
401 | throw new Error\AuthSource('negotiate', $msg); |
||
402 | } |
||
403 | } |
||
404 | |||
405 | |||
406 | /** |
||
407 | * Log out from this authentication source. |
||
408 | * |
||
409 | * This method either logs the user out from Negotiate or passes the |
||
410 | * logout call to the fallback module. |
||
411 | * |
||
412 | * @param array &$state Information about the current logout operation. |
||
413 | */ |
||
414 | public function logout(array &$state): void |
||
415 | { |
||
416 | // get the source that was used to authenticate |
||
417 | $authId = $state['negotiate:backend']; |
||
418 | Logger::debug('Negotiate - logout has the following authId: "' . $authId . '"'); |
||
419 | |||
420 | if ($authId === null) { |
||
421 | $session = Session::getSessionFromRequest(); |
||
422 | $session->setData('negotiate:disable', 'session', true, 0); |
||
434 |