PluginProbe
Shibboleth / 2.5.0
Shibboleth v2.5.0
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.0, at shibboleth.php

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