Passed
Push — master ( b888cb...b772c0 )
by Tim
07:12 queued 04:30
created

Negotiate::adminBind()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 3
nop 0
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\negotiateext\Auth\Source;
6
7
use Exception;
8
use SimpleSAML\Assert\Assert;
9
use SimpleSAML\Auth;
10
use SimpleSAML\Error;
11
use SimpleSAML\Logger;
12
use SimpleSAML\Module;
13
use SimpleSAML\Session;
14
use SimpleSAML\Utils;
15
16
/**
17
 * The Negotiate module. Allows for password-less, secure login by Kerberos and Negotiate.
18
 *
19
 * @package simplesamlphp/simplesamlphp-module-negotiate
20
 */
21
22
class Negotiate extends \SimpleSAML\Auth\Source
23
{
24
    // Constants used in the module
25
    public const STAGEID = '\SimpleSAML\Module\negotiateext\Auth\Source\Negotiate.StageId';
26
27
    /** @var string */
28
    protected string $backend;
29
30
    /** @var string */
31
    protected string $fallback;
32
33
    /** @var string */
34
    protected string $keytab = '';
35
36
    /** @var array|null */
37
    protected ?array $subnet = null;
38
39
40
    /**
41
     * Constructor for this authentication source.
42
     *
43
     * @param array $info Information about this authentication source.
44
     * @param array $config The configuration of the module
45
     */
46
    public function __construct(array $info, array $config)
47
    {
48
        // call the parent constructor first, as required by the interface
49
        parent::__construct($info, $config);
50
51
        $cfg = \SimpleSAML\Configuration::loadFromArray($config);
52
53
        $this->backend = $cfg->getString('backend');
54
        $this->fallback = $cfg->getString('fallback');
55
        $this->subnet = $cfg->getOptionalArray('subnet', null);
56
    }
57
58
59
    /**
60
     * The inner workings of the module.
61
     *
62
     * Checks to see if client is in the defined subnets (if defined in config). Sends the client a 401 Negotiate and
63
     * responds to the result. If the client fails to provide a proper Kerberos ticket, the login process is handed over
64
     * to the 'fallback' module defined in the config.
65
     *
66
     * LDAP is used as a user metadata source.
67
     *
68
     * @param array &$state Information about the current authentication.
69
     */
70
    public function authenticate(array &$state): void
71
    {
72
        // set the default backend to config
73
        $state['LogoutState'] = [
74
            'negotiate:backend' => $this->backend,
75
        ];
76
        $state['negotiate:authId'] = $this->authId;
77
        $state['negotiate:fallback'] = $this->fallback;
78
79
80
        // check for disabled SPs. The disable flag is store in the SP metadata
81
        if (array_key_exists('SPMetadata', $state) && $this->spDisabledInMetadata($state['SPMetadata'])) {
82
            $this->fallBack($state);
83
        }
84
        /* Go straight to fallback if Negotiate is disabled or if you are sent back to the IdP directly from the SP
85
        after having logged out. */
86
        $session = Session::getSessionFromRequest();
87
        $disabled = $session->getData('negotiate:disable', 'session');
88
89
        if (
90
            $disabled ||
91
            (!empty($_REQUEST['negotiateext_auth']) &&
92
                $_REQUEST['negotiateext_auth'] === 'false') ||
93
            (!empty($_COOKIE['NEGOTIATE_AUTOLOGIN_DISABLE_PERMANENT']) &&
94
                $_COOKIE['NEGOTIATE_AUTOLOGIN_DISABLE_PERMANENT'] === 'true')
95
        ) {
96
            Logger::debug('Negotiate - session disabled. falling back');
97
            $this->fallBack($state);
98
            // never executed
99
            assert(false);
100
        }
101
102
        $mask = $this->checkMask();
103
        if (!$mask) {
104
            $this->fallBack($state);
105
            // never executed
106
            assert(false);
107
        }
108
109
        // No auth token. Send it.
110
        Logger::debug('Negotiate - authenticate(): Sending Negotiate.');
111
        // Save the $state array, so that we can restore if after a redirect
112
        Logger::debug('Negotiate - fallback: ' . $state['LogoutState']['negotiate:backend']);
113
        $id = Auth\State::saveState($state, self::STAGEID);
114
        $params = ['AuthState' => $id];
115
116
        $this->sendNegotiate($params);
117
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
118
    }
119
120
121
    /**
122
     * @param array $spMetadata
123
     * @return bool
124
     */
125
    public function spDisabledInMetadata(array $spMetadata): bool
126
    {
127
        if (array_key_exists('negotiate:disable', $spMetadata)) {
128
            if ($spMetadata['negotiate:disable'] === true) {
129
                Logger::debug('Negotiate - SP disabled. falling back');
130
                return true;
131
            } else {
132
                Logger::debug('Negotiate - SP disable flag found but set to FALSE');
133
            }
134
        } else {
135
            Logger::debug('Negotiate - SP disable flag not found');
136
        }
137
        return false;
138
    }
139
140
141
    /**
142
     * checkMask() looks up the subnet config option and verifies
143
     * that the client is within that range.
144
     *
145
     * Will return TRUE if no subnet option is configured.
146
     *
147
     * @return bool
148
     */
149
    public function checkMask(): bool
150
    {
151
        // No subnet means all clients are accepted.
152
        if ($this->subnet === null) {
153
            return true;
154
        }
155
        $ip = $_SERVER['REMOTE_ADDR'];
156
        $netUtils = new Utils\Net();
157
        foreach ($this->subnet as $cidr) {
158
            $ret = $netUtils->ipCIDRcheck($cidr);
159
            if ($ret) {
160
                Logger::debug('Negotiate: Client "' . $ip . '" matched subnet.');
161
                return true;
162
            }
163
        }
164
        Logger::debug('Negotiate: Client "' . $ip . '" did not match subnet.');
165
        return false;
166
    }
167
168
169
    /**
170
     * Send the actual headers and body of the 401. Embedded in the body is a post that is triggered by JS if the client
171
     * wants to show the 401 message.
172
     *
173
     * @param array $params additional parameters to the URL in the URL in the body.
174
     */
175
    protected function sendNegotiate(array $params): void
176
    {
177
        $authPage = Module::getModuleURL('negotiateext/auth.php');
178
        $httpUtils = new Utils\HTTP();
179
        $httpUtils->redirectTrustedURL($authPage, $params);
180
    }
181
182
183
    /**
184
     * Passes control of the login process to a different module.
185
     *
186
     * @param array $state Information about the current authentication.
187
     *
188
     * @throws \SimpleSAML\Error\Error If couldn't determine the auth source.
189
     * @throws \SimpleSAML\Error\Exception
190
     * @throws \Exception
191
     */
192
    public static function fallBack(array &$state): void // never
193
    {
194
        $authId = $state['negotiate:fallback'];
195
196
        if ($authId === null) {
197
            throw new Error\Error([500, "Unable to determine auth source."]);
198
        }
199
        Logger::debug('Negotiate: fallBack to ' . $authId);
200
        $source = Auth\Source::getById($authId);
201
202
        if ($source === null) {
203
            throw new Exception('Could not find authentication source with id ' . $authId);
204
        }
205
206
        try {
207
            $source->authenticate($state);
208
        } catch (Error\Exception $e) {
209
            Auth\State::throwException($state, $e);
210
        } catch (Exception $e) {
211
            $e = new Error\UnserializableException($e);
212
            Auth\State::throwException($state, $e);
213
        }
214
215
        // fallBack never returns after loginCompleted()
216
        Logger::debug('Negotiate: backend returned');
217
        self::loginCompleted($state);
218
    }
219
220
221
    /**
222
     * @param array $state Information about the current authentication.
223
     */
224
    public function externalAuth(array &$state): void
225
    {
226
        Logger::debug('Negotiate - authenticate(): remote user found');
227
228
        $user = $_SERVER['REMOTE_USER'];
229
        Logger::info('Negotiate - authenticate(): ' . $user . ' authenticated.');
230
        $lookup = $this->lookupUserData($user);
231
        if ($lookup) {
232
            $state['Attributes'] = $lookup;
233
            // Override the backend so logout will know what to look for
234
            $state['LogoutState'] = [
235
                'negotiate:backend' => null,
236
            ];
237
            Logger::info('Negotiate - authenticate(): ' . $user . ' authorized.');
238
            Auth\Source::completeAuth($state);
239
            // Never reached.
240
            assert(false);
241
        }
242
    }
243
244
245
    /**
246
     * Passes control of the login process to a different module.
247
     *
248
     * @param string $state Information about the current authentication.
249
     *
250
     * @throws \SimpleSAML\Error\BadRequest If couldn't determine the auth source.
251
     * @throws \SimpleSAML\Error\NoState
252
     * @throws \SimpleSAML\Error\Exception
253
     */
254
    public static function external(): void
255
    {
256
        if (!isset($_REQUEST['AuthState'])) {
257
            throw new Error\BadRequest('Missing "AuthState" parameter.');
258
        }
259
        Logger::debug('Negotiate: external returned');
260
        $sid = Auth\State::parseStateID($_REQUEST['AuthState']);
261
262
        $state = Auth\State::loadState($_REQUEST['AuthState'], self::STAGEID, true);
263
        if ($state === null) {
264
            if ($sid['url'] === null) {
265
                throw new Error\NoState();
266
            }
267
            $httpUtils = new Utils\HTTP();
268
            $httpUtils->redirectUntrustedURL($sid['url'], ['negotiateext.auth' => 'false']);
269
            assert(false);
270
        }
271
272
        Assert::isArray($state);
273
274
        if (!empty($_SERVER['REMOTE_USER'])) {
275
            $source = Auth\Source::getById($state['negotiate:authId']);
276
            if ($source === null) {
277
                /*
278
                 * The only way this should fail is if we remove or rename the authentication source
279
                 * while the user is at the login page.
280
                 */
281
                throw new Error\Exception(
282
                    'Could not find authentication source with id ' . $state['negotiate:authId']
283
                );
284
            }
285
            /*
286
             * Make sure that we haven't switched the source type while the
287
             * user was at the authentication page. This can only happen if we
288
             * change config/authsources.php while an user is logging in.
289
             */
290
            if (!($source instanceof self)) {
291
                throw new Error\Exception('Authentication source type changed.');
292
            }
293
            Logger::debug('Negotiate - authenticate(): looking for Negotate');
294
            $source->externalAuth($state);
295
        }
296
297
        self::fallBack($state);
298
        assert(false);
299
    }
300
301
302
    /**
303
     * Strips away the realm of the Kerberos identifier, looks up what attributes to fetch from SP metadata and
304
     * searches the directory.
305
     *
306
     * @param string $user The Kerberos user identifier.
307
     *
308
     * @return array|null The attributes for the user or NULL if not found.
309
     */
310
    protected function lookupUserData(string $user): ?array
311
    {
312
        // Kerberos user names include realm. Strip that away.
313
        $pos = strpos($user, '@');
314
        if ($pos === false) {
315
            return null;
316
        }
317
        $uid = substr($user, 0, $pos);
318
319
        /** @var \SimpleSAML\Module\ldap\Auth\Source\Ldap|null $source */
320
        $source = Auth\Source::getById($this->backend);
321
        if ($source === null) {
322
            throw new Exception('Could not find authentication source with id ' . $this->backend);
323
        }
324
325
        try {
326
            return $source->getAttributes($uid);
327
        } catch (Error\Exception $e) {
328
            Logger::debug('Negotiate - ldap lookup failed: ' . $e);
329
            return null;
330
        }
331
    }
332
333
334
    /**
335
     * Log out from this authentication source.
336
     *
337
     * This method either logs the user out from Negotiate or passes the
338
     * logout call to the fallback module.
339
     *
340
     * @param array &$state Information about the current logout operation.
341
     */
342
    public function logout(array &$state): void
343
    {
344
        // get the source that was used to authenticate
345
        $authId = $state['LogoutState']['negotiate:backend'];
346
        Logger::debug('Negotiate - logout has the following authId: "' . $authId . '"');
347
348
        if ($authId === null) {
349
            $session = Session::getSessionFromRequest();
350
            $session->setData('negotiate:disable', 'session', true, 0);
351
            parent::logout($state);
352
        } else {
353
            $source = Auth\Source::getById($authId);
354
            if ($source === null) {
355
                throw new Exception('Could not find authentication source with id ' . $authId);
356
            }
357
358
            $source->logout($state);
359
        }
360
    }
361
}
362