Created
September 25, 2024 15:01
-
-
Save kasparsd/4560f3013afccbaa327beb4dc90eb477 to your computer and use it in GitHub Desktop.
A prototype of native WP multisite single sign-on (SSO)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Plugin Name: Multisite SSO Login | |
* Network: true | |
* | |
* @see https://x.com/konstruktors/status/1838956536049094659 | |
*/ | |
if ( ! is_multisite() ) { | |
return; | |
} | |
function is_sso_site() { | |
return is_main_site(); | |
} | |
function get_sso_login_url( $query_args = [] ) { | |
$query_args = wp_parse_args( | |
$query_args, | |
[ | |
'multisite_sso_blog_id' => get_current_blog_id(), | |
] | |
); | |
return add_query_arg( $query_args, network_site_url( 'wp-login.php', 'login' ) ); | |
} | |
function get_sso_callback_url( $blog_id, $query_args = [] ) { | |
return add_query_arg( $query_args, get_site_url( $blog_id, 'wp-login.php', 'login' ) ); | |
} | |
add_filter( | |
'login_message', | |
function ( $messages ) { | |
if ( is_user_logged_in() || is_sso_site() ) { | |
return; | |
} | |
$sso_args = []; | |
$redirect_to = filter_input( INPUT_GET, 'redirect_to', FILTER_SANITIZE_URL ); | |
if ( $redirect_to ) { | |
$sso_args['redirect_to'] = rawurlencode( $redirect_to ); | |
} | |
$messages .= '<style type="text/css"> | |
.login .wp-multisite-sso-login--cta a { font-size:inherit; float:none; display:inline-block; width:100%; clear:both; text-align:center; } | |
</style>'; | |
$messages .= sprintf( | |
'<div class="message wp-multisite-sso-login--cta"> | |
<p><a href="%s" class="button button-primary button-large">%s</a></p> | |
</div>', | |
esc_url( get_sso_login_url( $sso_args ) ), | |
esc_html__( 'Login with Multisite SSO' ), | |
); | |
return $messages; | |
}, | |
200 | |
); | |
add_action( | |
'login_init', | |
function () { | |
if ( ! is_sso_site() || is_user_logged_in() ) { | |
return; | |
} | |
$blog_id = isset( $_REQUEST['multisite_sso_blog_id'] ) ? absint( $_REQUEST['multisite_sso_blog_id'] ) : null; | |
if ( empty( $blog_id ) ) { | |
return; | |
} | |
$blog = get_blog_details( $blog_id ); | |
if ( ! $blog ) { | |
return; | |
} | |
add_action( | |
'login_form', | |
function () use ( $blog_id ) { | |
?> | |
<input type="hidden" name="multisite_sso_blog_id" value="<?php echo esc_attr( $blog_id ); ?>"> | |
<?php | |
} | |
); | |
add_filter( | |
'wp_login_errors', | |
function ( $errors ) use ( $blog ) { | |
$message = sprintf( | |
esc_html__( 'You are logging into %s' ), | |
sprintf( | |
'<a href="%s" target="_blank">%s</a>', | |
esc_url( $blog->home ), | |
esc_html( $blog->blogname ) | |
) | |
); | |
$errors->add( 'multisite_sso_notice', $message, 'message' ); | |
return $errors; | |
} | |
); | |
} | |
); | |
add_filter( | |
'login_redirect', | |
function ( $redirect_to, $requested_redirect_to, $user ) { | |
if ( ! is_sso_site() ) { | |
return $redirect_to; | |
} | |
$blog_id = isset( $_REQUEST['multisite_sso_blog_id'] ) ? absint( $_REQUEST['multisite_sso_blog_id'] ) : null; | |
if ( ! empty( $blog_id ) && $user instanceof WP_User ) { | |
// Add the destination blog hostname to the allowed safe-redirect list. | |
add_filter( | |
'allowed_redirect_hosts', | |
function ( $hosts ) use ( $blog_id ) { | |
$hosts[] = wp_parse_url( get_site_url( $blog_id ), PHP_URL_HOST ); | |
$hosts[] = wp_parse_url( get_home_url( $blog_id ), PHP_URL_HOST ); | |
return $hosts; | |
} | |
); | |
$session_token = wp_generate_password( 42, false, false ); | |
// Relay the private session token through a back-channel. | |
update_user_meta( $user->ID, 'multisite_sso_token', $session_token ); | |
$args = [ | |
'multisite_sso_user_id' => $user->ID, | |
'multisite_sso_token' => wp_hash( $session_token ), // Transfer hashed for one-time verification. | |
]; | |
if ( ! empty( $requested_redirect_to ) && $redirect_to !== $requested_redirect_to ) { | |
$args['redirect_to'] = urlencode( $requested_redirect_to ); | |
} | |
return get_sso_callback_url( $blog_id, $args ); | |
} | |
return $redirect_to; | |
}, | |
100, | |
3 | |
); | |
add_action( | |
'login_init', | |
function () { | |
if ( is_sso_site() || is_user_logged_in() ) { | |
return; | |
} | |
$login_user_id = isset( $_GET['multisite_sso_user_id'] ) ? absint( $_GET['multisite_sso_user_id'] ) : null; | |
$login_token_hash = isset( $_GET['multisite_sso_token'] ) ? sanitize_text_field( $_GET['multisite_sso_token'] ) : null; | |
if ( ! $login_user_id || ! $login_token_hash ) { | |
return; | |
} | |
// Get the session token from the SSO source and delete it to ensure it is used only once. | |
$token_source = get_user_meta( $login_user_id, 'multisite_sso_token', true ); | |
delete_user_meta( $login_user_id, 'multisite_sso_token' ); | |
if ( $token_source && hash_equals( wp_hash( $token_source ), $login_token_hash ) ) { | |
wp_clear_auth_cookie(); | |
wp_set_auth_cookie( $login_user_id ); | |
wp_set_current_user( $login_user_id ); | |
$redirect_to = filter_input( INPUT_GET, 'redirect_to', FILTER_SANITIZE_URL ); | |
if ( ! $redirect_to ) { | |
$redirect_to = get_dashboard_url( $login_user_id ); | |
} | |
wp_redirect( $redirect_to ); | |
exit; | |
} | |
} | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment