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

shibboleth.php in Shibboleth 2.4.2, at shibboleth.php

1,032 lines 34.1 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.2
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.2' );
22
23 /**
24 * Determine if this is a new install or upgrade and, if so, run the
25 * shibboleth_activate_plugin() function.
26 *
27 * @since 1.0
28 */
29 $plugin_version = get_site_option( 'shibboleth_plugin_version', '0' );
30 if ( SHIBBOLETH_PLUGIN_VERSION !== $plugin_version ) {
31 add_action( 'admin_init', 'shibboleth_activate_plugin' );
32 }
33
34 /**
35 * Determine if a constant is defined. If it is, return the value of the constant.
36 * If it isn't, return the value from get_site_option(). If you'd like to pass a default
37 * for get_site_option(), set $default to the requested default. If you'd like to check
38 * for arrays in constants, set $array to true. If you'd like to return that the object
39 * was obtained as a constant, set $compact to true and the result is an array. To get the
40 * value of the constant or option, look at the value key. To check if the value was
41 * retreived from a constant, look at the constant key.
42 *
43 * @since 2.1
44 * @param string $option Option identifier.
45 * @param bool $default Default value.
46 * @param bool $array If we expect the value to be an array.
47 * @param bool $compact If you want the constant and value returned as an array.
48 * @return mixed
49 */
50 function shibboleth_getoption( $option, $default = false, $array = false, $compact = false ) {
51 // If a constant is defined with the provided option name, get the value of the constant.
52 if ( defined( strtoupper( $option ) ) ) {
53 $value = constant( strtoupper( $option ) );
54 $constant = true;
55 } else {
56 // If no constant is set, just get the value from get_site_option().
57 $value = get_site_option( $option, $default );
58 $constant = false;
59 }
60
61 // If compact is set to true, we compact $value and $constant together for easy use.
62 if ( $compact ) {
63 return array(
64 $value,
65 $constant,
66 'value' => $value,
67 'constant' => $constant,
68 );
69 // Otherwise, just return the $value.
70 } else {
71 return $value;
72 }
73 }
74
75 /**
76 * HTTP and FastCGI friendly getenv() replacement that handles
77 * standard and REDIRECT_ environment variables, as well as HTTP
78 * headers. Users select which method to use to allow for the most
79 * secure configuration possible.
80 *
81 * @since 1.8
82 * @param string $var Environment variable.
83 * @return string|bool
84 */
85 function shibboleth_getenv( $var ) {
86 // 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 $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 $redirect_to = esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) );
430 $initiator_url = shibboleth_session_initiator_url( $redirect_to );
431 } else {
432 $initiator_url = shibboleth_session_initiator_url();
433 }
434 wp_redirect( $initiator_url );
435 exit;
436 }
437 }
438
439
440 /**
441 * When wp-login.php is loaded with 'action=shibboleth', hook Shibboleth
442 * into the WordPress authentication flow.
443 *
444 * @since 1.3
445 */
446 function shibboleth_login_form_shibboleth() {
447 add_filter( 'authenticate', 'shibboleth_authenticate', 10, 3 );
448 }
449 add_action( 'login_form_shibboleth', 'shibboleth_login_form_shibboleth' );
450
451
452 /**
453 * If a Shibboleth user requests a password reset, and the Shibboleth password
454 * reset URL is set, redirect the user there.
455 *
456 * @since 1.3
457 * @param string $user_login Username.
458 */
459 function shibboleth_retrieve_password( $user_login ) {
460 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url' );
461
462 if ( ! empty( $password_reset_url ) ) {
463 $user = get_user_by( 'login', $user_login );
464 if ( $user && get_user_meta( $user->ID, 'shibboleth_account' ) ) {
465 wp_redirect( $password_reset_url );
466 exit;
467 }
468 }
469 }
470 add_action( 'retrieve_password', 'shibboleth_retrieve_password' );
471
472
473 /**
474 * If Shibboleth is the default login method, add 'action=shibboleth' to the
475 * WordPress login URL.
476 *
477 * @since 1.0
478 * @param string $login_url The login URL.
479 */
480 function shibboleth_login_url( $login_url ) {
481 $default = shibboleth_getoption( 'shibboleth_default_to_shib_login' );
482
483 if ( $default ) {
484 $login_url = add_query_arg( 'action', 'shibboleth', $login_url );
485 }
486 return $login_url;
487 }
488 add_filter( 'login_url', 'shibboleth_login_url' );
489
490
491 /**
492 * If the Shibboleth logout URL is set and the user has an active Shibboleth
493 * session, log the user out of Shibboleth after logging them out of WordPress.
494 *
495 * @since 1.0
496 */
497 function shibboleth_logout() {
498 $logout_url = shibboleth_getoption( 'shibboleth_logout_url' );
499
500 if ( ! empty( $logout_url ) && shibboleth_session_active() ) {
501 wp_redirect( $logout_url );
502 exit;
503 }
504 }
505 add_action( 'wp_logout', 'shibboleth_logout', 20 );
506
507
508 /**
509 * Generate the URL to initiate Shibboleth login.
510 *
511 * @param string $redirect the final URL to redirect the user to after all login is complete.
512 * @return the URL to direct the user to in order to initiate Shibboleth login
513 * @uses apply_filters() Calls 'shibboleth_session_initiator_url' before returning session intiator URL
514 * @since 1.3
515 */
516 function shibboleth_session_initiator_url( $redirect = null ) {
517
518 // first build the target URL. This is the WordPress URL the user will be returned to after Shibboleth
519 // is done, and will handle actually logging the user into WordPress using the data provided by Shibboleth.
520 if ( function_exists( 'switch_to_blog' ) ) {
521 if ( ! empty( $GLOBALS['current_blog']->blog_id ) && $GLOBALS['current_blog']->blog_id !== $GLOBALS['current_site']->site_id ) {
522 switch_to_blog( $GLOBALS['current_blog']->blog_id );
523 } else {
524 switch_to_blog( $GLOBALS['current_site']->blog_id );
525 }
526 }
527
528 $target = site_url( 'wp-login.php' );
529
530 if ( function_exists( 'restore_current_blog' ) ) {
531 restore_current_blog();
532 }
533
534 $target = add_query_arg( 'action', 'shibboleth', $target );
535 if ( ! empty( $redirect ) ) {
536 $target = add_query_arg( 'redirect_to', rawurlencode( $redirect ), $target );
537 }
538
539 // now build the Shibboleth session initiator URL.
540 $initiator_url = shibboleth_getoption( 'shibboleth_login_url' );
541
542 $initiator_url = add_query_arg( 'target', rawurlencode( $target ), $initiator_url );
543
544 $initiator_url = apply_filters( 'shibboleth_session_initiator_url', $initiator_url );
545
546 return $initiator_url;
547 }
548
549
550 /**
551 * Authenticate the user based on the current Shibboleth headers.
552 *
553 * If the data available does not map to a WordPress role (based on the
554 * configured role-mapping), the user will not be allowed to login.
555 *
556 * If this is the first time we've seen this user (based on the username
557 * attribute), a new account will be created.
558 *
559 * Known users will have their profile data updated based on the Shibboleth
560 * data present if the plugin is configured to do so.
561 *
562 * @uses apply_filters() Calls 'shibboleth_override_username' before authenticating
563 * @uses apply_filters() Calls 'shibboleth_override_email' before authenticating
564 *
565 * @return WP_User|WP_Error authenticated user or error if unable to authenticate
566 * @since 1.0
567 */
568 function shibboleth_authenticate_user() {
569 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
570 $shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
571 $auto_combine_accounts = shibboleth_getoption( 'shibboleth_auto_combine_accounts' );
572 $manually_combine_accounts = shibboleth_getoption( 'shibboleth_manually_combine_accounts' );
573
574 $username = shibboleth_getenv( $shib_headers['username']['name'] );
575 $email = shibboleth_getenv( $shib_headers['email']['name'] );
576
577 /**
578 * Be VERY careful with the below two filters! They can lead to unintended
579 * consequences, such as multiple Shibboleth users mapping to the same
580 * WordPress user, or introducing security risks by improperly escaping
581 * and validating usernames and email addresses.
582 */
583
584 /**
585 * Override the username provided by Shibboleth.
586 *
587 * This can be used to escape or normalize the Shibboleth username.
588 *
589 * @param string $username
590 */
591 $username = apply_filters( 'shibboleth_override_username', $username );
592
593 /**
594 * Override the email address provided by Shibboleth.
595 *
596 * This can be used to escape or normalize the Shibboleth email address.
597 *
598 * @param string $email
599 */
600 $email = apply_filters( 'shibboleth_override_email', $email );
601
602 /**
603 * Allows a bypass mechanism for native Shibboleth authentication.
604 *
605 * Returning a non-null value from this filter will result in your value being
606 * returned to WordPress. You can prevent a user from being authenticated
607 * by returning a WP_Error object.
608 *
609 * @param null $auth
610 * @param string $username
611 */
612 $authenticate = apply_filters( 'shibboleth_authenticate_user', null, $username );
613 if ( null !== $authenticate ) {
614 return $authenticate;
615 }
616
617 // look up existing account by username, with email as a fallback.
618 $user_by = 'username';
619 $user = get_user_by( 'login', $username );
620 if ( ! $user ) {
621 $user_by = 'email';
622 $user = get_user_by( 'email', $email );
623 }
624
625 // if this account is not a Shibboleth account, then do account combine (if allowed).
626 if ( is_object( $user ) && $user->ID && ! get_user_meta( $user->ID, 'shibboleth_account' ) ) {
627 $do_account_combine = false;
628 if ( 'username' === $user_by && ( 'allow' === $auto_combine_accounts || 'allow' === $manually_combine_accounts ) ) {
629 $do_account_combine = true;
630 } elseif ( 'bypass' === $auto_combine_accounts || 'bypass' === $manually_combine_accounts ) {
631 $do_account_combine = true;
632 }
633
634 if ( $do_account_combine ) {
635 update_user_meta( $user->ID, 'shibboleth_account', true );
636 if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
637 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts automatically.' );
638 }
639 } elseif ( 'username' === $user_by ) {
640 if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
641 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.' );
642 }
643 return new WP_Error( 'invalid_username', __( 'An account already exists with this username.', 'shibboleth' ) );
644 } else {
645 if ( in_array( 'account_merge', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
646 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.' );
647 }
648 return new WP_Error( 'invalid_email', __( 'An account already exists with this email.', 'shibboleth' ) );
649 }
650 }
651
652 // create account if new user.
653 if ( ! $user ) {
654 $user = shibboleth_create_new_user( $username, $email );
655 if ( is_wp_error( $user ) ) {
656 return new WP_Error( $user->get_error_code(), $user->get_error_message() );
657 }
658 }
659
660 if ( ! $user ) {
661 $error_message = 'Unable to create account based on data provided.';
662 if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
663 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: Unable to create account based on data provided.' );
664 }
665 return new WP_Error( 'missing_data', $error_message );
666 }
667
668 // update user data.
669 shibboleth_update_user_data( $user->ID );
670
671 $update = shibboleth_getoption( 'shibboleth_update_roles' );
672
673 if ( $update ) {
674 $user_role = shibboleth_get_user_role();
675 $user->set_role( $user_role );
676 if ( in_array( 'role_update', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
677 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') role was updated to ' . $user_role . '.' );
678 }
679 do_action( 'shibboleth_set_user_roles', $user );
680 }
681
682 if ( in_array( 'auth', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
683 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') successfully authenticated.' );
684 }
685 return $user;
686 }
687
688
689 /**
690 * Create a new WordPress user account, and mark it as a Shibboleth account.
691 *
692 * @param string $user_login login name for the new user.
693 * @param string $user_email email address for the new user.
694 * @return object WP_User object for newly created user.
695 * @since 1.0
696 */
697 function shibboleth_create_new_user( $user_login, $user_email ) {
698 $create_accounts = shibboleth_getoption( 'shibboleth_create_accounts' );
699 $shib_logging = shibboleth_getoption( 'shibboleth_logging', array(), true );
700 $user_role = shibboleth_get_user_role();
701
702 if ( ! empty( $create_accounts ) ) {
703 if ( empty( $user_login ) || empty( $user_email ) || '_no_account' === $user_role ) {
704 return null;
705 }
706
707 // create account and flag as a shibboleth account.
708 $user_id = wp_insert_user(
709 array(
710 'user_login' => $user_login,
711 'user_email' => $user_email,
712 'user_pass' => null,
713 )
714 );
715 if ( is_wp_error( $user_id ) ) {
716 if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
717 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: Unable to create account based on data provided. Reason: ' . $user_id->get_error_message() . '.' );
718 }
719 return new WP_Error( 'account_create_failed', $user_id->get_error_message() );
720 } else {
721 $user = new WP_User( $user_id );
722 update_user_meta( $user->ID, 'shibboleth_account', true );
723
724 // always update user data and role on account creation.
725 shibboleth_update_user_data( $user->ID, true );
726 $user->set_role( $user_role );
727 do_action( 'shibboleth_set_user_roles', $user );
728 if ( in_array( 'account_create', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
729 error_log( '[Shibboleth WordPress Plugin Logging] SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') was created with role ' . ( $user_role ? $user_role : 'none' ) . '.' );
730 }
731 return $user;
732 }
733 } else {
734 if ( in_array( 'auth', $shib_logging, true ) || defined( 'WP_DEBUG' ) && WP_DEBUG ) {
735 error_log( '[Shibboleth WordPress Plugin Logging] ERROR: User account does not exist and account creation is disabled.' );
736 }
737 return new WP_Error( 'no_access', __( 'You do not have sufficient access.' ) );
738 }
739 }
740
741 /**
742 * Get the role the current user should have. This is determined by the role
743 * mapping configured for the plugin, and the Shibboleth headers present at the
744 * time of login.
745 *
746 * @return string the role the current user should have
747 * @uses apply_filters() Calls 'shibboleth_roles' after retrieving shibboleth_roles array
748 * @uses apply_filters() Calls 'shibboleth_user_role' before returning final user role
749 * @since 1.0
750 */
751 function shibboleth_get_user_role() {
752 // wp_roles() requires WordPress version 4.3 or higher.
753 if ( function_exists( 'wp_roles' ) ) {
754 $roles = wp_roles();
755 } else {
756 global $wp_roles;
757
758 if ( isset( $wp_roles ) ) {
759 $roles = $wp_roles;
760 } else {
761 $roles = new WP_Roles();
762 }
763 }
764
765 $shib_roles = apply_filters( 'shibboleth_roles', shibboleth_getoption( 'shibboleth_roles', array(), true ) );
766 $user_role = shibboleth_getoption( 'shibboleth_default_role' );
767
768 foreach ( $roles->role_names as $key => $name ) {
769 if ( isset( $shib_roles[ $key ]['header'] ) ) {
770 $role_header = $shib_roles[ $key ]['header'];
771 }
772 if ( isset( $shib_roles[ $key ]['value'] ) ) {
773 $role_value = $shib_roles[ $key ]['value'];
774 }
775 if ( empty( $role_header ) || empty( $role_value ) ) {
776 continue;
777 }
778 $values = explode( ';', shibboleth_getenv( $role_header ) );
779 if ( in_array( $role_value, $values, true ) ) {
780 $user_role = $key;
781 break;
782 }
783 }
784
785 $user_role = apply_filters( 'shibboleth_user_role', $user_role );
786
787 return $user_role;
788 }
789
790
791 /**
792 * Get the user fields that are managed by Shibboleth.
793 *
794 * @return Array user fields managed by Shibboleth
795 * @since 1.3
796 */
797 function shibboleth_get_managed_user_fields() {
798 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
799
800 $managed = array();
801
802 foreach ( $shib_headers as $name => $value ) {
803 if ( isset( $value['managed'] ) ) {
804 if ( $value['managed'] ) {
805 $managed[] = $name;
806 }
807 }
808 }
809
810 return $managed;
811 }
812
813
814 /**
815 * Update the user data for the specified user based on the current Shibboleth headers. Unless
816 * the 'force_update' parameter is true, only the user fields marked as 'managed' fields will be
817 * updated.
818 *
819 * @param int $user_id ID of the user to update.
820 * @param boolean $force_update force update of user data, regardless of 'managed' flag on fields.
821 * @uses apply_filters() Calls 'shibboleth_user_*' before setting user attributes,
822 * where '*' is one of: login, nicename, first_name, last_name,
823 * nickname, display_name, email
824 * @since 1.0
825 */
826 function shibboleth_update_user_data( $user_id, $force_update = false ) {
827 $shib_headers = shibboleth_getoption( 'shibboleth_headers', array(), true );
828
829 $user_fields = array(
830 'user_login' => 'username',
831 'user_nicename' => 'username',
832 'first_name' => 'first_name',
833 'last_name' => 'last_name',
834 'nickname' => 'nickname',
835 'display_name' => 'display_name',
836 'user_email' => 'email',
837 );
838
839 $user_data = array(
840 'ID' => $user_id,
841 );
842
843 foreach ( $user_fields as $field => $header ) {
844 $managed = false;
845 if ( isset( $shib_headers[ $header ]['managed'] ) ) {
846 $managed = $shib_headers[ $header ]['managed'];
847 }
848 if ( $force_update || $managed ) {
849 $filter = 'shibboleth_' . ( strpos( $field, 'user_' ) === 0 ? '' : 'user_' ) . $field;
850 $user_data[ $field ] = apply_filters( $filter, shibboleth_getenv( $shib_headers[ $header ]['name'] ) );
851 }
852 }
853
854 // Shibboleth users do not use their email address for authentication.
855 add_filter( 'send_email_change_email', '__return_false' );
856
857 wp_update_user( $user_data );
858 }
859
860
861 /**
862 * Sanitize the nicename using sanitize_title
863 *
864 * @since 1.4
865 * @see http://wordpress.org/support/topic/377030
866 */
867 add_filter( 'shibboleth_user_nicename', 'sanitize_title' );
868
869 /**
870 * Enqueues scripts and styles necessary for the Shibboleth button.
871 *
872 * @since 2.0
873 */
874 function shibboleth_login_enqueue_scripts() {
875 global $action;
876
877 // Only add scripts for the login action to avoid breaking other forms.
878 if ( 'login' === $action || 'shibboleth' === $action ) {
879 wp_enqueue_style( 'shibboleth-login', plugins_url( 'assets/css/shibboleth_login_form.css', __FILE__ ), array( 'login' ), SHIBBOLETH_PLUGIN_VERSION );
880 wp_enqueue_script( 'shibboleth-login', plugins_url( 'assets/js/shibboleth_login_form.js', __FILE__ ), array( 'jquery' ), SHIBBOLETH_PLUGIN_VERSION, true );
881 }
882 }
883 add_action( 'login_enqueue_scripts', 'shibboleth_login_enqueue_scripts' );
884
885 /**
886 * Prevents local WordPress authentication if disabled by an administrator.
887 *
888 * @since 2.0
889 */
890 function shibboleth_disable_login() {
891 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
892
893 $bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
894
895 if ( $disable && ! $bypass ) {
896 if ( isset( $_GET['action'] ) && 'lostpassword' === $_GET['action'] ) {
897 // Disable the ability to reset passwords from wp-login.php.
898 add_filter( 'allow_password_reset', '__return_false' );
899 } elseif ( isset( $_POST['log'] ) || isset( $_POST['user_login'] ) ) {
900 // Disable the ability to login using local authentication.
901 wp_die( esc_html( __( 'Shibboleth authentication is required.', 'shibboleth' ) ) );
902
903 check_admin_referer( 'log-in' );
904 }
905 }
906 }
907 add_action( 'login_init', 'shibboleth_disable_login' );
908
909 /**
910 * Disables wp-login.php login form if disabled by an administrator.
911 *
912 * @since 2.0
913 */
914 function shibboleth_disable_login_form() {
915 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
916 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
917
918 $bypass = defined( 'SHIBBOLETH_ALLOW_LOCAL_AUTH' ) && SHIBBOLETH_ALLOW_LOCAL_AUTH;
919
920 if ( $disable && ! $bypass ) {
921 ?>
922 <style type="text/css">
923 .login #loginform p,
924 .login #loginform .user-pass-wrap {
925 display: none;
926 }
927 <?php if ( ! $password_reset_url ) { ?>
928 .login #nav {
929 display: none;
930 }
931 <?php } ?>
932 </style>
933 <?php
934 }
935 }
936 add_action( 'login_enqueue_scripts', 'shibboleth_disable_login_form' );
937
938 /**
939 * Updates the lost password URL, if specified.
940 *
941 * @param string $url original password reset URL.
942 * @since 2.1
943 */
944 function shibboleth_custom_password_reset_url( $url ) {
945 $password_reset_url = shibboleth_getoption( 'shibboleth_password_reset_url', false );
946
947 if ( $password_reset_url ) {
948 return $password_reset_url;
949 } else {
950 return $url;
951 }
952 }
953 add_filter( 'lostpassword_url', 'shibboleth_custom_password_reset_url' );
954
955 /**
956 * Add a "Log in with Shibboleth" link to the WordPress login form. This link
957 * will be wrapped in a <p> with an id value of "shibboleth_login" so that
958 * deployers can style this however they choose.
959 *
960 * @since 1.0
961 */
962 function shibboleth_login_form() {
963 global $wp;
964 $url = false;
965 if ( ! empty( $wp->request ) ) {
966 $url = wp_login_url( home_url( $wp->request ) );
967 }
968 $login_url = add_query_arg( 'action', 'shibboleth', $url );
969 $login_url = remove_query_arg( 'reauth', $login_url );
970 $button_text = shibboleth_getoption( 'shibboleth_button_text', __( 'Log in with Shibboleth', 'shibboleth' ) );
971 $disable = shibboleth_getoption( 'shibboleth_disable_local_auth', false );
972 ?>
973 <div id="shibboleth-wrap" <?php echo $disable ? 'style="margin-top:0;"' : ''; ?>>
974 <?php
975 if ( ! $disable ) {
976 ?>
977 <div class="shibboleth-or">
978 <span><?php esc_html_e( 'Or', 'shibboleth' ); ?></span>
979 </div>
980 <?php
981 }
982 ?>
983 <a href="<?php echo esc_url( $login_url ); ?>" rel="nofollow" class="shibboleth-button button button-primary default">
984 <span class="shibboleth-icon"></span>
985 <?php echo esc_html( $button_text ); ?>
986 </a>
987 </div>
988 <?php
989 }
990 add_action( 'login_form', 'shibboleth_login_form' );
991
992
993 /**
994 * Insert directives into .htaccess file to enable Shibboleth Lazy Sessions.
995 *
996 * @since 1.0
997 */
998 function shibboleth_insert_htaccess() {
999 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
1000
1001 if ( got_mod_rewrite() && ! $disabled ) {
1002 $htaccess = get_home_path() . '.htaccess';
1003 $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>' );
1004 insert_with_markers( $htaccess, 'Shibboleth', $rules );
1005 }
1006 }
1007
1008
1009 /**
1010 * Remove directives from .htaccess file to enable Shibboleth Lazy Sessions.
1011 *
1012 * @since 1.1
1013 */
1014 function shibboleth_remove_htaccess() {
1015 $disabled = defined( 'SHIBBOLETH_DISALLOW_FILE_MODS' ) && SHIBBOLETH_DISALLOW_FILE_MODS;
1016
1017 if ( got_mod_rewrite() && ! $disabled ) {
1018 $htaccess = get_home_path() . '.htaccess';
1019 insert_with_markers( $htaccess, 'Shibboleth', array() );
1020 }
1021 }
1022
1023 /**
1024 * Load localization files.
1025 *
1026 * @since 1.7
1027 */
1028 function shibboleth_load_textdomain() {
1029 load_plugin_textdomain( 'shibboleth', false, dirname( plugin_basename( __FILE__ ) ) . '/localization/' );
1030 }
1031 add_action( 'plugins_loaded', 'shibboleth_load_textdomain' );
1032