Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/wp-plugin-ci-full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ jobs:
run: |
vendor/bin/phpcs -q -n --ignore=vendor --standard=WordPress --report=checkstyle $GITHUB_WORKSPACE | cs2pr

- name: Be evil and hide .eslintrc from plugin check
run: |
rm $GITHUB_WORKSPACE/.eslintrc

- name: Run plugin check
uses: wordpress/plugin-check-action@v1
with:
exclude-directories: '.github'
exclude-files: '.eslintrc'
5 changes: 4 additions & 1 deletion options-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
* @package shibboleth
*/

defined( 'ABSPATH' ) || exit;

/**
* Setup admin tabs for the Shibboleth option page.
*
Expand Down Expand Up @@ -383,7 +385,7 @@ function shibboleth_options_idps() {
if ( isset( $_POST['submit'] ) ) {
check_admin_referer( 'shibboleth_update_options' );

$idp_options = shibboleth_getoption( 'shibboleth_idps', array(), true, false );
$idp_options = shibboleth_getoption( 'shibboleth_idps', array() );

if ( ! defined( 'SHIBBOLETH_IDPS' ) ) {
if ( isset( $_POST['idps'] ) ) {
Expand Down Expand Up @@ -982,6 +984,7 @@ function shibboleth_options_logging() {
* @since ?
*/
function shibboleth_options_page() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
?>
<div class="wrap">
Expand Down
172 changes: 86 additions & 86 deletions options-user.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
* @package shibboleth
*/

defined( 'ABSPATH' ) || exit;

