PluginProbe
Shibboleth / 2.4.1
Shibboleth v2.4.1
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.4.1, at shibboleth.php

1,029 lines 33.9 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, mitcho (Michael 芳貴 Erlewine), Will Norris
12 * Version: 2.4.1
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.4.1' );
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 $_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( $_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_button_text', 'Log in with Shibboleth' );
206 add_site_option( 'shibboleth_auto_combine_accounts', 'disallow' );
207 add_site_option( 'shibboleth_manually_combine_accounts', 'disallow' );
208 add_site_option( 'shibboleth_disable_local_auth', false );
209
210 $headers = array(
211 'username' => array(
212 'name' => 'eppn',
213 'managed' => 'on',
214 ),
215 'first_name' => array(
216 'name' => 'givenName',
217 'managed' => 'on',
218 ),
219 'last_name' => array(
220 'name' => 'sn',
221 'managed' => 'on',
222 ),
223 'nickname' => array(
224 'name' => 'eppn',
225 'managed' => 'off',
226 ),
227 'display_name' => array(
228 'name' => 'displayName',
229 'managed' => 'off',
230 ),
231 'email' => array(
232 'name' => 'mail',
233 'managed' => 'on',
234 ),
235 );
236 add_site_option( 'shibboleth_headers', $headers );
237
238 $roles = array(
239 'administrator' => array(
240 'header' => 'entitlement',
241 'value' => 'urn:mace:example.edu:entitlement:wordpress:admin',
242 ),
243 'author' => array(
244 'header' => 'affiliation',
245 'value' => 'faculty',
246 ),
247 );
248 add_site_option( 'shibboleth_roles', $roles );
249
250 shibboleth_insert_htaccess();
251
252 shibboleth_migrate_old_data();
253
254 update_site_option( 'shibboleth_plugin_version', SHIBBOLETH_PLUGIN_VERSION );
255
256 if ( function_exists( 'restore_current_blog' ) ) {
257 restore_current_blog();
258 }
259 }
260 register_activation_hook( __FILE__, 'shibboleth_activate_plugin' );
261
262 /**
263 * Cleanup .htaccess rules and delete the option shibboleth_plugin_version
264 * on deactivation.
265 *
266 * @since 1.0
267 */
268 function shibboleth_deactivate_plugin() {
269 shibboleth_remove_htaccess();
270 delete_site_option( 'shibboleth_plugin_version' );
271 }
272 register_deactivation_hook( __FILE__, 'shibboleth_deactivate_plugin' );
273
274 /**
275 * Migrate old (before version 1.9) data to a newer format that
276 * doesn't allow the default role to be stored with the rest of
277 * the role mappings.
278 */
279 function shibboleth_migrate_old_data() {
280 /**
281 * Moves data from before version 1.3 to a new header format,
282 * allowing each header to be marked as 'managed' individually
283 *
284 * @since 1.3
285 */
286 $managed = get_site_option( 'shibboleth_update_users', 'off' );
287 $headers = get_site_option( 'shibboleth_headers', array() );
288 $updated = false;
289 foreach ( $headers as $key => $value ) {
290 if ( is_string( $value ) ) {
291 $headers[ $key ] = array(
292 'name' => $value,
293 'managed' => $managed,
294 );
295 $updated = true;
296 }
297 }
298 if ( $updated ) {
299 update_site_option( 'shibboleth_headers', $headers );
300 }
301 delete_site_option( 'shibboleth_update_users' );
302
303 /**
304 * Changes to use plugin version instead of SVN revision.
305 *
306 * @since 1.8
307 */
308 delete_site_option( 'shibboleth_plugin_revision' );
309
310 /**
311 * Moves data from before version 1.9 to a new default role format,
312 * preventing a possible conflict with custom roles.
313 *
314 * @since 2.0
315 */
316 $roles = get_site_option( 'shibboleth_roles', array() );
317 if ( isset( $roles['default'] ) && '' !== $roles['default'] ) {
318 update_site_option( 'shibboleth_testing', '1' );
319 update_site_option( 'shibboleth_default_role', $roles['default'] );
320 update_site_option( 'shibboleth_create_accounts', true );
321 unset( $roles['default'] );
322 update_site_option( 'shibboleth_roles', $roles );
323 } elseif ( isset( $roles['default'] ) && '' === $roles['default'] ) {
324 update_site_option( 'shibboleth_testing', '2' );
325 update_site_option( 'shibboleth_default_role', 'subscriber' );
326 update_site_option( 'shibboleth_create_accounts', false );
327 unset( $roles['default'] );
328 update_site_option( 'shibboleth_roles', $roles );
329 }
330
331 /**
332 * Changes to support the shibboleth_getoption() function to match
333 * naming conventions of constants.
334 *
335 * @since 2.1
336 */
337 $attribute_access = get_site_option( 'shibboleth_attribute_access' );
338 if ( $attribute_access ) {
339 update_site_option( 'shibboleth_attribute_access_method', $attribute_access );
340 delete_site_option( 'shibboleth_attribute_access' );
341 }
342 $spoofkey = get_site_option( 'shibboleth_spoofkey' );
343 if ( $spoofkey ) {
344 update_site_option( 'shibboleth_spoof_key', $attribute_access );
345 delete_site_option( 'shibboleth_spoofkey' );
346 }
347 $default_login = get_site_option( 'shibboleth_default_login' );
348 if ( $default_login ) {
349 update_site_option( 'shibboleth_default_to_shib_login', $default_login );
350 delete_site_option( 'shibboleth_default_login' );
351 }
352 }
353
354 /**
355 * Load Shibboleth admin hooks only on admin page loads.
356 *
357 * @since 1.3
358 */
359 function shibboleth_admin_hooks() {
360 if ( defined( 'WP_ADMIN' ) && WP_ADMIN === true ) {
361 require_once dirname( __FILE__ ) . '/options-admin.php';
362 require_once dirname( __FILE__ ) . '/options-user.php';
363 }
364 }
365 add_action( 'init', 'shibboleth_admin_hooks' );
366
367 /**
368 * Check if a Shibboleth session is active. If HTTP headers are being used
369 * we do additional testing to see if a spoofkey needs to be validated.
370 *
371 * @uses apply_filters calls 'shibboleth_session_active' before returning final result
372 * @param boolean $auto_login whether this is being triggered by an auto_login request or not.
373 * @return boolean|WP_Error
374 * @since 1.3
375 */
376 function shibboleth_session_active( $auto_login = false ) {
377 $active = false;
378 $method = shibboleth_getoption( 'shibboleth_attribute_access_method' );
379 $session = shibboleth_getenv( 'Shib-Session-ID' );
380
381 if ( $session && 'http' !== $method ) {
382 $active = true;
383 } elseif ( $session && 'http' === $method ) {
384 /**
385 * Handling HTTP header cases with a spoofkey to better protect against
386 * HTTP header spoofing.
387 *
388 * @see https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSpoofChecking
389 */
390 $spoofkey = shibboleth_getoption( 'shibboleth_spoof_key' );
391 $shibboleth_auto_login = shibboleth_getoption( 'shibboleth_auto_login' );
392
393 if ( false !== $spoofkey && '' !== $spoofkey ) {
394 $bypass = defined( 'SHIBBOLETH_BYPASS_SPOOF_CHECKING' ) && SHIBBOLETH_BYPASS_SPOOF_CHECKING;
395 $checkkey = shibboleth_getenv( 'Shib-Spoof-Check' );
396 if ( $checkkey === $spoofkey || $bypass ) {
397 $active = true;
398 } elseif ( $auto_login ) {
399 $active = false;
400 } else {
401 wp_die( esc_html( __( 'The Shibboleth request you submitted failed validation. Please contact your site administrator for further assistance.', 'shibboleth' ) ) );
402 }
403 } else {
404 $active = true;
405 }
406 }
407
408 $active = apply_filters( 'shibboleth_session_active', $active );
409 return $active;
410 }
411
412
413 /**
414 * Authenticate the user using Shibboleth. If a Shibboleth session is active,
415 * use the data provided by Shibboleth to log the user in. If a Shibboleth
416 * session is not active, redirect the user to the Shibboleth Session Initiator
417 * URL to initiate the session.
418 *
419 * @since 1.0
420 * @param null|WP_User|WP_Error $user WP_User if the user is authenticated. WP_Error or null otherwise.
421 * @param string $username Username or email address.
422 * @param string $password User password.
423 */
424 function shibboleth_authenticate( $user, $username, $password ) {
425 if ( shibboleth_session_active() ) {
426 return shibboleth_authenticate_user();
427 } else {
428 if ( isset( $_REQUEST['redirect_to'] ) ) {
429 $initiator_url = shibboleth_session_initiator_url( $_REQUEST['redirect_to'] );
430 } else {
431 $initiator_url = shibboleth_session_initiator_url();
432 }
433 wp_redirect( $initiator_url );
434 exit;
435 }
436 }
437
438
439 /**
440 * When wp-login.php is loaded with 'action=shibboleth', hook Shibboleth
441 * into the WordPress authentication flow.
442 *
443 * @since 1.3
444 */
445 function shibboleth_login_form_shibboleth() {
446 add_filter( 'authenticate', 'shibboleth_authenticate', 10, 3 );
447 }
448 add_action( 'login_form_shibboleth', 'shibboleth_login_form_shibboleth' );
449
450
451 /**
452 * If a Shibboleth user requests a password reset, and the Shibboleth password
453 * reset URL is set, redirect the user there.
454 *
455 * @since 1.3
456 * @param string $user_login Username.
457 */
458 function shibboleth_retrieve_password( $user_login ) {
459 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url' );
460
461 if ( ! empty( $password_reset_url ) ) {
462 $user = get_user_by( 'login', $user_login );
463 if ( $user && get_user_meta( $user->ID, 'shibboleth_account' ) ) {
464 wp_redirect( $password_reset_url );
465 exit;
466 }
467 }
468 }
469 add_action( 'retrieve_password', 'shibboleth_retrieve_password' );
470
471
472 /**
473 * If Shibboleth is the default login method, add 'action=shibboleth' to the
474 * WordPress login URL.
475 *
476 * @since 1.0
477 * @param string $login_url The login URL.
478 */
479 function shibboleth_login_url( $login_url ) {
480 $default = shibboleth_getoption( 'shibboleth_default_to_shib_login' );
481
482 if ( $default ) {
483 $login_url = add_query_arg( 'action', 'shibboleth', $login_url );
484 }
485 return $login_url;
486 }
487 add_filter( 'login_url', 'shibboleth_login_url' );
488
489
490 /**
491 * If the Shibboleth logout URL is set and the user has an active Shibboleth
492 * session, log the user out of Shibboleth after logging them out of WordPress.
493 *
494 * @since 1.0
495 */
496 function shibboleth_logout() {
497 $logout_url = shibboleth_getoption( 'shibboleth_logout_url' );
498
499 if ( ! empty( $logout_url ) && shibboleth_session_active() ) {
500 wp_redirect( $logout_url );
501 exit;
502 }
503 }
504 add_action( 'wp_logout', 'shibboleth_logout', 20 );
505
506
507 /**
508 * Generate the URL to initiate Shibboleth login.
509 *
510 * @param string $redirect the final URL to redirect the user to after all login is complete.
511 * @return the URL to direct the user to in order to initiate Shibboleth login
512 * @uses apply_filters() Calls 'shibboleth_session_initiator_url' before returning session intiator URL
513 * @since 1.3
514 */
515 function shibboleth_session_initiator_url( $redirect = null ) {
516
517 // first build the target URL. This is the WordPress URL the user will be returned to after Shibboleth
518 // is done, and will handle actually logging the user into WordPress using the data provided by Shibboleth
519 if ( function_exists( 'switch_to_blog' ) ) {
520 if ( ! empty( $GLOBALS['current_blog']->blog_id ) && $GLOBALS['current_blog']->blog_id !== $GLOBALS['current_site']->site_id ) {
521 switch_to_blog( $GLOBALS['current_blog']->blog_id );
522 } else {
523 switch_to_blog( $GLOBALS['current_site']->blog_id );
524 }
525 }
526
527 $target = site_url( 'wp-login.php' );
528
529 if ( function_exists( 'restore_current_blog' ) ) {
530 restore_current_blog();
531 }
532
533 $target = add_query_arg( 'action', 'shibboleth', $target );
534 if ( ! empty( $redirect ) ) {
535 $target = add_query_arg( 'redirect_to', rawurlencode( $redirect ), $target );
536 }
537
538 // now build the Shibboleth session initiator URL
539 $initiator_url = shibboleth_getoption( 'shibboleth_login_url' );
540
541 $initiator_url = add_query_arg( 'target', rawurlencode( $target ), $initiator_url );
542
543 $initiator_url = apply_filters( 'shibboleth_session_initiator_url', $initiator_url );
544
545 return $initiator_url;
546 }
547
548
549 /**
550 * Authenticate the user based on the current Shibboleth headers.
551 *
552 * If the data available does not map to a WordPress role (based on the
553 * configured role-mapping), the user will not be allowed to login.
554 *
555 * If this is the first time we've seen this user (based on the username
556 * attribute), a new account will be created.
557 *
558 * Known users will have their profile data updated based on the Shibboleth
559 * data present if the plugin is configured to do so.
560 *
561 * @uses apply_filters() Calls 'shibboleth_override_username' before authenticating
562 * @uses apply_filters() Calls 'shibboleth_override_email' before authenticating
563 *
564 * @return WP_User|WP_Error authenticated user or error if unable to authenticate
565 * @since 1.0
566 */
567 function shibboleth_authenticate_user() {
568 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
569 $shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
570 $auto_combine_accounts = shibboleth_getoption( 'shibboleth_auto_combine_accounts' );
571 $manually_combine_accounts = shibboleth_getoption( 'shibboleth_manually_combine_accounts' );
572
573 $username = shibboleth_getenv( $shib_headers['username']['name'] );
574 $email = shibboleth_getenv( $shib_headers['email']['name'] );
575
576 /**
577 * Be VERY careful with the below two filters! They can lead to unintended
578 * consequences, such as multiple Shibboleth users mapping to the same
579 * WordPress user, or introducing security risks by improperly escaping
580 * and validating usernames and email addresses.
581 */
582
583 /**
584 * Override the username provided by Shibboleth.
585 *
586 * This can be used to escape or normalize the Shibboleth username.
587 *
588 * @param string $username
589 */
590 $username = apply_filters( 'shibboleth_override_username', $username );
591
592 /**
593 * Override the email address provided by Shibboleth.
594 *
595 * This can be used to escape or normalize the Shibboleth email address.
596 *
597 * @param string $email
598 */
599 $email = apply_filters( 'shibboleth_override_email', $email );
600
601 /**
602 * Allows a bypass mechanism for native Shibboleth authentication.
603 *
604 * Returning a non-null value from this filter will result in your value being
605 * returned to WordPress. You can prevent a user from being authenticated
606 * by returning a WP_Error object.
607 *
608 * @param null $auth
609 * @param string $username
610 */
611 $authenticate = apply_filters( 'shibboleth_authenticate_user', null, $username );
612 if ( null !== $authenticate ) {
613 return $authenticate;
614 }
615
616 // look up existing account by username, with email as a fallback
617 $user_by = 'username';
618 $user = get_user_by( 'login', $username );
619 if ( ! $user ) {
620 $user_by = 'email';
621 $user = get_user_by( 'email', $email );
622 }
623
624 // if this account is not a Shibboleth account, then do account combine (if allowed)
625 if ( is_object( $user ) && $user->ID && ! get_user_meta( $user->ID, 'shibboleth_account' ) ) {
626 $do_account_combine = false;
627 if ( 'username' === $user_by && ( 'allow' === $auto_combine_accounts || 'allow' === $manually_combine_accounts ) ) {
628 $do_account_combine = true;
629 } elseif ( 'bypass' === $auto_combine_accounts || 'bypass' === $manually_combine_accounts ) {
630 $do_account_combine = true;
631 }
632
633 if ( $do_account_combine ) {
634 update_user_meta( $user->ID, 'shibboleth_account', true );
635 if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
636 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts automatically.' );
637 }
638 } elseif ( 'username' === $user_by ) {
639 if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
640 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to automatically merge accounts. Reason: An account already exists with this username.' );
641 }
642 return new WP_Error( 'invalid_username', __( 'An account already exists with this username.', 'shibboleth' ) );
643 } else {
644 if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
645 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to automatically merge accounts. Reason: An account already exists with this email.' );
646 }
647 return new WP_Error( 'invalid_email', __( 'An account already exists with this email.', 'shibboleth' ) );
648 }
649 }
650
651 // create account if new user
652 if ( ! $user ) {
653 $user = shibboleth_create_new_user( $username, $email );
654 if ( is_wp_error( $user ) ) {
655 return new WP_Error( $user->get_error_code(), $user->get_error_message() );
656 }
657 }
658
659 if ( ! $user ) {
660 $error_message = 'Unable to create account based on data provided.';
661 if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
662 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: Unable to create account based on data provided.' );
663 }
664 return new WP_Error( 'missing_data', $error_message );
665 }
666
667 // update user data
668 shibboleth_update_user_data( $user->ID );
669
670 $update = shibboleth_getoption( 'shibboleth_update_roles' );
671
672 if ( $update ) {
673 $user_role = shibboleth_get_user_role();
674 $user->set_role( $user_role );
675 if ( in_array( 'role_update', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
676 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') role was updated to ' . $user_role . '.' );
677 }
678 do_action( 'shibboleth_set_user_roles', $user );
679 }
680
681 if ( in_array( 'auth', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
682 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') successfully authenticated.' );
683 }
684 return $user;
685 }
686
687
688 /**
689 * Create a new WordPress user account, and mark it as a Shibboleth account.
690 *
691 * @param string $user_login login name for the new user.
692 * @param string $user_email email address for the new user.
693 * @return object WP_User object for newly created user.
694 * @since 1.0
695 */
696 function shibboleth_create_new_user( $user_login, $user_email ) {
697 $create_accounts = shibboleth_getoption( 'shibboleth_create_accounts' );
698 $shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
699 $user_role = shibboleth_get_user_role();
700
701 if ( ! empty( $create_accounts ) ) {
702 if ( empty( $user_login ) || empty( $user_email ) || '_no_account' === $user_role ) {
703 return null;
704 }
705
706 // create account and flag as a shibboleth account
707 $user_id = wp_insert_user(
708 array(
709 'user_login' => $user_login,
710 'user_email' => $user_email,
711 'user_pass' => null,
712 )
713 );
714 if ( is_wp_error( $user_id ) ) {
715 if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
716 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: Unable to create account based on data provided. Reason: ' . $user_id->get_error_message() . '.' );
717 }
718 return new WP_Error( 'account_create_failed', $user_id->get_error_message() );
719 } else {
720 $user = new WP_User( $user_id );
721 update_user_meta( $user->ID, 'shibboleth_account', true );
722
723 // always update user data and role on account creation
724 shibboleth_update_user_data( $user->ID, true );
725 $user->set_role( $user_role );
726 do_action( 'shibboleth_set_user_roles', $user );
727 if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
728 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') was created with role ' . ( $user_role ? $user_role : 'none' ) . '.' );
729 }
730 return $user;
731 }
732 } else {
733 if ( in_array( 'auth', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
734 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User account does not exist and account creation is disabled.' );
735 }
736 return new WP_Error( 'no_access', __( 'You do not have sufficient access.' ) );
737 }
738 }
739
740 /**
741 * Get the role the current user should have. This is determined by the role
742 * mapping configured for the plugin, and the Shibboleth headers present at the
743 * time of login.
744 *
745 * @return string the role the current user should have
746 * @uses apply_filters() Calls 'shibboleth_roles' after retrieving shibboleth_roles array
747 * @uses apply_filters() Calls 'shibboleth_user_role' before returning final user role
748 * @since 1.0
749 */
750 function shibboleth_get_user_role() {
751 // wp_roles() requires WordPress version 4.3 or higher.
752 if ( function_exists( 'wp_roles' ) ) {
753 $roles = wp_roles();
754 } else {
755 global $wp_roles;
756
757 if ( isset( $wp_roles ) ) {
758 $roles = $wp_roles;
759 } else {
760 $roles = new WP_Roles();
761 }
762 }
763
764 $shib_roles = apply_filters( 'shibboleth_roles', shibboleth_getoption( 'shibboleth_roles', array(), true ) );
765 $user_role = shibboleth_getoption( 'shibboleth_default_role' );
766
767 foreach ( $roles->role_names as $key => $name ) {
768 if ( isset( $shib_roles[ $key ]['header'] ) ) {
769 $role_header = $shib_roles[ $key ]['header'];
770 }
771 if ( isset( $shib_roles[ $key ]['value'] ) ) {
772 $role_value = $shib_roles[ $key ]['value'];
773 }
774 if ( empty( $role_header ) || empty( $role_value ) ) {
775 continue;
776 }
777 $values = explode( ';', shibboleth_getenv( $role_header ) );
778 if ( in_array( $role_value, $values, true ) ) {
779 $user_role = $key;
780 break;
781 }
782 }
783
784 $user_role = apply_filters( 'shibboleth_user_role', $user_role );
785
786 return $user_role;
787 }
788
789
790 /**
791 * Get the user fields that are managed by Shibboleth.
792 *
793 * @return Array user fields managed by Shibboleth
794 * @since 1.3
795 */
796 function shibboleth_get_managed_user_fields() {
797 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
798
799 $managed = array();
800
801 foreach ( $shib_headers as $name => $value ) {
802 if ( isset( $value['managed'] ) ) {
803 if ( $value['managed'] ) {
804 $managed[] = $name;
805 }
806 }
807 }
808
809 return $managed;
810 }
811
812
813 /**
814 * Update the user data for the specified user based on the current Shibboleth headers. Unless
815 * the 'force_update' parameter is true, only the user fields marked as 'managed' fields will be
816 * updated.
817 *
818 * @param int $user_id ID of the user to update.
819 * @param boolean $force_update force update of user data, regardless of 'managed' flag on fields.
820 * @uses apply_filters() Calls 'shibboleth_user_*' before setting user attributes,
821 * where '*' is one of: login, nicename, first_name, last_name,
822 * nickname, display_name, email
823 * @since 1.0
824 */
825 function shibboleth_update_user_data( $user_id, $force_update = false ) {
826 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
827
828 $user_fields = array(
829 'user_login' => 'username',
830 'user_nicename' => 'username',
831 'first_name' => 'first_name',
832 'last_name' => 'last_name',
833 'nickname' => 'nickname',
834 'display_name' => 'display_name',
835 'user_email' => 'email',
836 );
837
838 $user_data = array(
839 'ID' => $user_id,
840 );
841
842 foreach ( $user_fields as $field => $header ) {
843 $managed = false;
844 if ( isset( $shib_headers[ $header ]['managed'] ) ) {
845 $managed = $shib_headers[ $header ]['managed'];
846 }
847 if ( $force_update || $managed ) {
848 $filter = 'shibboleth_' . ( strpos( $field, 'user_' ) === 0 ? '' : 'user_' ) . $field;
849 $user_data[ $field ] = apply_filters( $filter, shibboleth_getenv( $shib_headers[ $header ]['name'] ) );
850 }
851 }
852
853 // Shibboleth users do not use their email address for authentication.
854 add_filter( 'send_email_change_email', '__return_false' );
855
856 wp_update_user( $user_data );
857 }
858
859
860 /**
861 * Sanitize the nicename using sanitize_title
862 *
863 * @since 1.4
864 * @see http://wordpress.org/support/topic/377030
865 */
866 add_filter( 'shibboleth_user_nicename', 'sanitize_title' );
867
868 /**
869 * Enqueues scripts and styles necessary for the Shibboleth button.
870 *
871 * @since 2.0
872 */
873 function shibboleth_login_enqueue_scripts() {
874 global $action;
875
876 // Only add scripts for the login action to avoid breaking other forms.
877 if ( 'login' === $action || 'shibboleth' === $action ) {
878 wp_enqueue_style( 'shibboleth-login', plugins_url( 'assets/css/shibboleth_login_form.css', __FILE__ ), array( 'login' ), SHIBBOLETH_PLUGIN_VERSION );
879 wp_enqueue_script( 'shibboleth-login', plugins_url( 'assets/js/shibboleth_login_form.js', __FILE__ ), array( 'jquery' ), SHIBBOLETH_PLUGIN_VERSION, true );
880 }
881 }
882 add_action( 'login_enqueue_scripts', 'shibboleth_login_enqueue_scripts' );
883
884 /**
885 * Prevents local WordPress authentication if disabled by an administrator.
886 *
887 * @since 2.0
888 */
889 function shibboleth_disable_login() {
890 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
891
892 $bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
893
894 if ( $disable && ! $bypass ) {
895 if ( isset( $_GET['action'] ) && 'lostpassword' === $_GET['action'] ) {
896 // Disable the ability to reset passwords from wp-login.php
897 add_filter( 'allow_password_reset', '__return_false' );
898 } elseif ( isset( $_POST['log'] ) || isset( $_POST['user_login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
899 // Disable the ability to login using local authentication
900 wp_die( esc_html( __( 'Shibboleth authentication is required.', 'shibboleth' ) ) );
901 }
902 }
903 }
904 add_action( 'login_init', 'shibboleth_disable_login' );
905
906 /**
907 * Disables wp-login.php login form if disabled by an administrator.
908 *
909 * @since 2.0
910 */
911 function shibboleth_disable_login_form() {
912 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
913 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
914
915 $bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
916
917 if ( $disable && ! $bypass ) {
918 ?>
919 <style type="text/css">
920 .login #loginform p,
921 .login #loginform .user-pass-wrap {
922 display: none;
923 }
924 <?php if ( ! $password_reset_url ) { ?>
925 .login #nav {
926 display: none;
927 }
928 <?php } ?>
929 </style>
930 <?php
931 }
932 }
933 add_action( 'login_enqueue_scripts', 'shibboleth_disable_login_form' );
934
935 /**
936 * Updates the lost password URL, if specified.
937 *
938 * @param string $url original password reset URL.
939 * @since 2.1
940 */
941 function shibboleth_custom_password_reset_url( $url ) {
942 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
943
944 if ( $password_reset_url ) {
945 return $password_reset_url;
946 } else {
947 return $url;
948 }
949 }
950 add_filter( 'lostpassword_url', 'shibboleth_custom_password_reset_url' );
951
952 /**
953 * Add a "Log in with Shibboleth" link to the WordPress login form. This link
954 * will be wrapped in a <p> with an id value of "shibboleth_login" so that
955 * deployers can style this however they choose.
956 *
957 * @since 1.0
958 */
959 function shibboleth_login_form() {
960 global $wp;
961 $url = false;
962 if ( ! empty( $wp->request ) ) {
963 $url = wp_login_url( home_url( $wp->request ) );
964 }
965 $login_url = add_query_arg( 'action', 'shibboleth', $url );
966 $login_url = remove_query_arg( 'reauth', $login_url );
967 $button_text = shibboleth_getoption( 'shibboleth_button_text', __( 'Log in with Shibboleth', 'shibboleth' ) );
968 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
969 ?>
970 <div id="shibboleth-wrap" <?php echo $disable ? 'style="margin-top:0;"' : ''; ?>>
971 <?php
972 if ( ! $disable ) {
973 ?>
974 <div class="shibboleth-or">
975 <span><?php esc_html_e( 'Or', 'shibboleth' ); ?></span>
976 </div>
977 <?php
978 }
979 ?>
980 <a href="<?php echo esc_url( $login_url ); ?>" rel="nofollow" class="shibboleth-button button button-primary default">
981 <span class="shibboleth-icon"></span>
982 <?php echo esc_html( $button_text ); ?>
983 </a>
984 </div>
985 <?php
986 }
987 add_action( 'login_form', 'shibboleth_login_form' );
988
989
990 /**
991 * Insert directives into .htaccess file to enable Shibboleth Lazy Sessions.
992 *
993 * @since 1.0
994 */
995 function shibboleth_insert_htaccess() {
996 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
997
998 if ( got_mod_rewrite() && ! $disabled ) {
999 $htaccess = get_home_path() . '.htaccess';
1000 $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>' );
1001 insert_with_markers( $htaccess, 'Shibboleth', $rules );
1002 }
1003 }
1004
1005
1006 /**
1007 * Remove directives from .htaccess file to enable Shibboleth Lazy Sessions.
1008 *
1009 * @since 1.1
1010 */
1011 function shibboleth_remove_htaccess() {
1012 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
1013
1014 if ( got_mod_rewrite() && ! $disabled ) {
1015 $htaccess = get_home_path() . '.htaccess';
1016 insert_with_markers( $htaccess, 'Shibboleth', array() );
1017 }
1018 }
1019
1020 /**
1021 * Load localization files.
1022 *
1023 * @since 1.7
1024 */
1025 function shibboleth_load_textdomain() {
1026 load_plugin_textdomain( 'shibboleth', false, dirname( plugin_basename( __FILE__ ) ) . '/localization/' );
1027 }
1028 add_action( 'plugins_loaded', 'shibboleth_load_textdomain' );
1029