Issues (5)

src/Auth/Source/CAS.php (4 issues)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\cas\Auth\Source;
6
7
use DOMXpath;
8
use Exception;
9
use SAML2\DOMDocumentFactory;
10
use SimpleSAML\Auth;
11
use SimpleSAML\Configuration;
12
use SimpleSAML\Module;
13
use SimpleSAML\Module\ldap\Auth\Ldap;
14
use SimpleSAML\Utils;
15
16
use function array_key_exists;
17
use function array_merge_recursive;
18
use function is_null;
19
use function preg_split;
20
use function strcmp;
21
use function var_export;
22
23
/**
24
 * Authenticate using CAS.
25
 *
26
 * Based on www/auth/login-cas.php by Mads Freek, RUC.
27
 *
28
 * @package SimpleSAMLphp
29
 */
30
31
class CAS extends Auth\Source
32
{
33
    /**
34
     * The string used to identify our states.
35
     */
36
    public const STAGE_INIT = '\SimpleSAML\Module\cas\Auth\Source\CAS.state';
37
38
    /**
39
     * The key of the AuthId field in the state.
40
     */
41
    public const AUTHID = '\SimpleSAML\Module\cas\Auth\Source\CAS.AuthId';
42
43
    /**
44
     * @var array<mixed> with ldap configuration
45
     */
46
    private array $ldapConfig;
47
48
    /**
49
     * @var array<mixed> cas configuration
50
     */
51
    private array $casConfig;
52
53
    /**
54
     * @var string cas chosen validation method
55
     */
56
57
    private string $validationMethod;
58
59
    /**
60
     * @var string cas login method
61
     */
62
    private string $loginMethod;
63
64
    /**
65
     * Constructor for this authentication source.
66
     *
67
     * @param array<mixed> $info  Information about this authentication source.
68
     * @param array<mixed> $config  Configuration.
69
     */
70
    public function __construct(array $info, array $config)
71
    {
72
        // Call the parent constructor first, as required by the interface
73
        parent::__construct($info, $config);
74
75
        if (!array_key_exists('cas', $config)) {
76
            throw new Exception('cas authentication source is not properly configured: missing [cas]');
77
        }
78
79
        if (!array_key_exists('ldap', $config)) {
80
            throw new Exception('ldap authentication source is not properly configured: missing [ldap]');
81
        }
82
83
        $this->casConfig = $config['cas'];
84
        $this->ldapConfig = $config['ldap'];
85
86
        if (isset($this->casConfig['serviceValidate'])) {
87
            $this->validationMethod = 'serviceValidate';
88
        } elseif (isset($this->casConfig['validate'])) {
89
            $this->validationMethod = 'validate';
90
        } else {
91
            throw new Exception("validate or serviceValidate not specified");
92
        }
93
94
        if (isset($this->casConfig['login'])) {
95
            $this->loginMethod = $this->casConfig['login'];
96
        } else {
97
            throw new Exception("cas login URL not specified");
98
        }
99
    }
100
101
102
    /**
103
     * This the most simple version of validating, this provides only authentication validation
104
     *
105
     * @param string $ticket
106
     * @param string $service
107
     *
108
     * @return array<mixed> username and attributes
109
     */
110
    private function casValidate(string $ticket, string $service): array
111
    {
112
        $httpUtils = new Utils\HTTP();
113
        $url = $httpUtils->addURLParameters($this->casConfig['validate'], [
114
            'ticket' => $ticket,
115
            'service' => $service,
116
        ]);
117
118
        /** @var string $result */
119
        $result = $httpUtils->fetch($url);
120
121
        /** @var string $res */
122
        $res = preg_split("/\r?\n/", $result);
123
124
        if (strcmp($res[0], "yes") == 0) {
125
            return [$res[1], []];
126
        } else {
127
            throw new Exception("Failed to validate CAS service ticket: $ticket");
128
        }
129
    }
130
131
132
    /**
133
     * Uses the cas service validate, this provides additional attributes
134
     *
135
     * @param string $ticket
136
     * @param string $service
137
     *
138
     * @return array<mixed> username and attributes
139
     */
140
    private function casServiceValidate(string $ticket, string $service): array
141
    {
142
        $httpUtils = new Utils\HTTP();
143
        $url = $httpUtils->addURLParameters(
144
            $this->casConfig['serviceValidate'],
145
            [
146
                'ticket' => $ticket,
147
                'service' => $service,
148
            ],
149
        );
150
        $result = $httpUtils->fetch($url);
151
152
        /** @var string $result */
153
        $dom = DOMDocumentFactory::fromString($result);
154
        $xPath = new DOMXpath($dom);
155
        $xPath->registerNamespace("cas", 'http://www.yale.edu/tp/cas');
156
157
        $success = $xPath->query("/cas:serviceResponse/cas:authenticationSuccess/cas:user");
158
        if ($success === false || $success->length === 0) {
159
            $failure = $xPath->evaluate("/cas:serviceResponse/cas:authenticationFailure");
160
            throw new Exception("Error when validating CAS service ticket: " . $failure->item(0)->textContent);
161
        } else {
162
            $attributes = [];
163
            if ($casattributes = $this->casConfig['attributes']) {
164
                // Some has attributes in the xml - attributes is a list of XPath expressions to get them
165
                foreach ($casattributes as $name => $query) {
166
                    /** @var \DOMNodeList<\DOMNode> $attrs */
167
                    $attrs = $xPath->query($query);
168
                    foreach ($attrs as $attrvalue) {
169
                        $attributes[$name][] = $attrvalue->textContent;
170
                    }
171
                }
172
            }
173
174
            $item = $success->item(0);
175
            if (is_null($item)) {
176
                throw new Exception("Error parsing serviceResponse.");
177
            }
178
            $casusername = $item->textContent;
179
180
            return [$casusername, $attributes];
181
        }
182
    }
183
184
185
    /**
186
     * Main validation method, redirects to correct method
187
     * (keeps finalStep clean)
188
     *
189
     * @param string $ticket
190
     * @param string $service
191
     * @return array<mixed> username and attributes
192
     */
193
    protected function casValidation(string $ticket, string $service): array
194
    {
195
        switch ($this->validationMethod) {
196
            case 'validate':
197
                return  $this->casValidate($ticket, $service);
198
            case 'serviceValidate':
199
                return $this->casServiceValidate($ticket, $service);
200
            default:
201
                throw new Exception("validate or serviceValidate not specified");
202
        }
203
    }
204
205
206
    /**
207
     * Called by linkback, to finish validate/ finish logging in.
208
     * @param array<mixed> $state
209
     */
210
    public function finalStep(array &$state): void
211
    {
212
        $ticket = $state['cas:ticket'];
213
        $stateId = Auth\State::saveState($state, self::STAGE_INIT);
214
        $service = Module::getModuleURL('cas/linkback.php', ['stateId' => $stateId]);
215
        list($username, $casattributes) = $this->casValidation($ticket, $service);
216
        $ldapattributes = [];
217
218
        $config = Configuration::loadFromArray(
219
            $this->ldapConfig,
220
            'Authentication source ' . var_export($this->authId, true),
221
        );
222
        if (!empty($this->ldapConfig['servers'])) {
223
            $ldap = new Ldap(
224
                $config->getString('servers'),
225
                $config->getOptionalBoolean('enable_tls', false),
0 ignored issues
show
It seems like $config->getOptionalBoolean('enable_tls', false) can also be of type null; however, parameter $enable_tls of SimpleSAML\Module\ldap\Auth\Ldap::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

225
                /** @scrutinizer ignore-type */ $config->getOptionalBoolean('enable_tls', false),
Loading history...
226
                $config->getOptionalBoolean('debug', false),
0 ignored issues
show
It seems like $config->getOptionalBoolean('debug', false) can also be of type null; however, parameter $debug of SimpleSAML\Module\ldap\Auth\Ldap::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
                /** @scrutinizer ignore-type */ $config->getOptionalBoolean('debug', false),
Loading history...
227
                $config->getOptionalInteger('timeout', 0),
228
                $config->getOptionalInteger('port', 389),
229
                $config->getOptionalBoolean('referrals', true),
0 ignored issues
show
It seems like $config->getOptionalBoolean('referrals', true) can also be of type null; however, parameter $referrals of SimpleSAML\Module\ldap\Auth\Ldap::__construct() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
                /** @scrutinizer ignore-type */ $config->getOptionalBoolean('referrals', true),
Loading history...
230
            );
231
            $ldapattributes = $ldap->validate($this->ldapConfig, $username);
232
            if ($ldapattributes === false) {
0 ignored issues
show
The condition $ldapattributes === false is always false.
Loading history...
233
                throw new Exception("Failed to authenticate against LDAP-server.");
234
            }
235
        }
236
        $attributes = array_merge_recursive($casattributes, $ldapattributes);
237
        $state['Attributes'] = $attributes;
238
    }
239
240
241
    /**
242
     * Log-in using cas
243
     *
244
     * @param array<mixed> &$state  Information about the current authentication.
245
     */
246
    public function authenticate(array &$state): void
247
    {
248
        // We are going to need the authId in order to retrieve this authentication source later
249
        $state[self::AUTHID] = $this->authId;
250
251
        $stateId = Auth\State::saveState($state, self::STAGE_INIT);
252
253
        $serviceUrl = Module::getModuleURL('cas/linkback.php', ['stateId' => $stateId]);
254
255
        $httpUtils = new Utils\HTTP();
256
        $httpUtils->redirectTrustedURL($this->loginMethod, ['service' => $serviceUrl]);
257
    }
258
259
260
    /**
261
     * Log out from this authentication source.
262
     *
263
     * This function should be overridden if the authentication source requires special
264
     * steps to complete a logout operation.
265
     *
266
     * If the logout process requires a redirect, the state should be saved. Once the
267
     * logout operation is completed, the state should be restored, and completeLogout
268
     * should be called with the state. If this operation can be completed without
269
     * showing the user a page, or redirecting, this function should return.
270
     *
271
     * @param array<mixed> &$state  Information about the current logout operation.
272
     */
273
    public function logout(array &$state): void
274
    {
275
        $logoutUrl = $this->casConfig['logout'];
276
277
        Auth\State::deleteState($state);
278
279
        // we want cas to log us out
280
        $httpUtils = new Utils\HTTP();
281
        $httpUtils->redirectTrustedURL($logoutUrl);
282
    }
283
}
284