/**
* For WordPress accounts that were created by Shibboleth, limit what administrators and users
* can edit via user-edit.php and profile.php.
Expand Down Expand Up @@ -195,96 +197,93 @@ function shibboleth_link_accounts_button( $user ) {
* @since 1.9
*/
function shibboleth_link_accounts() {
$screen = get_current_screen();

if ( is_admin() && 'profile' === $screen->id ) {
$user_id = get_current_user_id();

// If profile page has ?shibboleth=link action and current user can edit their profile, proceed.
if ( isset( $_GET['shibboleth'] ) && 'link' === $_GET['shibboleth'] && current_user_can( 'edit_user', $user_id ) ) {
check_admin_referer( 'shibboleth-link' );

$allowed = shibboleth_getoption( 'shibboleth_manually_combine_accounts', 'disallow' );

$user_idp = shibboleth_get_user_idp( $user_id );

// If user's account is not already linked with shibboleth, proceed.
if ( empty( $user_idp ) ) {
// If manual account merging is enabled, proceed.
if ( 'allow' === $allowed || 'bypass' === $allowed ) {
// If there is an existing shibboleth session, proceed.
if ( shibboleth_session_active() ) {
$shib_headers = shibboleth_getoption( 'shibboleth_headers', false, true );

$username = shibboleth_getenv( $shib_headers['username']['name'] );
$email = shibboleth_getenv( $shib_headers['email']['name'] );

$user = get_user_by( 'id', $user_id );

$set_user_idp = false;

if ( $user->user_login === $username && strtolower( $user->user_email ) === strtolower( $email ) ) {
// If username and email match, safe to merge.
$set_user_idp = true;
} elseif ( $user->user_login === $username ) {
// If username matches, check if there is a conflict with the email.
$prevent_conflict = get_user_by( 'email', $email );

// If username matches and there is no existing account with the email, safe to merge.
if ( ! $prevent_conflict->ID ) {
$set_user_idp = true;
} else {
// If username matches and there is an existing account with the email, fail.
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: An account already exists with the email: ' . $email . ' .' );
}
} elseif ( strtolower( $user->user_email ) === strtolower( $email ) && 'bypass' === $allowed ) {
// If email matches and username bypass is enabled, check if there is a conflict with the username.
$prevent_conflict = get_user_by( 'user_login', $username );

// If email matches and there is no existing account with the username, safe to merge.
if ( ! $prevent_conflict->ID ) {
$set_user_idp = true;
} else {
// If there is an existing account with the email, fail.
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts using username bypass. Reason: An account already exists with the email: ' . $email . ' .' );
}
} else {
// If no other conditions are met, fail.
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Username and email do not match what is provided by attributes. Username provided by attribute is: ' . $username . ' and email provided by attribute is ' . $email . '.' );
}

if ( $set_user_idp ) {
if ( shibboleth_set_user_idp( $user->ID ) ) {
shibboleth_log_message( 'account_merge', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts manually for IdP: ' . shibboleth_get_user_idp( $user->ID ) . '.' );
wp_safe_redirect( get_edit_user_link() . '?shibboleth=linked' );
exit;
} else {
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Unable to automatically determine IdP.' );
}
}

wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
exit;
} else {
// If there is no existing shibboleth session, kick to the shibboleth_session_initiator_url
// and redirect to this page with the ?shibboleth=link action.
$initiator_url = shibboleth_session_initiator_url( wp_nonce_url( get_edit_user_link() . '?shibboleth=link', 'shibboleth-link' ) );
wp_redirect( $initiator_url );
exit;
}
// If manual merging is disabled, fail.
} else {
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Manual account merging is disabled.' );
wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
exit;
}
// If account is already merged, warn.
if ( ! is_admin() || 'profile' !== get_current_screen()->id ) {
return;
}

$user_id = get_current_user_id();

// If profile page has ?shibboleth=link action and current user can edit their profile, proceed.
if ( isset( $_GET['shibboleth'] ) && 'link' === $_GET['shibboleth'] && current_user_can( 'edit_user', $user_id ) ) {
check_admin_referer( 'shibboleth-link' );

$allowed = shibboleth_getoption( 'shibboleth_manually_combine_accounts', 'disallow' );

$user_idp = shibboleth_get_user_idp( $user_id );

// If account is already merged, warn.
if ( ! empty( $user_idp ) ) {
shibboleth_log_message( 'account_merge', 'WARN: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: User\'s account is already merged.' );
wp_safe_redirect( get_edit_user_link() . '?shibboleth=duplicate' );
exit;
}

// If manual account merging is disabled, fail.
if ( 'allow' !== $allowed && 'bypass' !== $allowed ) {
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Manual account merging is disabled.' );
wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
exit;
}

// If there is no existing shibboleth session, initiate a session with the ?shibboleth=link action.
if ( ! shibboleth_session_active() ) {
$initiator_url = shibboleth_session_initiator_url( wp_nonce_url( get_edit_user_link() . '?shibboleth=link', 'shibboleth-link' ) );
shibboleth_allow_redirect( $initiator_url );
wp_safe_redirect( $initiator_url );
exit;
}

$shib_headers = shibboleth_getoption( 'shibboleth_headers', array() );

$username = shibboleth_getenv( $shib_headers['username']['name'] );
$email = shibboleth_getenv( $shib_headers['email']['name'] );

$user = get_user_by( 'id', $user_id );

$set_user_idp = false;

if ( $user->user_login === $username && strtolower( $user->user_email ) === strtolower( $email ) ) {
// If username and email match, safe to merge.
$set_user_idp = true;
} elseif ( $user->user_login === $username ) {
// If username matches, check if there is a conflict with the email.
$prevent_conflict = get_user_by( 'email', $email );

// If username matches and there is no existing account with the email, safe to merge.
if ( ! $prevent_conflict->ID ) {
$set_user_idp = true;
} else {
// If username matches and there is an existing account with the email, fail.
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: An account already exists with the email: ' . $email . ' .' );
}
} elseif ( strtolower( $user->user_email ) === strtolower( $email ) && 'bypass' === $allowed ) {
// If email matches and username bypass is enabled, check if there is a conflict with the username.
$prevent_conflict = get_user_by( 'user_login', $username );

// If email matches and there is no existing account with the username, safe to merge.
if ( ! $prevent_conflict->ID ) {
$set_user_idp = true;
} else {
shibboleth_log_message( 'account_merge', 'WARN: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: User\'s account is already merged.' );
wp_safe_redirect( get_edit_user_link() . '?shibboleth=duplicate' );
// If there is an existing account with the email, fail.
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts using username bypass. Reason: An account already exists with the email: ' . $email . ' .' );
}
} else {
// If no other conditions are met, fail.
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Username and email do not match what is provided by attributes. Username provided by attribute is: ' . $username . ' and email provided by attribute is ' . $email . '.' );
}

if ( $set_user_idp ) {
if ( shibboleth_set_user_idp( $user->ID ) ) {
shibboleth_log_message( 'account_merge', 'SUCCESS: User ' . $user->user_login . ' (ID: ' . $user->ID . ') merged accounts manually for IdP: ' . shibboleth_get_user_idp( $user->ID ) . '.' );
wp_safe_redirect( get_edit_user_link() . '?shibboleth=linked' );
exit;
} else {
shibboleth_log_message( 'account_merge', 'ERROR: User ' . $user->user_login . ' (ID: ' . $user->ID . ') failed to manually merge accounts. Reason: Unable to automatically determine IdP.' );
}
}

wp_safe_redirect( get_edit_user_link() . '?shibboleth=failed' );
exit;
}
}
add_action( 'current_screen', 'shibboleth_link_accounts' );
Expand Down Expand Up @@ -313,6 +312,7 @@ function shibboleth_disable_password_changes() {
* @since 1.9
*/
function shibboleth_link_accounts_notice() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$message_code = isset( $_GET['shibboleth'] ) ? sanitize_key( wp_unslash( $_GET['shibboleth'] ) ) : '';

if ( 'failed' === $message_code ) {
Expand Down
7 changes: 5 additions & 2 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
Contributors: michaelryanmcneill, willnorris, mitchoyoshitaka, jrchamp, dericcrago, bshelton229, Alhrath, dandalpiaz, masteradhoc, junaidkbr
Tags: shibboleth, authentication, login, saml
Requires at least: 4.0
Tested up to: 6.8
Tested up to: 6.9
Requires PHP: 5.6
Stable tag: 2.5.2
Stable tag: 2.5.3
License: Apache-2.0

Allows WordPress to externalize user authentication and account creation to a Shibboleth Service Provider.
Expand Down Expand Up @@ -195,6 +195,9 @@ Accessing Shibboleth attributes has changed. Typically, no additional configurat
Accessing Shibboleth attributes has changed. Typically, no additional configuration is necessary. Check the changelog if you have specialized server configurations, such as a Shibboleth Service Provider on a reverse proxy or a server configuration that prefixes environment variables with REDIRECT_.

== Changelog ==
= version 2.5.3 (2026-02-23) =
- Security: Limit redirects to approved hosts [#112](https://github.com/michaelryanmcneill/shibboleth/issues/112) (thanks @Belippo, @Jefhumbe)

= version 2.5.2 (2025-07-22) =
- Compatibility: PHP 8.0 and newer require count() argument to be countable [#108](https://github.com/michaelryanmcneill/shibboleth/issues/108) (thanks @Gameink, @frereut)

Expand Down
Loading