Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
44.54% |
53 / 119 |
|
44.44% |
4 / 9 |
CRAP | |
0.00% |
0 / 1 |
CAS | |
44.54% |
53 / 119 |
|
44.44% |
4 / 9 |
358.45 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validateConfig | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
7 | |||
authenticate | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
110 | |||
getSessionInitiator | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
isExpired | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
logout | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getRequiredAttributes | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getServiceBaseUrl | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
6 | |||
setupCAS | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 |
1 | <?php |
2 | |
3 | /** |
4 | * CAS authentication module. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Authentication |
25 | * @author Franck Borel <franck.borel@gbv.de> |
26 | * @author Demian Katz <demian.katz@villanova.edu> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org Main Page |
29 | */ |
30 | |
31 | namespace VuFind\Auth; |
32 | |
33 | use Laminas\Log\PsrLoggerAdapter; |
34 | use VuFind\Db\Entity\UserEntityInterface; |
35 | use VuFind\Exception\Auth as AuthException; |
36 | |
37 | use function constant; |
38 | |
39 | /** |
40 | * CAS authentication module. |
41 | * |
42 | * @category VuFind |
43 | * @package Authentication |
44 | * @author Tom Misilo <tmisilo@gmail.com> |
45 | * @author Franck Borel <franck.borel@gbv.de> |
46 | * @author Demian Katz <demian.katz@villanova.edu> |
47 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
48 | * @link https://vufind.org Main Page |
49 | */ |
50 | class CAS extends AbstractBase |
51 | { |
52 | use \VuFind\Log\LoggerAwareTrait; |
53 | |
54 | /** |
55 | * Already Setup phpCAS |
56 | * |
57 | * @var bool |
58 | */ |
59 | protected $phpCASSetup = false; |
60 | |
61 | /** |
62 | * Constructor |
63 | * |
64 | * @param ILSAuthenticator $ilsAuthenticator ILS authenticator |
65 | */ |
66 | public function __construct(protected ILSAuthenticator $ilsAuthenticator) |
67 | { |
68 | } |
69 | |
70 | /** |
71 | * Validate configuration parameters. This is a support method for getConfig(), |
72 | * so the configuration MUST be accessed using $this->config; do not call |
73 | * $this->getConfig() from within this method! |
74 | * |
75 | * @throws AuthException |
76 | * @return void |
77 | */ |
78 | protected function validateConfig() |
79 | { |
80 | $cas = $this->config->CAS; |
81 | // Throw an exception if the required server setting is missing. |
82 | if (!isset($cas->server)) { |
83 | throw new AuthException( |
84 | 'CAS server configuration parameter is not set.' |
85 | ); |
86 | } |
87 | |
88 | // Throw an exception if the required port setting is missing. |
89 | if (!isset($cas->port)) { |
90 | throw new AuthException( |
91 | 'CAS port configuration parameter is not set.' |
92 | ); |
93 | } |
94 | |
95 | // Throw an exception if the required context setting is missing. |
96 | if (!isset($cas->context)) { |
97 | throw new AuthException( |
98 | 'CAS context configuration parameter is not set.' |
99 | ); |
100 | } |
101 | |
102 | // Throw an exception if the required CACert setting is missing. |
103 | if (!isset($cas->CACert)) { |
104 | throw new AuthException( |
105 | 'CAS CACert configuration parameter is not set.' |
106 | ); |
107 | } |
108 | |
109 | // Throw an exception if the required login setting is missing. |
110 | if (!isset($cas->login)) { |
111 | throw new AuthException( |
112 | 'CAS login configuration parameter is not set.' |
113 | ); |
114 | } |
115 | |
116 | // Throw an exception if the required logout setting is missing. |
117 | if (!isset($cas->logout)) { |
118 | throw new AuthException( |
119 | 'CAS logout configuration parameter is not set.' |
120 | ); |
121 | } |
122 | } |
123 | |
124 | /** |
125 | * Attempt to authenticate the current user. Throws exception if login fails. |
126 | * |
127 | * @param \Laminas\Http\PhpEnvironment\Request $request Request object containing |
128 | * account credentials. |
129 | * |
130 | * @throws AuthException |
131 | * @return UserEntityInterface Object representing logged-in user. |
132 | */ |
133 | public function authenticate($request) |
134 | { |
135 | // Configure phpCAS |
136 | $cas = $this->getConfig()->CAS; |
137 | $casauth = $this->setupCAS(); |
138 | $casauth->forceAuthentication(); |
139 | |
140 | // Check if username is set. |
141 | if (isset($cas->username) && !empty($cas->username)) { |
142 | $username = $casauth->getAttribute($cas->username); |
143 | } else { |
144 | $username = $casauth->getUser(); |
145 | } |
146 | if (empty($username)) { |
147 | throw new AuthException('authentication_error_admin'); |
148 | } |
149 | |
150 | // If we made it this far, we should log in the user! |
151 | $userService = $this->getUserService(); |
152 | $user = $this->getOrCreateUserByUsername($username); |
153 | |
154 | // Has the user configured attributes to use for populating the user table? |
155 | $attribsToCheck = [ |
156 | 'cat_username', 'cat_password', 'email', 'lastname', 'firstname', |
157 | 'college', 'major', 'home_library', |
158 | ]; |
159 | $catPassword = null; |
160 | foreach ($attribsToCheck as $attribute) { |
161 | if (isset($cas->$attribute)) { |
162 | $value = $casauth->getAttribute($cas->$attribute); |
163 | if ($attribute == 'email') { |
164 | $userService->updateUserEmail($user, $value); |
165 | } elseif ($attribute != 'cat_password') { |
166 | $this->setUserValueByField($user, $attribute, $value ?? ''); |
167 | } else { |
168 | $catPassword = $value; |
169 | } |
170 | } |
171 | } |
172 | |
173 | // Save credentials if applicable. Note that we want to allow empty |
174 | // passwords (see https://github.com/vufind-org/vufind/pull/532), but |
175 | // we also want to be careful not to replace a non-blank password with a |
176 | // blank one in case the auth mechanism fails to provide a password on |
177 | // an occasion after the user has manually stored one. (For discussion, |
178 | // see https://github.com/vufind-org/vufind/pull/612). Note that in the |
179 | // (unlikely) scenario that a password can actually change from non-blank |
180 | // to blank, additional work may need to be done here. |
181 | if (!empty($catUsername = $user->getCatUsername())) { |
182 | $this->ilsAuthenticator->setUserCatalogCredentials( |
183 | $user, |
184 | $catUsername, |
185 | empty($catPassword) ? $this->ilsAuthenticator->getCatPasswordForUser($user) : $catPassword |
186 | ); |
187 | } |
188 | |
189 | // Save and return the user object: |
190 | $this->getUserService()->persistEntity($user); |
191 | return $user; |
192 | } |
193 | |
194 | /** |
195 | * Get the URL to establish a session (needed when the internal VuFind login |
196 | * form is inadequate). Returns false when no session initiator is needed. |
197 | * |
198 | * @param string $target Full URL where external authentication method should |
199 | * send user after login (some drivers may override this). |
200 | * |
201 | * @return bool|string |
202 | */ |
203 | public function getSessionInitiator($target) |
204 | { |
205 | $config = $this->getConfig(); |
206 | if (isset($config->CAS->target)) { |
207 | $casTarget = $config->CAS->target; |
208 | } else { |
209 | $casTarget = $target; |
210 | } |
211 | $append = (str_contains($casTarget, '?')) ? '&' : '?'; |
212 | $sessionInitiator = $config->CAS->login |
213 | . '?service=' . urlencode($casTarget) |
214 | . urlencode($append . 'auth_method=CAS'); |
215 | |
216 | return $sessionInitiator; |
217 | } |
218 | |
219 | /** |
220 | * Has the user's login expired? |
221 | * |
222 | * @return bool |
223 | */ |
224 | public function isExpired() |
225 | { |
226 | $config = $this->getConfig(); |
227 | if ( |
228 | isset($config->CAS->username) |
229 | && isset($config->CAS->logout) |
230 | ) { |
231 | $casauth = $this->setupCAS(); |
232 | if ($casauth->checkAuthentication() === false) { |
233 | return true; |
234 | } |
235 | } |
236 | return false; |
237 | } |
238 | |
239 | /** |
240 | * Perform cleanup at logout time. |
241 | * |
242 | * @param string $url URL to redirect user to after logging out. |
243 | * |
244 | * @return string Redirect URL (usually same as $url, but modified in |
245 | * some authentication modules). |
246 | */ |
247 | public function logout($url) |
248 | { |
249 | // If single log-out is enabled, use a special URL: |
250 | $config = $this->getConfig(); |
251 | if ( |
252 | isset($config->CAS->logout) |
253 | && !empty($config->CAS->logout) |
254 | ) { |
255 | $url = $config->CAS->logout . '?service=' . urlencode($url); |
256 | } |
257 | |
258 | // Send back the redirect URL (possibly modified): |
259 | return $url; |
260 | } |
261 | |
262 | /** |
263 | * Extract required user attributes from the configuration. |
264 | * |
265 | * @return array Only username and attribute-related values |
266 | */ |
267 | protected function getRequiredAttributes() |
268 | { |
269 | // Special case -- store username as-is to establish return array: |
270 | $sortedUserAttributes = []; |
271 | |
272 | // Now extract user attribute values: |
273 | $cas = $this->getConfig()->CAS; |
274 | foreach ($cas as $key => $value) { |
275 | if (preg_match('/userattribute_[0-9]{1,}/', $key)) { |
276 | $valueKey = 'userattribute_value_' . substr($key, 14); |
277 | $sortedUserAttributes[$value] = $cas->$valueKey ?? null; |
278 | |
279 | // Throw an exception if attributes are missing/empty. |
280 | if (empty($sortedUserAttributes[$value])) { |
281 | throw new AuthException( |
282 | 'User attribute value of ' . $value . ' is missing!' |
283 | ); |
284 | } |
285 | } |
286 | } |
287 | |
288 | return $sortedUserAttributes; |
289 | } |
290 | |
291 | /** |
292 | * Return an array of service base URLs for the CAS client. |
293 | * |
294 | * @return string[] |
295 | * @throws AuthException |
296 | */ |
297 | protected function getServiceBaseUrl(): array |
298 | { |
299 | $config = $this->getConfig(); |
300 | $cas = $config->CAS; |
301 | if (isset($cas->service_base_url)) { |
302 | return $cas->service_base_url->toArray(); |
303 | } elseif (isset($config->Site->url)) { |
304 | // fallback method |
305 | $siteUrl = parse_url($config->Site->url); |
306 | if (isset($siteUrl['scheme']) && isset($siteUrl['host'])) { |
307 | return [ |
308 | $siteUrl['scheme'] . '://' . $siteUrl['host'] . |
309 | (isset($siteUrl['port']) ? ':' . $siteUrl['port'] : ''), |
310 | ]; |
311 | } |
312 | } |
313 | throw new AuthException( |
314 | 'Valid CAS/service_base_url or Site/url config parameters are required.' |
315 | ); |
316 | } |
317 | |
318 | /** |
319 | * Establishes phpCAS Configuration and Enables the phpCAS Client |
320 | * |
321 | * @return object Returns phpCAS Object |
322 | */ |
323 | protected function setupCAS() |
324 | { |
325 | $casauth = new \phpCAS(); |
326 | |
327 | // Check to see if phpCAS has already been setup. If it has, than skip as |
328 | // client can only be called once. |
329 | if (!$this->phpCASSetup) { |
330 | $cas = $this->getConfig()->CAS; |
331 | |
332 | $casauth->setLogger(new PsrLoggerAdapter($this->logger)); |
333 | |
334 | if ($cas->debug ?? false) { |
335 | $casauth->setVerbose(true); |
336 | } |
337 | |
338 | $protocol = constant($cas->protocol ?? 'SAML_VERSION_1_1'); |
339 | |
340 | $casauth->client( |
341 | $protocol, |
342 | $cas->server, |
343 | (int)$cas->port, |
344 | $cas->context, |
345 | $this->getServiceBaseUrl(), |
346 | false |
347 | ); |
348 | |
349 | if (isset($cas->CACert) && !empty($cas->CACert)) { |
350 | $casauth->setCasServerCACert($cas->CACert); |
351 | } else { |
352 | $casauth->setNoCasServerValidation(); |
353 | } |
354 | |
355 | $this->phpCASSetup = true; |
356 | } |
357 | |
358 | return $casauth; |
359 | } |
360 | } |