PluginProbe
Shibboleth / 2.4.3
Shibboleth v2.4.3
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.3, at shibboleth.php

1,031 lines 33.5 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.4.3
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.3' );
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_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 __DIR__ . '/options-admin.php';
362 require_once __DIR__ . '/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 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
380 $session = shibboleth_getenv( $shib_headers['username']['name'] );
381
382 if ( $session && 'http' !== $method ) {
383 $active = true;
384 } elseif ( $session && 'http' === $method ) {
385 /**
386 * Handling HTTP header cases with a spoofkey to better protect against
387 * HTTP header spoofing.
388 *
389 * @see https://wiki.shibboleth.net/confluence/display/SHIB2/NativeSPSpoofChecking
390 */
391 $spoofkey = shibboleth_getoption( 'shibboleth_spoof_key' );
392 $shibboleth_auto_login = shibboleth_getoption( 'shibboleth_auto_login' );
393
394 if ( false !== $spoofkey && '' !== $spoofkey ) {
395 $bypass = defined( 'SHIBBOLETH_BYPASS_SPOOF_CHECKING' ) && SHIBBOLETH_BYPASS_SPOOF_CHECKING;
396 $checkkey = shibboleth_getenv( 'Shib-Spoof-Check' );
397 if ( $checkkey === $spoofkey || $bypass ) {
398 $active = true;
399 } elseif ( $auto_login ) {
400 $active = false;
401 } else {
402 wp_die( esc_html( __( 'The Shibboleth request you submitted failed validation. Please contact your site administrator for further assistance.', 'shibboleth' ) ) );
403 }
404 } else {
405 $active = true;
406 }
407 }
408
409 $active = apply_filters( 'shibboleth_session_active', $active );
410 return $active;
411 }
412
413
414 /**
415 * Authenticate the user using Shibboleth. If a Shibboleth session is active,
416 * use the data provided by Shibboleth to log the user in. If a Shibboleth
417 * session is not active, redirect the user to the Shibboleth Session Initiator
418 * URL to initiate the session.
419 *
420 * @since 1.0
421 * @param null|WP_User|WP_Error $user WP_User if the user is authenticated. WP_Error or null otherwise.
422 * @param string $username Username or email address.
423 * @param string $password User password.
424 */
425 function shibboleth_authenticate( $user, $username, $password ) {
426 if ( shibboleth_session_active() ) {
427 return shibboleth_authenticate_user();
428 } else {
429 if ( isset( $_REQUEST['redirect_to'] ) ) {
430 $redirect_to = esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) );
431 $initiator_url = shibboleth_session_initiator_url( $redirect_to );
432 } else {
433 $initiator_url = shibboleth_session_initiator_url();
434 }
435 wp_redirect( $initiator_url );
436 exit;
437 }
438 }
439
440
441 /**
442 * When wp-login.php is loaded with 'action=shibboleth', hook Shibboleth
443 * into the WordPress authentication flow.
444 *
445 * @since 1.3
446 */
447 function shibboleth_login_form_shibboleth() {
448 add_filter( 'authenticate', 'shibboleth_authenticate', 10, 3 );
449 }
450 add_action( 'login_form_shibboleth', 'shibboleth_login_form_shibboleth' );
451
452
453 /**
454 * If a Shibboleth user requests a password reset, and the Shibboleth password
455 * reset URL is set, redirect the user there.
456 *
457 * @since 1.3
458 * @param string $user_login Username.
459 */
460 function shibboleth_retrieve_password( $user_login ) {
461 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url' );
462
463 if ( ! empty( $password_reset_url ) ) {
464 $user = get_user_by( 'login', $user_login );
465 if ( $user && get_user_meta( $user->ID, 'shibboleth_account' ) ) {
466 wp_redirect( $password_reset_url );
467 exit;
468 }
469 }
470 }
471 add_action( 'retrieve_password', 'shibboleth_retrieve_password' );
472
473
474 /**
475 * If Shibboleth is the default login method, add 'action=shibboleth' to the
476 * WordPress login URL.
477 *
478 * @since 1.0
479 * @param string $login_url The login URL.
480 */
481 function shibboleth_login_url( $login_url ) {
482 $default = shibboleth_getoption( 'shibboleth_default_to_shib_login' );
483
484 if ( $default ) {
485 $login_url = add_query_arg( 'action', 'shibboleth', $login_url );
486 }
487 return $login_url;
488 }
489 add_filter( 'login_url', 'shibboleth_login_url' );
490
491
492 /**
493 * If the Shibboleth logout URL is set and the user has an active Shibboleth
494 * session, log the user out of Shibboleth after logging them out of WordPress.
495 *
496 * @since 1.0
497 */
498 function shibboleth_logout() {
499 $logout_url = shibboleth_getoption( 'shibboleth_logout_url' );
500
501 if ( ! empty( $logout_url ) && shibboleth_session_active() ) {
502 wp_redirect( $logout_url );
503 exit;
504 }
505 }
506 add_action( 'wp_logout', 'shibboleth_logout', 20 );
507
508
509 /**
510 * Generate the URL to initiate Shibboleth login.
511 *
512 * @param string $redirect the final URL to redirect the user to after all login is complete.
513 * @return the URL to direct the user to in order to initiate Shibboleth login
514 * @uses apply_filters() Calls 'shibboleth_session_initiator_url' before returning session intiator URL
515 * @since 1.3
516 */
517 function shibboleth_session_initiator_url( $redirect = null ) {
518
519 // first build the target URL. This is the WordPress URL the user will be returned to after Shibboleth
520 // is done, and will handle actually logging the user into WordPress using the data provided by Shibboleth.
521 if ( function_exists( 'switch_to_blog' ) ) {
522 if ( ! empty( $GLOBALS['current_blog']->blog_id ) && $GLOBALS['current_blog']->blog_id !== $GLOBALS['current_site']->site_id ) {
523 switch_to_blog( $GLOBALS['current_blog']->blog_id );
524 } else {
525 switch_to_blog( $GLOBALS['current_site']->blog_id );
526 }
527 }
528
529 $target = site_url( 'wp-login.php' );
530
531 if ( function_exists( 'restore_current_blog' ) ) {
532 restore_current_blog();
533 }
534
535 $target = add_query_arg( 'action', 'shibboleth', $target );
536 if ( ! empty( $redirect ) ) {
537 $target = add_query_arg( 'redirect_to', rawurlencode( $redirect ), $target );
538 }
539
540 // now build the Shibboleth session initiator URL.
541 $initiator_url = shibboleth_getoption( 'shibboleth_login_url' );
542
543 $initiator_url = add_query_arg( 'target', rawurlencode( $target ), $initiator_url );
544
545 $initiator_url = apply_filters( 'shibboleth_session_initiator_url', $initiator_url );
546
547 return $initiator_url;
548 }
549
550 /**
551 * Log Shibboleth message.
552 *
553 * @param string $message_type Message type.
554 * @param string $message Message.
555 * @since 2.4.3
556 */
557 function shibboleth_log_message( $message_type, $message ) {
558 static $shib_logging;
559
560 if ( ! isset( $shib_logging ) ) {
561 $shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
562 }
563
564 if ( defined( 'WP_DEBUG' ) && WP_DEBUG || in_array( $message_type, $shib_logging, true ) ) {
565 error_log( '[Shibboleth WordPress Plugin Logging] ' . $message );
566 }
567 }
568
569 /**
570 * Authenticate the user based on the current Shibboleth headers.
571 *
572 * If the data available does not map to a WordPress role (based on the
573 * configured role-mapping), the user will not be allowed to login.
574 *
575 * If this is the first time we've seen this user (based on the username
576 * attribute), a new account will be created.
577 *
578 * Known users will have their profile data updated based on the Shibboleth
579 * data present if the plugin is configured to do so.
580 *
581 * @uses apply_filters() Calls 'shibboleth_override_username' before authenticating
582 * @uses apply_filters() Calls 'shibboleth_override_email' before authenticating
583 *
584 * @return WP_User|WP_Error authenticated user or error if unable to authenticate
585 * @since 1.0
586 */
587 function shibboleth_authenticate_user() {
588 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
589 $auto_combine_accounts = shibboleth_getoption( 'shibboleth_auto_combine_accounts' );
590 $manually_combine_accounts = shibboleth_getoption( 'shibboleth_manually_combine_accounts' );
591
592 $username = shibboleth_getenv( $shib_headers['username']['name'] );
593 $email = shibboleth_getenv( $shib_headers['email']['name'] );
594
595 /**
596 * Be VERY careful with the below two filters! They can lead to unintended
597 * consequences, such as multiple Shibboleth users mapping to the same
598 * WordPress user, or introducing security risks by improperly escaping
599 * and validating usernames and email addresses.
600 */
601
602 /**
603 * Override the username provided by Shibboleth.
604 *
605 * This can be used to escape or normalize the Shibboleth username.
606 *
607 * @param string $username
608 */
609 $username = apply_filters( 'shibboleth_override_username', $username );
610
611 /**
612 * Override the email address provided by Shibboleth.
613 *
614 * This can be used to escape or normalize the Shibboleth email address.
615 *
616 * @param string $email
617 */
618 $email = apply_filters( 'shibboleth_override_email', $email );
619
620 /**
621 * Allows a bypass mechanism for native Shibboleth authentication.
622 *
623 * Returning a non-null value from this filter will result in your value being
624 * returned to WordPress. You can prevent a user from being authenticated
625 * by returning a WP_Error object.
626 *
627 * @param null $auth
628 * @param string $username
629 */
630 $authenticate = apply_filters( 'shibboleth_authenticate_user', null, $username );
631 if ( null !== $authenticate ) {
632 return $authenticate;
633 }
634
635 // look up existing account by username, with email as a fallback.
636 $user_by = 'username';
637 $user = get_user_by( 'login', $username );
638 if ( ! $user ) {
639 $user_by = 'email';
640 $user = get_user_by( 'email', $email );
641 }
642
643 // if this account is not a Shibboleth account, then do account combine (if allowed).
644 if ( is_object( $user ) && $user->ID && ! get_user_meta( $user->ID, 'shibboleth_account' ) ) {
645 $do_account_combine = false;
646 if ( 'username' === $user_by && ( 'allow' === $auto_combine_accounts || 'allow' === $manually_combine_accounts ) ) {
647 $do_account_combine = true;
648 } elseif ( 'bypass' === $auto_combine_accounts || 'bypass' === $manually_combine_accounts ) {
649 $do_account_combine = true;
650 }
651
652 if ( $do_account_combine ) {
653 update_user_meta( $user->ID, 'shibboleth_account', true );
654 shibboleth_log_message( 'account_merge', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts automatically.' );
655 } elseif ( 'username' === $user_by ) {
656 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.' );
657 return new WP_Error( 'invalid_username', __( 'An account already exists with this username.', 'shibboleth' ) );
658 } else {
659 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.' );
660 return new WP_Error( 'invalid_email', __( 'An account already exists with this email.', 'shibboleth' ) );
661 }
662 }
663
664 // create account if new user.
665 if ( ! $user ) {
666 $user = shibboleth_create_new_user( $username, $email );
667 if ( is_wp_error( $user ) ) {
668 return new WP_Error( $user->get_error_code(), $user->get_error_message() );
669 }
670 }
671
672 if ( ! $user ) {
673 $error_message = 'Unable to create account based on data provided.';
674 shibboleth_log_message( 'account_create', 'ERROR: Unable to create account based on data provided.' );
675 return new WP_Error( 'missing_data', $error_message );
676 }
677
678 // update user data.
679 shibboleth_update_user_data( $user->ID );
680
681 $update = shibboleth_getoption( 'shibboleth_update_roles' );
682
683 if ( $update ) {
684 $user_role = shibboleth_get_user_role();
685 $user->set_role( $user_role );
686 shibboleth_log_message( 'role_update', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') role was updated to ' . $user_role . '.' );
687 do_action( 'shibboleth_set_user_roles', $user );
688 }
689
690 shibboleth_log_message( 'auth', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') successfully authenticated.' );
691 return $user;
692 }
693
694
695 /**
696 * Create a new WordPress user account, and mark it as a Shibboleth account.
697 *
698 * @param string $user_login login name for the new user.
699 * @param string $user_email email address for the new user.
700 * @return object WP_User object for newly created user.
701 * @since 1.0
702 */
703 function shibboleth_create_new_user( $user_login, $user_email ) {
704 $create_accounts = shibboleth_getoption( 'shibboleth_create_accounts' );
705 $user_role = shibboleth_get_user_role();
706
707 if ( ! empty( $create_accounts ) ) {
708 if ( empty( $user_login ) || empty( $user_email ) || '_no_account' === $user_role ) {
709 return null;
710 }
711
712 // create account and flag as a shibboleth account.
713 $user_id = wp_insert_user(
714 array(
715 'user_login' => $user_login,
716 'user_email' => $user_email,
717 'user_pass' => null,
718 )
719 );
720 if ( is_wp_error( $user_id ) ) {
721 shibboleth_log_message( 'account_create', 'ERROR: Unable to create account based on data provided. Reason: ' . $user_id->get_error_message() . '.' );
722 return new WP_Error( 'account_create_failed', $user_id->get_error_message() );
723 } else {
724 $user = new WP_User( $user_id );
725 update_user_meta( $user->ID, 'shibboleth_account', true );
726
727 // always update user data and role on account creation.
728 shibboleth_update_user_data( $user->ID, true );
729 $user->set_role( $user_role );
730 do_action( 'shibboleth_set_user_roles', $user );
731 shibboleth_log_message( 'account_create', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') was created with role ' . ( $user_role ? $user_role : 'none' ) . '.' );
732 return $user;
733 }
734 } else {
735 shibboleth_log_message( 'auth', 'ERROR: User account does not exist and account creation is disabled.' );
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'] ) ) {
899 // Disable the ability to login using local authentication.
900 wp_die( esc_html( __( 'Shibboleth authentication is required.', 'shibboleth' ) ) );
901
902 check_admin_referer( 'log-in' );
903 }
904 }
905 }
906 add_action( 'login_init', 'shibboleth_disable_login' );
907
908 /**
909 * Disables wp-login.php login form if disabled by an administrator.
910 *
911 * @since 2.0
912 */
913 function shibboleth_disable_login_form() {
914 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
915 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
916
917 $bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
918
919 if ( $disable && ! $bypass ) {
920 ?>
921 <style type="text/css">
922 .login #loginform p,
923 .login #loginform .user-pass-wrap {
924 display: none;
925 }
926 <?php if ( ! $password_reset_url ) { ?>
927 .login #nav {
928 display: none;
929 }
930 <?php } ?>
931 </style>
932 <?php
933 }
934 }
935 add_action( 'login_enqueue_scripts', 'shibboleth_disable_login_form' );
936
937 /**
938 * Updates the lost password URL, if specified.
939 *
940 * @param string $url original password reset URL.
941 * @since 2.1
942 */
943 function shibboleth_custom_password_reset_url( $url ) {
944 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
945
946 if ( $password_reset_url ) {
947 return $password_reset_url;
948 } else {
949 return $url;
950 }
951 }
952 add_filter( 'lostpassword_url', 'shibboleth_custom_password_reset_url' );
953
954 /**
955 * Add a "Log in with Shibboleth" link to the WordPress login form. This link
956 * will be wrapped in a <p> with an id value of "shibboleth_login" so that
957 * deployers can style this however they choose.
958 *
959 * @since 1.0
960 */
961 function shibboleth_login_form() {
962 global $wp;
963 $url = false;
964 if ( ! empty( $wp->request ) ) {
965 $url = wp_login_url( home_url( $wp->request ) );
966 }
967 $login_url = add_query_arg( 'action', 'shibboleth', $url );
968 $login_url = remove_query_arg( 'reauth', $login_url );
969 $button_text = shibboleth_getoption( 'shibboleth_button_text', __( 'Log in with Shibboleth', 'shibboleth' ) );
970 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
971 ?>
972 <div id="shibboleth-wrap" <?php echo $disable ? 'style="margin-top:0;"' : ''; ?>>
973 <?php
974 if ( ! $disable ) {
975 ?>
976 <div class="shibboleth-or">
977 <span><?php esc_html_e( 'Or', 'shibboleth' ); ?></span>
978 </div>
979 <?php
980 }
981 ?>
982 <a href="<?php echo esc_url( $login_url ); ?>" rel="nofollow" class="shibboleth-button button button-primary default">
983 <span class="shibboleth-icon"></span>
984 <?php echo esc_html( $button_text ); ?>
985 </a>
986 </div>
987 <?php
988 }
989 add_action( 'login_form', 'shibboleth_login_form' );
990
991
992 /**
993 * Insert directives into .htaccess file to enable Shibboleth Lazy Sessions.
994 *
995 * @since 1.0
996 */
997 function shibboleth_insert_htaccess() {
998 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
999
1000 if ( got_mod_rewrite() && ! $disabled ) {
1001 $htaccess = get_home_path() . '.htaccess';
1002 $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>' );
1003 insert_with_markers( $htaccess, 'Shibboleth', $rules );
1004 }
1005 }
1006
1007
1008 /**
1009 * Remove directives from .htaccess file to enable Shibboleth Lazy Sessions.
1010 *
1011 * @since 1.1
1012 */
1013 function shibboleth_remove_htaccess() {
1014 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
1015
1016 if ( got_mod_rewrite() && ! $disabled ) {
1017 $htaccess = get_home_path() . '.htaccess';
1018 insert_with_markers( $htaccess, 'Shibboleth', array() );
1019 }
1020 }
1021
1022 /**
1023 * Load localization files.
1024 *
1025 * @since 1.7
1026 */
1027 function shibboleth_load_textdomain() {
1028 load_plugin_textdomain( 'shibboleth', false, dirname( plugin_basename( __FILE__ ) ) . '/localization/' );
1029 }
1030 add_action( 'plugins_loaded', 'shibboleth_load_textdomain' );
1031