PluginProbe
Shibboleth / 2.5.2
Shibboleth v2.5.2
trunk 1.0 1.1 1.2 1.3 1.4 1.6 1.7 1.8 1.8.1 2.0 2.0.1 2.0.2 2.1 2.1.1 2.2 2.2.1 2.2.2 2.3 2.4 2.4.1 2.4.2 2.4.3 2.5.0 2.5.1 All 28 releases
shibboleth / shibboleth.php

shibboleth.php in Shibboleth 2.5.2, at shibboleth.php

1,224 lines 38.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Shibboleth
4 *
5 * @package shibboleth
6 *
7 * @wordpress-plugin
8 * Plugin Name: Shibboleth
9 * Plugin URI: https://wordpress.org/plugins/shibboleth/
10 * Description: Easily externalize user authentication to a <a href="https://www.incommon.org/software/shibboleth/">Shibboleth</a> Service Provider
11 * Author: Michael McNeill, Jonathan Champ, Michael Erlewine, Will Norris
12 * Version: 2.5.2
13 * Requires PHP: 5.6
14 * Requires at least: 4.0
15 * License: Apache-2.0
16 * Text Domain: shibboleth
17 */
18
19 define( 'SHIBBOLETH_MINIMUM_WP_VERSION', '4.0' );
20 define( 'SHIBBOLETH_MINIMUM_PHP_VERSION', '5.6' );
21 define( 'SHIBBOLETH_PLUGIN_VERSION', '2.5.2' );
22
23 /**
24 * Determine if this is a new install or upgrade and, if so, run the
25 * shibboleth_activate_plugin() function.
26 *
27 * @since 1.0
28 */
29 $plugin_version = get_site_option( 'shibboleth_plugin_version', '0' );
30 if ( SHIBBOLETH_PLUGIN_VERSION !== $plugin_version ) {
31 add_action( 'admin_init', 'shibboleth_activate_plugin' );
32 }
33
34 /**
35 * Determine if a constant is defined. If it is, return the value of the constant.
36 * If it isn't, return the value from get_site_option(). If you'd like to pass a default
37 * for get_site_option(), set $default to the requested default. If you'd like to check
38 * for arrays in constants, set $array to true. If you'd like to return that the object
39 * was obtained as a constant, set $compact to true and the result is an array. To get the
40 * value of the constant or option, look at the value key. To check if the value was
41 * retreived from a constant, look at the constant key.
42 *
43 * @since 2.1
44 * @param string $option Option identifier.
45 * @param bool $default Default value.
46 * @param bool $array If we expect the value to be an array.
47 * @param bool $compact If you want the constant and value returned as an array.
48 * @return mixed
49 */
50 function shibboleth_getoption( $option, $default = false, $array = false, $compact = false ) {
51 // If a constant is defined with the provided option name, get the value of the constant.
52 if ( defined( strtoupper( $option ) ) ) {
53 $value = constant( strtoupper( $option ) );
54 $constant = true;
55 } else {
56 // If no constant is set, just get the value from get_site_option().
57 $value = get_site_option( $option, $default );
58 $constant = false;
59 }
60
61 // If compact is set to true, we compact $value and $constant together for easy use.
62 if ( $compact ) {
63 return array(
64 $value,
65 $constant,
66 'value' => $value,
67 'constant' => $constant,
68 );
69 // Otherwise, just return the $value.
70 } else {
71 return $value;
72 }
73 }
74
75 /**
76 * HTTP and FastCGI friendly getenv() replacement that handles
77 * standard and REDIRECT_ environment variables, as well as HTTP
78 * headers. Users select which method to use to allow for the most
79 * secure configuration possible.
80 *
81 * @since 1.8
82 * @param string $var Environment variable.
83 * @return string|bool
84 */
85 function shibboleth_getenv( $var ) {
86 if ( empty( $var ) ) {
87 return false;
88 }
89
90 // Get the specified shibboleth attribute access method; if one isn't specified
91 // simply use standard environment variables since they're the safest.
92 $method = shibboleth_getoption( 'shibboleth_attribute_access_method', 'standard' );
93 $fallback = shibboleth_getoption( 'shibboleth_attribute_access_method_fallback' );
94
95 switch ( $method ) {
96 // Use standard by default for security.
97 case 'standard':
98 $var_method = '';
99 // Disable fallback to prevent the same variables from being checked twice.
100 $fallback = false;
101 break;
102 // If specified, use redirect.
103 case 'redirect':
104 $var_method = 'REDIRECT_';
105 break;
106 // If specified, use http.
107 case 'http':
108 $var_method = 'HTTP_';
109 break;
110 // If specified, use the custom specified method.
111 case 'custom':
112 $custom = shibboleth_getoption( 'shibboleth_attribute_custom_access_method', '' );
113 $var_method = $custom;
114 break;
115 // Otherwise, fall back to standard for security.
116 default:
117 $var_method = '';
118 // Disable fallback to prevent the same variables from being checked twice.
119 $fallback = false;
120 }
121
122 // Using the selected attribute access method, check all possible cases.
123 $var_under = str_replace( '-', '_', $var );
124 $var_upper = strtoupper( $var );
125 $var_under_upper = strtoupper( $var_under );
126
127 $check_vars = array(
128 $var_method . $var => true,
129 $var_method . $var_under => true,
130 $var_method . $var_upper => true,
131 $var_method . $var_under_upper => true,
132 );
133
134 // If fallback is enabled, we will add the standard environment variables to the end of the array to allow for fallback.
135 if ( $fallback ) {
136 $fallback_check_vars = array(
137 $var => true,
138 $var_under => true,
139 $var_upper => true,
140 $var_under_upper => true,
141 );
142
143 $check_vars = array_merge( $check_vars, $fallback_check_vars );
144 }
145
146 foreach ( $check_vars as $check_var => $true ) {
147 if ( isset( $_SERVER[ $check_var ] ) && false !== $_SERVER[ $check_var ] ) {
148 return sanitize_text_field( wp_unslash( $_SERVER[ $check_var ] ) );
149 }
150 }
151
152 return false;
153 }
154
155 /**
156 * Perform automatic login. This is based on the user not being logged in,
157 * an active session and the option being set to true.
158 *
159 * @since 1.6
160 */
161 function shibboleth_auto_login() {
162 $shibboleth_auto_login = shibboleth_getoption( 'shibboleth_auto_login' );
163
164 if ( ! is_user_logged_in() && shibboleth_session_active( true ) && $shibboleth_auto_login ) {
165 do_action( 'login_form_shibboleth' );
166
167 $userobj = wp_signon( '', true );
168 if ( ! is_wp_error( $userobj ) ) {
169 wp_safe_redirect( isset( $_SERVER['REQUEST_URI'] ) ? wp_unslash( $_SERVER['REQUEST_URI'] ) : '' );
170 exit();
171 }
172 }
173 }
174 add_action( 'init', 'shibboleth_auto_login' );
175
176 /**
177 * Activate the plugin. This registers default values for all of the
178 * Shibboleth options and attempts to add the appropriate mod_rewrite rules to
179 * WordPress's .htaccess file.
180 *
181 * @since 1.0
182 */
183 function shibboleth_activate_plugin() {
184 if ( version_compare( $GLOBALS['wp_version'], SHIBBOLETH_MINIMUM_WP_VERSION, '<' ) ) {
185 deactivate_plugins( plugin_basename( __FILE__ ) );
186 /* translators: 1: A version number */
187 wp_die( sprintf( esc_html( __( 'Shibboleth requires WordPress %1$s or higher!', 'shibboleth' ) ), esc_html( SHIBBOLETH_MINIMUM_WP_VERSION ) ) );
188 } elseif ( version_compare( PHP_VERSION, SHIBBOLETH_MINIMUM_PHP_VERSION, '<' ) ) {
189 deactivate_plugins( plugin_basename( __FILE__ ) );
190 /* translators: 1: A version number */
191 wp_die( sprintf( esc_html( __( 'Shibboleth requires PHP %1$s or higher!', 'shibboleth' ) ), esc_html( SHIBBOLETH_MINIMUM_PHP_VERSION ) ) );
192 }
193
194 if ( function_exists( 'switch_to_blog' ) ) {
195 if ( is_multisite() ) {
196 switch_to_blog( $GLOBALS['current_blog']->blog_id );
197 } else {
198 switch_to_blog( $GLOBALS['current_site']->blog_id );
199 }
200 }
201
202 add_site_option( 'shibboleth_login_url', get_site_option( 'home' ) . '/Shibboleth.sso/Login' );
203 add_site_option( 'shibboleth_default_to_shib_login', false );
204 add_site_option( 'shibboleth_auto_login', false );
205 add_site_option( 'shibboleth_logout_url', get_site_option( 'home' ) . '/Shibboleth.sso/Logout' );
206 add_site_option( 'shibboleth_attribute_access_method', 'standard' );
207 add_site_option( 'shibboleth_default_role', '' );
208 add_site_option( 'shibboleth_update_roles', false );
209 add_site_option( 'shibboleth_auto_combine_accounts', 'disallow' );
210 add_site_option( 'shibboleth_manually_combine_accounts', 'disallow' );
211 add_site_option( 'shibboleth_disable_local_auth', false );
212
213 $headers = array(
214 'username' => array(
215 'name' => 'eppn',
216 'managed' => 'on',
217 ),
218 'first_name' => array(
219 'name' => 'givenName',
220 'managed' => 'on',
221 ),
222 'last_name' => array(
223 'name' => 'sn',
224 'managed' => 'on',
225 ),
226 'nickname' => array(
227 'name' => 'eppn',
228 'managed' => 'off',
229 ),
230 'display_name' => array(
231 'name' => 'displayName',
232 'managed' => 'off',
233 ),
234 'email' => array(
235 'name' => 'mail',
236 'managed' => 'on',
237 ),
238 );
239 add_site_option( 'shibboleth_headers', $headers );
240
241 $roles = array(
242 'administrator' => array(
243 'header' => 'entitlement',
244 'value' => 'urn:mace:example.edu:entitlement:wordpress:admin',
245 ),
246 'author' => array(
247 'header' => 'affiliation',
248 'value' => 'faculty',
249 ),
250 );
251 add_site_option( 'shibboleth_roles', $roles );
252
253 shibboleth_insert_htaccess();
254
255 shibboleth_migrate_old_data();
256
257 update_site_option( 'shibboleth_plugin_version', SHIBBOLETH_PLUGIN_VERSION );
258
259 if ( function_exists( 'restore_current_blog' ) ) {
260 restore_current_blog();
261 }
262 }
263 register_activation_hook( __FILE__, 'shibboleth_activate_plugin' );
264
265 /**
266 * Cleanup .htaccess rules and delete the option shibboleth_plugin_version
267 * on deactivation.
268 *
269 * @since 1.0
270 */
271 function shibboleth_deactivate_plugin() {
272 shibboleth_remove_htaccess();
273 delete_site_option( 'shibboleth_plugin_version' );
274 }
275 register_deactivation_hook( __FILE__, 'shibboleth_deactivate_plugin' );
276
277
278 /**
279 * Update user meta from old IdP code to new IdP code.
280 *
281 * @since 2.5.0
282 * @param string $new_idp_code New IdP code.
283 * @param string $old_idp_code Old IdP code.
284 */
285 function shibboleth_update_idp_users( $new_idp_code, $old_idp_code ) {
286 // Update the shibboleth_account rows to have the new IdP code value.
287 $shibboleth_users = get_users(
288 array(
289 'meta_key' => 'shibboleth_account',
290 'fields' => 'ID',
291 )
292 );
293
294 foreach ( $shibboleth_users as $user_id ) {
295 update_user_meta( $user_id, 'shibboleth_account', $new_idp_code, $old_idp_code );
296 }
297 }
298
299 /**
300 * Migrate old (before version 1.9) data to a newer format that
301 * doesn't allow the default role to be stored with the rest of
302 * the role mappings.
303 */
304 function shibboleth_migrate_old_data() {
305 /**
306 * Moves data from before version 1.3 to a new header format,
307 * allowing each header to be marked as 'managed' individually
308 *
309 * @since 1.3
310 */
311 $managed = get_site_option( 'shibboleth_update_users', 'off' );
312 $headers = get_site_option( 'shibboleth_headers', array() );
313 $updated = false;
314 foreach ( $headers as $key => $value ) {
315 if ( is_string( $value ) ) {
316 $headers[ $key ] = array(
317 'name' => $value,
318 'managed' => $managed,
319 );
320 $updated = true;
321 }
322 }
323 if ( $updated ) {
324 update_site_option( 'shibboleth_headers', $headers );
325 }
326 delete_site_option( 'shibboleth_update_users' );
327
328 /**
329 * Changes to use plugin version instead of SVN revision.
330 *
331 * @since 1.8
332 */
333 delete_site_option( 'shibboleth_plugin_revision' );
334
335 /**
336 * Moves data from before version 1.9 to a new default role format,
337 * preventing a possible conflict with custom roles.
338 *
339 * @since 2.0
340 */
341 $roles = get_site_option( 'shibboleth_roles', array() );
342 if ( isset( $roles['default'] ) && '' !== $roles['default'] ) {
343 update_site_option( 'shibboleth_default_role', $roles['default'] );
344 update_site_option( 'shibboleth_create_accounts', true );
345 unset( $roles['default'] );
346 update_site_option( 'shibboleth_roles', $roles );
347 } elseif ( isset( $roles['default'] ) && '' === $roles['default'] ) {
348 update_site_option( 'shibboleth_default_role', 'subscriber' );
349 update_site_option( 'shibboleth_create_accounts', false );
350 unset( $roles['default'] );
351 update_site_option( 'shibboleth_roles', $roles );
352 }
353
354 /**
355 * Changes to support the shibboleth_getoption() function to match
356 * naming conventions of constants.
357 *
358 * @since 2.1
359 */
360 $attribute_access = get_site_option( 'shibboleth_attribute_access' );
361 if ( $attribute_access ) {
362 update_site_option( 'shibboleth_attribute_access_method', $attribute_access );
363 delete_site_option( 'shibboleth_attribute_access' );
364 }
365 $spoofkey = get_site_option( 'shibboleth_spoofkey' );
366 if ( $spoofkey ) {
367 update_site_option( 'shibboleth_spoof_key', $attribute_access );
368 delete_site_option( 'shibboleth_spoofkey' );
369 }
370 $default_login = get_site_option( 'shibboleth_default_login' );
371 if ( $default_login ) {
372 update_site_option( 'shibboleth_default_to_shib_login', $default_login );
373 delete_site_option( 'shibboleth_default_login' );
374 }
375
376 /**
377 * Convert from single to multiple IdP support.
378 *
379 * @since 2.5.0
380 */
381 $idps = get_site_option( 'shibboleth_idps', array() );
382 if ( empty( $idps ) ) {
383 $button_text = get_site_option( 'shibboleth_button_text', '' );
384 if ( 'Log in with Shibboleth' === $button_text ) {
385 $button_text = '';
386 }
387
388 $idps['preset'] = array(
389 'entity_id' => '',
390 'password_change_url' => get_site_option( 'shibboleth_password_change_url', '' ),
391 'password_reset_url' => get_site_option( 'shibboleth_password_reset_url', '' ),
392 'button_text' => $button_text,
393 );
394 update_site_option( 'shibboleth_idps', $idps );
395 delete_site_option( 'shibboleth_password_change_url' );
396 delete_site_option( 'shibboleth_password_reset_url' );
397 delete_site_option( 'shibboleth_button_text' );
398
399 // Update existing users to have the new IdP code value.
400 shibboleth_update_idp_users( 'preset', '1' );
401 }
402 }
403
404 /**
405 * Load Shibboleth admin hooks only on admin page loads.
406 *
407 * @since 1.3
408 */
409 function shibboleth_admin_hooks() {
410 if ( defined( 'WP_ADMIN' ) && WP_ADMIN === true ) {
411 require_once __DIR__ . '/options-admin.php';
412 require_once __DIR__ . '/options-user.php';
413 }
414 }
415 add_action( 'init', 'shibboleth_admin_hooks' );
416
417 /**
418 * Check if a Shibboleth session is active. If HTTP headers are being used
419 * we do additional testing to see if a spoofkey needs to be validated.
420 *
421 * @uses apply_filters calls 'shibboleth_session_active' before returning final result
422 * @param boolean $auto_login whether this is being triggered by an auto_login request or not.
423 * @return boolean|WP_Error
424 * @since 1.3
425 */
426 function shibboleth_session_active( $auto_login = false ) {
427 $active = false;
428 $method = shibboleth_getoption( 'shibboleth_attribute_access_method' );
429 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
430 $session = null;
431 if ( isset( $shib_headers['username']['name'] ) ) {
432 $session = shibboleth_getenv( $shib_headers['username']['name'] );
433 }
434
435 if ( $session && 'http' !== $method ) {
436 $active = true;
437 } elseif ( $session && 'http' === $method ) {
438 /**
439 * Handling HTTP header cases with a spoofkey to better protect against
440 * HTTP header spoofing.
441 *
442 * @see https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSpoofChecking
443 */
444 $spoofkey = shibboleth_getoption( 'shibboleth_spoof_key' );
445 $shibboleth_auto_login = shibboleth_getoption( 'shibboleth_auto_login' );
446
447 if ( false !== $spoofkey && '' !== $spoofkey ) {
448 $bypass = defined( 'SHIBBOLETH_BYPASS_SPOOF_CHECKING' ) && SHIBBOLETH_BYPASS_SPOOF_CHECKING;
449 $checkkey = shibboleth_getenv( 'Shib-Spoof-Check' );
450 if ( $checkkey === $spoofkey || $bypass ) {
451 $active = true;
452 } elseif ( $auto_login ) {
453 $active = false;
454 } else {
455 wp_die( esc_html( __( 'The Shibboleth request you submitted failed validation. Please contact your site administrator for further assistance.', 'shibboleth' ) ) );
456 }
457 } else {
458 $active = true;
459 }
460 }
461
462 $active = apply_filters( 'shibboleth_session_active', $active );
463 return $active;
464 }
465
466
467 /**
468 * Authenticate the user using Shibboleth. If a Shibboleth session is active,
469 * use the data provided by Shibboleth to log the user in. If a Shibboleth
470 * session is not active, redirect the user to the Shibboleth Session Initiator
471 * URL to initiate the session.
472 *
473 * @since 1.0
474 * @param null|WP_User|WP_Error $user WP_User if the user is authenticated. WP_Error or null otherwise.
475 * @param string $username Username or email address.
476 * @param string $password User password.
477 */
478 function shibboleth_authenticate( $user, $username, $password ) {
479 if ( shibboleth_session_active() ) {
480 return shibboleth_authenticate_user();
481 } else {
482 $idps = shibboleth_getoption( 'shibboleth_idps', array() );
483 $idp = key( $idps );
484 $redirect_to = null;
485
486 if ( isset( $_REQUEST['idp'] ) ) {
487 $idp = sanitize_text_field( wp_unslash( $_REQUEST['idp'] ) );
488 }
489
490 if ( isset( $_REQUEST['redirect_to'] ) ) {
491 $redirect_to = esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) );
492 }
493
494 $initiator_url = shibboleth_session_initiator_url( $redirect_to, $idp );
495 wp_redirect( $initiator_url );
496 exit;
497 }
498 }
499
500
501 /**
502 * When wp-login.php is loaded with 'action=shibboleth', hook Shibboleth
503 * into the WordPress authentication flow.
504 *
505 * @since 1.3
506 */
507 function shibboleth_login_form_shibboleth() {
508 add_filter( 'authenticate', 'shibboleth_authenticate', 10, 3 );
509 }
510 add_action( 'login_form_shibboleth', 'shibboleth_login_form_shibboleth' );
511
512 /**
513 * Get the associated password reset URL for the user.
514 *
515 * @since 2.5.0
516 * @param string $user_login Username.
517 * @return ?string
518 */
519 function shibboleth_get_password_reset_url( $user_login ) {
520 $user_idp = '';
521
522 $idps = shibboleth_getoption( 'shibboleth_idps', array() );
523
524 if ( ! empty( $user_login ) ) {
525 $user = get_user_by( 'login', $user_login );
526 if ( $user ) {
527 $user_idp = shibboleth_get_user_idp( $user->ID );
528
529 if ( empty( $user_idp ) ) {
530 return null;
531 }
532 }
533 } elseif ( count( $idps ) === 1 ) {
534 // If there is only one IdP, we can use it as the default.
535 $user_idp = key( $idps );
536 }
537
538 // Use the provided constant for all Shibboleth accounts.
539 if ( defined( 'SHIBBOLETH_PASSWORD_RESET_URL' ) ) {
540 return SHIBBOLETH_PASSWORD_RESET_URL;
541 }
542
543 if ( ! empty( $user_idp ) && isset( $idps[ $user_idp ] ) ) {
544 return $idps[ $user_idp ]['password_reset_url'];
545 }
546 }
547
548 /**
549 * If a Shibboleth user requests a password reset, and the Shibboleth password
550 * reset URL is set, redirect the user there.
551 *
552 * @since 1.3
553 * @param string $user_login Username.
554 */
555 function shibboleth_retrieve_password( $user_login ) {
556 $password_reset_url = shibboleth_get_password_reset_url( $user_login );
557
558 if ( ! empty( $password_reset_url ) ) {
559 wp_redirect( $password_reset_url );
560 exit;
561 }
562 }
563 add_action( 'retrieve_password', 'shibboleth_retrieve_password' );
564
565
566 /**
567 * If Shibboleth is the default login method, add 'action=shibboleth' to the
568 * WordPress login URL.
569 *
570 * @since 1.0
571 * @param string $login_url The login URL.
572 */
573 function shibboleth_login_url( $login_url ) {
574 $default = shibboleth_getoption( 'shibboleth_default_to_shib_login' );
575
576 if ( $default ) {
577 $idps = shibboleth_getoption( 'shibboleth_idps', array() );
578
579 // Only send people directly to Shibboleth if there is only 1 IdP.
580 if ( count( $idps ) === 1 ) {
581 $login_url = add_query_arg( 'action', 'shibboleth', $login_url );
582 $login_url = add_query_arg( 'idp', key( $idps ), $login_url );
583 }
584 }
585
586 return $login_url;
587 }
588 add_filter( 'login_url', 'shibboleth_login_url' );
589
590
591 /**
592 * If the Shibboleth logout URL is set and the user has an active Shibboleth
593 * session, log the user out of Shibboleth after logging them out of WordPress.
594 *
595 * @since 1.0
596 */
597 function shibboleth_logout() {
598 $logout_url = shibboleth_getoption( 'shibboleth_logout_url' );
599
600 if ( ! empty( $logout_url ) && shibboleth_session_active() ) {
601 wp_redirect( $logout_url );
602 exit;
603 }
604 }
605 add_action( 'wp_logout', 'shibboleth_logout', 20 );
606
607
608 /**
609 * Generate the URL to initiate Shibboleth login.
610 *
611 * @param string $redirect the final URL to redirect the user to after all login is complete.
612 * @param string $idp_code The chosen IdP to use for the login process.
613 * @return the URL to direct the user to in order to initiate Shibboleth login
614 * @uses apply_filters() Calls 'shibboleth_session_initiator_url' before returning session intiator URL
615 * @since 1.3
616 */
617 function shibboleth_session_initiator_url( $redirect = null, $idp_code = null ) {
618 // first build the target URL. This is the WordPress URL the user will be returned to after Shibboleth
619 // is done, and will handle actually logging the user into WordPress using the data provided by Shibboleth.
620 if ( function_exists( 'switch_to_blog' ) ) {
621 if ( ! empty( $GLOBALS['current_blog']->blog_id ) && $GLOBALS['current_blog']->blog_id !== $GLOBALS['current_site']->site_id ) {
622 switch_to_blog( $GLOBALS['current_blog']->blog_id );
623 } else {
624 switch_to_blog( $GLOBALS['current_site']->blog_id );
625 }
626 }
627
628 $target = site_url( 'wp-login.php' );
629
630 if ( function_exists( 'restore_current_blog' ) ) {
631 restore_current_blog();
632 }
633
634 $target = add_query_arg( 'action', 'shibboleth', $target );
635 if ( ! empty( $redirect ) ) {
636 $target = add_query_arg( 'redirect_to', rawurlencode( $redirect ), $target );
637 }
638
639 // now build the Shibboleth session initiator URL.
640 $initiator_url = shibboleth_getoption( 'shibboleth_login_url' );
641
642 $initiator_url = add_query_arg( 'target', rawurlencode( $target ), $initiator_url );
643
644 $idps = shibboleth_getoption( 'shibboleth_idps', array() );
645 if ( isset( $idps[ $idp_code ] ) && $idps[ $idp_code ]['entity_id'] ) {
646 $initiator_url = add_query_arg( 'entityID', rawurlencode( $idps[ $idp_code ]['entity_id'] ), $initiator_url );
647 }
648
649 $initiator_url = apply_filters( 'shibboleth_session_initiator_url', $initiator_url );
650
651 return $initiator_url;
652 }
653
654 /**
655 * Log Shibboleth message.
656 *
657 * @param string $message_type Message type.
658 * @param string $message Message.
659 * @since 2.4.3
660 */
661 function shibboleth_log_message( $message_type, $message ) {
662 static $shib_logging;
663
664 if ( ! isset( $shib_logging ) ) {
665 $shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
666 }
667
668 if ( ( defined( 'WP_DEBUG' ) && WP_DEBUG ) || in_array( $message_type, $shib_logging, true ) ) {
669 error_log( '[Shibboleth WordPress Plugin Logging] ' . $message );
670 }
671 }
672
673 /**
674 * Get a user's Shibboleth IdP.
675 *
676 * @param int $user_id The ID of the user.
677 * @return string
678 * @since 2.5.0
679 */
680 function shibboleth_get_user_idp( $user_id ) {
681 return get_user_meta( $user_id, 'shibboleth_account', true );
682 }
683
684 /**
685 * Set a user's Shibboleth IdP.
686 *
687 * @param int $user_id The ID of the user.
688 * @param string $user_idp IdP short label.
689 * @return bool
690 * @since 2.5.0
691 */
692 function shibboleth_set_user_idp( $user_id, $user_idp = null ) {
693 if ( empty( $user_idp ) ) {
694 $default_idp = null;
695
696 // Allow the environment variable name to be overriden.
697 $entity_id_env_var = 'Shib-Identity-Provider';
698 if ( defined( 'SHIBBOLETH_IDP_ENV_VAR' ) ) {
699 $entity_id_env_var = SHIBBOLETH_IDP_ENV_VAR;
700 }
701
702 $session_entity_id = shibboleth_getenv( $entity_id_env_var );
703
704 $idps = get_site_option( 'shibboleth_idps', array() );
705
706 foreach ( $idps as $idp_code => $idp_config ) {
707 if ( empty( $idp_config['entity_id'] ) ) {
708 $default_idp = $idp_code;
709 } elseif ( ! empty( $session_entity_id ) && $idp_config['entity_id'] === $session_entity_id ) {
710 $user_idp = $idp_code;
711 }
712 }
713
714 if ( empty( $user_idp ) ) {
715 $user_idp = $default_idp;
716 }
717 }
718
719 if ( ! empty( $user_idp ) ) {
720 update_user_meta( $user_id, 'shibboleth_account', $user_idp );
721 return true;
722 }
723
724 return false;
725 }
726
727 /**
728 * Authenticate the user based on the current Shibboleth headers.
729 *
730 * If the data available does not map to a WordPress role (based on the
731 * configured role-mapping), the user will not be allowed to login.
732 *
733 * If this is the first time we've seen this user (based on the username
734 * attribute), a new account will be created.
735 *
736 * Known users will have their profile data updated based on the Shibboleth
737 * data present if the plugin is configured to do so.
738 *
739 * @uses apply_filters() Calls 'shibboleth_override_username' before authenticating
740 * @uses apply_filters() Calls 'shibboleth_override_email' before authenticating
741 *
742 * @return WP_User|WP_Error authenticated user or error if unable to authenticate
743 * @since 1.0
744 */
745 function shibboleth_authenticate_user() {
746 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
747 $auto_combine_accounts = shibboleth_getoption( 'shibboleth_auto_combine_accounts' );
748 $manually_combine_accounts = shibboleth_getoption( 'shibboleth_manually_combine_accounts' );
749
750 $username = shibboleth_getenv( $shib_headers['username']['name'] );
751 $email = shibboleth_getenv( $shib_headers['email']['name'] );
752
753 /**
754 * Be VERY careful with the below two filters! They can lead to unintended
755 * consequences, such as multiple Shibboleth users mapping to the same
756 * WordPress user, or introducing security risks by improperly escaping
757 * and validating usernames and email addresses.
758 */
759
760 /**
761 * Override the username provided by Shibboleth.
762 *
763 * This can be used to escape or normalize the Shibboleth username.
764 *
765 * @param string $username
766 */
767 $username = apply_filters( 'shibboleth_override_username', $username );
768
769 /**
770 * Override the email address provided by Shibboleth.
771 *
772 * This can be used to escape or normalize the Shibboleth email address.
773 *
774 * @param string $email
775 */
776 $email = apply_filters( 'shibboleth_override_email', $email );
777
778 /**
779 * Allows a bypass mechanism for native Shibboleth authentication.
780 *
781 * Returning a non-null value from this filter will result in your value being
782 * returned to WordPress. You can prevent a user from being authenticated
783 * by returning a WP_Error object.
784 *
785 * @param null $auth
786 * @param string $username
787 */
788 $authenticate = apply_filters( 'shibboleth_authenticate_user', null, $username );
789 if ( null !== $authenticate ) {
790 return $authenticate;
791 }
792
793 // look up existing account by username, with email as a fallback.
794 $user_by = 'username';
795 $user = get_user_by( 'login', $username );
796 if ( ! $user ) {
797 $user_by = 'email';
798 $user = get_user_by( 'email', $email );
799 }
800
801 // if this account is not a Shibboleth account, then do account combine (if allowed).
802 if ( is_object( $user ) && $user->ID && ! shibboleth_get_user_idp( $user->ID ) ) {
803 $do_account_combine = false;
804 if ( 'username' === $user_by && ( 'allow' === $auto_combine_accounts || 'allow' === $manually_combine_accounts ) ) {
805 $do_account_combine = true;
806 } elseif ( 'bypass' === $auto_combine_accounts || 'bypass' === $manually_combine_accounts ) {
807 $do_account_combine = true;
808 }
809
810 if ( $do_account_combine ) {
811 if ( shibboleth_set_user_idp( $user->ID ) ) {
812 shibboleth_log_message( 'account_merge', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts automatically.' );
813 } else {
814 shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to automatically merge accounts. Reason: Unable to automatically determine IdP.' );
815 return new WP_Error( 'missing_data', __( 'Failed to match Identity Provider.', 'shibboleth' ) );
816 }
817 } elseif ( 'username' === $user_by ) {
818 shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to automatically merge accounts. Reason: An account already exists with this username.' );
819 return new WP_Error( 'invalid_username', __( 'An account already exists with this username.', 'shibboleth' ) );
820 } else {
821 shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to automatically merge accounts. Reason: An account already exists with this email.' );
822 return new WP_Error( 'invalid_email', __( 'An account already exists with this email.', 'shibboleth' ) );
823 }
824 }
825
826 // create account if new user.
827 if ( ! $user ) {
828 $user = shibboleth_create_new_user( $username, $email );
829 if ( is_wp_error( $user ) ) {
830 return new WP_Error( $user->get_error_code(), $user->get_error_message() );
831 }
832 }
833
834 if ( ! $user ) {
835 $error_message = 'Unable to create account based on data provided.';
836 shibboleth_log_message( 'account_create', 'ERROR: Unable to create account based on data provided.' );
837 return new WP_Error( 'missing_data', $error_message );
838 }
839
840 // update user data.
841 shibboleth_update_user_data( $user->ID );
842
843 $update = shibboleth_getoption( 'shibboleth_update_roles' );
844
845 if ( $update ) {
846 $user_role = shibboleth_get_user_role();
847 $user->set_role( $user_role );
848 shibboleth_log_message( 'role_update', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') role was updated to ' . $user_role . '.' );
849 do_action( 'shibboleth_set_user_roles', $user );
850 }
851
852 shibboleth_log_message( 'auth', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') successfully authenticated.' );
853 return $user;
854 }
855
856
857 /**
858 * Create a new WordPress user account, and mark it as a Shibboleth account.
859 *
860 * @param string $user_login login name for the new user.
861 * @param string $user_email email address for the new user.
862 * @return object WP_User object for newly created user.
863 * @since 1.0
864 */
865 function shibboleth_create_new_user( $user_login, $user_email ) {
866 $create_accounts = shibboleth_getoption( 'shibboleth_create_accounts' );
867 $user_role = shibboleth_get_user_role();
868
869 if ( ! empty( $create_accounts ) ) {
870 if ( empty( $user_login ) || empty( $user_email ) || '_no_account' === $user_role ) {
871 return null;
872 }
873
874 // create account and flag as a shibboleth account.
875 $user_id = wp_insert_user(
876 array(
877 'user_login' => $user_login,
878 'user_email' => $user_email,
879 'user_pass' => null,
880 )
881 );
882 if ( is_wp_error( $user_id ) ) {
883 shibboleth_log_message( 'account_create', 'ERROR: Unable to create account based on data provided. Reason: ' . $user_id->get_error_message() . '.' );
884 return new WP_Error( 'account_create_failed', $user_id->get_error_message() );
885 } else {
886 $user = new WP_User( $user_id );
887 shibboleth_set_user_idp( $user->ID );
888
889 wp_new_user_notification( $user_id );
890
891 // always update user data and role on account creation.
892 shibboleth_update_user_data( $user->ID, true );
893 $user->set_role( $user_role );
894 do_action( 'shibboleth_set_user_roles', $user );
895 shibboleth_log_message( 'account_create', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') was created with role ' . ( $user_role ? $user_role : 'none' ) . '.' );
896 return $user;
897 }
898 } else {
899 shibboleth_log_message( 'auth', 'ERROR: User account does not exist and account creation is disabled.' );
900 return new WP_Error( 'no_access', __( 'You do not have sufficient access.', 'shibboleth' ) );
901 }
902 }
903
904 /**
905 * Get the role the current user should have. This is determined by the role
906 * mapping configured for the plugin, and the Shibboleth headers present at the
907 * time of login.
908 *
909 * @return string the role the current user should have
910 * @uses apply_filters() Calls 'shibboleth_roles' after retrieving shibboleth_roles array
911 * @uses apply_filters() Calls 'shibboleth_user_role' before returning final user role
912 * @since 1.0
913 */
914 function shibboleth_get_user_role() {
915 // wp_roles() requires WordPress version 4.3 or higher.
916 if ( function_exists( 'wp_roles' ) ) {
917 $roles = wp_roles();
918 } else {
919 global $wp_roles;
920
921 if ( isset( $wp_roles ) ) {
922 $roles = $wp_roles;
923 } else {
924 $roles = new WP_Roles();
925 }
926 }
927
928 $shib_roles = apply_filters( 'shibboleth_roles', shibboleth_getoption( 'shibboleth_roles', array(), true ) );
929 $user_role = shibboleth_getoption( 'shibboleth_default_role' );
930
931 foreach ( $roles->role_names as $key => $name ) {
932 if ( isset( $shib_roles[ $key ]['header'] ) ) {
933 $role_header = $shib_roles[ $key ]['header'];
934 }
935 if ( isset( $shib_roles[ $key ]['value'] ) ) {
936 $role_value = $shib_roles[ $key ]['value'];
937 }
938 if ( empty( $role_header ) || empty( $role_value ) ) {
939 continue;
940 }
941 $values = explode( ';', shibboleth_getenv( $role_header ) );
942 if ( in_array( $role_value, $values, true ) ) {
943 $user_role = $key;
944 break;
945 }
946 }
947
948 $user_role = apply_filters( 'shibboleth_user_role', $user_role );
949
950 return $user_role;
951 }
952
953
954 /**
955 * Get the user fields that are managed by Shibboleth.
956 *
957 * @return Array user fields managed by Shibboleth
958 * @since 1.3
959 */
960 function shibboleth_get_managed_user_fields() {
961 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
962
963 $managed = array();
964
965 foreach ( $shib_headers as $name => $value ) {
966 if ( isset( $value['managed'] ) ) {
967 if ( $value['managed'] ) {
968 $managed[] = $name;
969 }
970 }
971 }
972
973 return $managed;
974 }
975
976
977 /**
978 * Update the user data for the specified user based on the current Shibboleth headers. Unless
979 * the 'force_update' parameter is true, only the user fields marked as 'managed' fields will be
980 * updated.
981 *
982 * @param int $user_id ID of the user to update.
983 * @param boolean $force_update force update of user data, regardless of 'managed' flag on fields.
984 * @uses apply_filters() Calls 'shibboleth_user_*' before setting user attributes,
985 * where '*' is one of: login, nicename, first_name, last_name,
986 * nickname, display_name, email
987 * @since 1.0
988 */
989 function shibboleth_update_user_data( $user_id, $force_update = false ) {
990 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
991
992 $user_fields = array(
993 'user_login' => 'username',
994 'user_nicename' => 'username',
995 'first_name' => 'first_name',
996 'last_name' => 'last_name',
997 'nickname' => 'nickname',
998 'display_name' => 'display_name',
999 'user_email' => 'email',
1000 );
1001
1002 $user_data = array(
1003 'ID' => $user_id,
1004 );
1005
1006 foreach ( $user_fields as $field => $header ) {
1007 $managed = false;
1008 if ( isset( $shib_headers[ $header ]['managed'] ) ) {
1009 $managed = $shib_headers[ $header ]['managed'];
1010 }
1011 if ( $force_update || $managed ) {
1012 $filter = 'shibboleth_' . ( strpos( $field, 'user_' ) === 0 ? '' : 'user_' ) . $field;
1013 $user_data[ $field ] = apply_filters( $filter, shibboleth_getenv( $shib_headers[ $header ]['name'] ) );
1014 }
1015 }
1016
1017 // Shibboleth users do not use their email address for authentication.
1018 add_filter( 'send_email_change_email', '__return_false' );
1019
1020 wp_update_user( $user_data );
1021 }
1022
1023
1024 /**
1025 * Sanitize the nicename using sanitize_title
1026 *
1027 * @since 1.4
1028 * @see http://wordpress.org/support/topic/377030
1029 */
1030 add_filter( 'shibboleth_user_nicename', 'sanitize_title' );
1031
1032 /**
1033 * Enqueue styles for the Shibboleth WordPress admin pages.
1034 *
1035 * @param string $hook_suffix The current admin page.
1036 */
1037 function shibboleth_admin_enqueue_scripts( $hook_suffix ) {
1038 if ( 'settings_page_shibboleth-options' !== $hook_suffix ) {
1039 return;
1040 }
1041
1042 wp_enqueue_style( 'shibboleth-options', plugins_url( 'assets/css/shibboleth-options.css', __FILE__ ), array(), SHIBBOLETH_PLUGIN_VERSION );
1043 }
1044 add_action( 'admin_enqueue_scripts', 'shibboleth_admin_enqueue_scripts' );
1045
1046 /**
1047 * Enqueues scripts and styles necessary for the Shibboleth button.
1048 *
1049 * @since 2.0
1050 */
1051 function shibboleth_login_enqueue_scripts() {
1052 global $action;
1053
1054 // Only add scripts for the login action to avoid breaking other forms.
1055 if ( 'login' === $action || 'shibboleth' === $action ) {
1056 wp_enqueue_style( 'shibboleth-login', plugins_url( 'assets/css/shibboleth_login_form.css', __FILE__ ), array( 'login' ), SHIBBOLETH_PLUGIN_VERSION );
1057 wp_enqueue_script( 'shibboleth-login', plugins_url( 'assets/js/shibboleth_login_form.js', __FILE__ ), array( 'jquery' ), SHIBBOLETH_PLUGIN_VERSION, true );
1058 }
1059 }
1060 add_action( 'login_enqueue_scripts', 'shibboleth_login_enqueue_scripts' );
1061
1062 /**
1063 * Prevents local WordPress authentication if disabled by an administrator.
1064 *
1065 * @since 2.0
1066 */
1067 function shibboleth_disable_login() {
1068 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
1069
1070 $bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
1071
1072 if ( $disable && ! $bypass ) {
1073 if ( isset( $_GET['action'] ) && 'lostpassword' === $_GET['action'] ) {
1074 // Disable the ability to reset passwords from wp-login.php.
1075 add_filter( 'allow_password_reset', '__return_false' );
1076 } elseif ( isset( $_POST['log'] ) || isset( $_POST['user_login'] ) ) {
1077 // Disable the ability to login using local authentication.
1078 wp_die( esc_html( __( 'Shibboleth authentication is required.', 'shibboleth' ) ) );
1079
1080 check_admin_referer( 'log-in' );
1081 }
1082 }
1083 }
1084 add_action( 'login_init', 'shibboleth_disable_login' );
1085
1086 /**
1087 * Disables wp-login.php login form if disabled by an administrator.
1088 *
1089 * @since 2.0
1090 */
1091 function shibboleth_disable_login_form() {
1092 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
1093 $bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
1094
1095 if ( $disable && ! $bypass ) {
1096 $password_reset_url = shibboleth_get_password_reset_url( '' );
1097 ?>
1098 <style type="text/css">
1099 .login #loginform p,
1100 .login #loginform .user-pass-wrap {
1101 display: none;
1102 }
1103 <?php if ( ! $password_reset_url ) { ?>
1104 .login #nav {
1105 display: none;
1106 }
1107 <?php } ?>
1108 </style>
1109 <?php
1110 }
1111 }
1112 add_action( 'login_enqueue_scripts', 'shibboleth_disable_login_form' );
1113
1114 /**
1115 * Updates the lost password URL, if specified.
1116 *
1117 * @param string $url original password reset URL.
1118 * @since 2.1
1119 */
1120 function shibboleth_custom_password_reset_url( $url ) {
1121 $password_reset_url = shibboleth_get_password_reset_url( '' );
1122
1123 if ( $password_reset_url ) {
1124 return $password_reset_url;
1125 } else {
1126 return $url;
1127 }
1128 }
1129 add_filter( 'lostpassword_url', 'shibboleth_custom_password_reset_url' );
1130
1131 /**
1132 * Add a "Log in with Shibboleth" link to the WordPress login form. This link
1133 * will be wrapped in a <p> with an id value of "shibboleth_login" so that
1134 * deployers can style this however they choose.
1135 *
1136 * @since 1.0
1137 */
1138 function shibboleth_login_form() {
1139 global $wp;
1140 $url = false;
1141 if ( ! empty( $wp->request ) ) {
1142 $url = wp_login_url( home_url( $wp->request ) );
1143 }
1144 $login_url = add_query_arg( 'action', 'shibboleth', $url );
1145 $login_url = remove_query_arg( 'reauth', $login_url );
1146 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
1147
1148 $idps = shibboleth_getoption( 'shibboleth_idps', array() );
1149
1150 $first = true;
1151
1152 foreach ( $idps as $idp_code => $idp ) {
1153 $idp_login_url = add_query_arg( 'idp', $idp_code, $login_url );
1154
1155 if ( defined( 'SHIBBOLETH_BUTTON_TEXT' ) && SHIBBOLETH_BUTTON_TEXT ) {
1156 $button_text = SHIBBOLETH_BUTTON_TEXT;
1157 } elseif ( ! empty( $idp['button_text'] ) ) {
1158 $button_text = $idp['button_text'];
1159 } else {
1160 $button_text = __( 'Log in with Shibboleth', 'shibboleth' );
1161 }
1162 ?>
1163 <div class="shibboleth-wrap" <?php echo ( $first && $disable ) ? 'style="margin-top:0;"' : ''; ?>>
1164 <?php
1165 if ( $first && ! $disable ) {
1166 ?>
1167 <div class="shibboleth-or">
1168 <span><?php esc_html_e( 'Or', 'shibboleth' ); ?></span>
1169 </div>
1170 <?php
1171 }
1172 ?>
1173 <a href="<?php echo esc_url( $idp_login_url ); ?>" rel="nofollow" class="shibboleth-button button button-primary default">
1174 <span class="shibboleth-icon"></span>
1175 <?php echo esc_html( $button_text ); ?>
1176 </a>
1177 </div>
1178 <?php
1179 $first = false;
1180 }
1181 }
1182 add_action( 'login_form', 'shibboleth_login_form' );
1183
1184
1185 /**
1186 * Insert directives into .htaccess file to enable Shibboleth Lazy Sessions.
1187 *
1188 * @since 1.0
1189 */
1190 function shibboleth_insert_htaccess() {
1191 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
1192
1193 if ( got_mod_rewrite() && ! $disabled ) {
1194 $htaccess = get_home_path() . '.htaccess';
1195 $rules = array( '<IfModule mod_shib>', 'AuthType shibboleth', 'Require shibboleth', '</IfModule>', '<IfModule mod_shib.c>', 'AuthType shibboleth', 'Require shibboleth', '</IfModule>', '<IfModule mod_shib.cpp>', 'AuthType shibboleth', 'Require shibboleth', '</IfModule>' );
1196 insert_with_markers( $htaccess, 'Shibboleth', $rules );
1197 }
1198 }
1199
1200
1201 /**
1202 * Remove directives from .htaccess file to enable Shibboleth Lazy Sessions.
1203 *
1204 * @since 1.1
1205 */
1206 function shibboleth_remove_htaccess() {
1207 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
1208
1209 if ( got_mod_rewrite() && ! $disabled ) {
1210 $htaccess = get_home_path() . '.htaccess';
1211 insert_with_markers( $htaccess, 'Shibboleth', array() );
1212 }
1213 }
1214
1215 /**
1216 * Load localization files.
1217 *
1218 * @since 1.7
1219 */
1220 function shibboleth_load_textdomain() {
1221 load_plugin_textdomain( 'shibboleth', false, dirname( plugin_basename( __FILE__ ) ) . '/localization/' );
1222 }
1223 add_action( 'plugins_loaded', 'shibboleth_load_textdomain' );
1224