CAS::__construct()   A
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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

219
                /** @scrutinizer ignore-type */ $config->getOptionalBoolean('enable_tls', false),
Loading history...
220
                $config->getOptionalBoolean('debug', false),
0 ignored issues
show
Bug introduced by
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

220
                /** @scrutinizer ignore-type */ $config->getOptionalBoolean('debug', false),
Loading history...
221
                $config->getOptionalInteger('timeout', 0),
222
                $config->getOptionalInteger('port', 389),
223
                $config->getOptionalBoolean('referrals', true)
0 ignored issues
show
Bug introduced by
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

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