forked from emfluencekc/multisite-multidomain-single-sign-on
-
Notifications
You must be signed in to change notification settings - Fork 1
/
multisite-multidomain-single-sign-on.php
290 lines (243 loc) · 9.9 KB
/
multisite-multidomain-single-sign-on.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
<?php
/*
Plugin Name: Multisite Multidomain Single Sign On
Description: Automatically sign the user in to separate-domain sites of the same multisite installation, when switching sites using the 'My Sites' links in the admin menu. Note that the user already has to be logged into a site in the network, this plugin just cuts down on having to log in again due to cookie isolation between domains. Note: This plugin must be installed on all sites in a network in order to work.
Version: 1.3.8
Requires at least: 5.0
Tested up to: 5.7.2
Requires PHP: 7.4
Author: emfluence Digital Marketing
Author URI: https://emfluence.com
License: GPL2
*/
const MMSSO_FROM_QUERY_VAR = 'sso-f';
const MMSSO_RETURN_TO_QUERY_VAR = 'sso-r';
const MMSSO_NONCE = 'sso-n';
const MMSSO_HASH_QUERY_VAR = 'sso-h';
const MMSSO_USER_ID_QUERY_VAR = 'sso-u';
const MMSSO_EXPIRES_QUERY_VAR = 'sso-e';
const MMSSO_NONCE_PREFIX = 'mmsso-';
/** @noinspection AutoloadingIssuesInspection */
class Multisite_Multidomain_Single_Sign_On {
/**
* Store the singleton instance
*
* @var Multisite_Multidomain_Single_Sign_On
*/
protected static $instance;
/**
* Create singleton instance.
*
* @return Multisite_Multidomain_Single_Sign_On
*/
public static function get_instance() : Multisite_Multidomain_Single_Sign_On {
if ( ! self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor. We can only be constructed via the get_instance method.
*/
private function __construct() {
add_action( 'wp_before_admin_bar_render', [ $this, 'change_site_switcher_links' ] );
add_action( 'init', [ $this, 'receive_sso_request' ] );
add_action( 'init', [ $this, 'authorize_request' ] );
add_action( 'init', [ $this, 'receive_auth' ] );
}
/**
* Change the links in the admin menu bar
*
* @see WP_Admin_Bar
* @see wp_admin_bar_my_sites_menu()
*/
public function change_site_switcher_links() : void {
global $wp_admin_bar;
$nodes = $wp_admin_bar->get_nodes();
$current_site_id = get_current_blog_id();
$current_site = get_site( $current_site_id );
foreach ( $nodes as $id => $node ) {
if ( empty( $node->href ) ) {
continue;
}
$is_site_node = ( 0 === stripos( $id, 'blog' ) );
$is_network_admin_node = ( 0 === stripos( $id, 'network-admin' ) );
if ( ! ( $is_site_node || $is_network_admin_node ) ) {
continue;
}
if ( in_array( $current_site->domain, explode( '/', $node->href ), true ) ) {
continue;
}
$node->href = $this->add_sso_to_url( $node->href );
$wp_admin_bar->add_node( $node );
}
}
/**
* Add Single Sign-on parameters to the passed in URL.
*
* @param string $url The passed in URL.
*
* @return string The url with added parameters.
*/
public function add_sso_to_url( string $url ) : string {
$current_site_id = get_current_blog_id();
$target_url_parts = wp_parse_url( $url );
if ( $target_url_parts === false || $target_url_parts === null || empty( $target_url_parts['host'] ) || empty( $target_url_parts['path'] ) ) {
return $url;
}
$site = get_site( $current_site_id );
if ( $target_url_parts['host'] === $site->domain ) {
return $url;
}
$target_site = get_site_by_path( $target_url_parts['host'], $target_url_parts['path'] );
if ( $target_site === false ) {
\Zed1\Debug\zed1_debug( 'Cannot find site from parts:', $target_url_parts );
return $url;
}
$nonce = wp_create_nonce( MMSSO_NONCE_PREFIX . $current_site_id . '-' . $target_site->blog_id );
return add_query_arg(
[
MMSSO_FROM_QUERY_VAR => $current_site_id,
MMSSO_NONCE => $nonce,
],
$url
);
}
/*
* Initiate the workflow on a target site that the user wants to log into.
*/
public function receive_sso_request() : void {
if ( empty( $_GET[ MMSSO_FROM_QUERY_VAR ] ) ) {
return;
} // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( is_user_logged_in() ) {
wp_redirect( remove_query_arg( [ MMSSO_FROM_QUERY_VAR, MMSSO_NONCE ] ) );
exit();
}
$coming_from = (int) $_GET[ MMSSO_FROM_QUERY_VAR ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$sso_site = get_site( $coming_from );
if ( $sso_site === null ) {
wp_die( 'Single Sign On is attempting to use an invalid site on this multisite.' );
}
if ( empty( $_GET[ MMSSO_NONCE ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Recommended
wp_die( 'Single Sign On was attempted with a missing key.' );
}
$return_url = get_site_url() . remove_query_arg( [ MMSSO_FROM_QUERY_VAR, MMSSO_NONCE ] );
$next_url = add_query_arg( [
MMSSO_RETURN_TO_QUERY_VAR => $return_url,
MMSSO_NONCE => sanitize_text_field( $_GET[ MMSSO_NONCE ] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended
], get_site_url( $coming_from ) );
wp_redirect( $next_url );
exit();
}
/**
* Used on the authorizing site
*/
public function authorize_request() : void {
if ( empty( $_GET[ MMSSO_RETURN_TO_QUERY_VAR ] ) ) {
return;
}
if ( ! is_user_logged_in() ) {
wp_die( 'Single Sign On requires that you be logged in. Please <a href="' . esc_url( wp_login_url() ) . '">log in</a>, then try again.' );
}
$return_url = esc_url_raw( $_GET[ MMSSO_RETURN_TO_QUERY_VAR ] );
// Prevent phishing attacks, make sure that the return-to site that gets the auth is a domain on this network.
$url_parts = explode( '/', $return_url );
$requesting_site_id = get_blog_id_from_url( $url_parts[2] );
if ( empty( $requesting_site_id ) ) {
wp_die( 'Single Sign On failed. The requested site could not be found on this network. If someone gave you this link, they may have sent you a phishing attack.' );
}
if ( empty( $_GET[ MMSSO_NONCE ] ) || ! wp_verify_nonce( $_GET[ MMSSO_NONCE ],
MMSSO_NONCE_PREFIX . get_current_blog_id() . '-' . $requesting_site_id ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
wp_die( 'Single Sign On was attempted with a missing or bad key.' );
}
$current_user = wp_get_current_user();
$expires = strtotime( '+2 minutes' );
/*
* The user's password hash is a user-specific, expire-able, private piece of information
* that prevents brute force hacking of the salt if an attacker has the query parameters.
*/
$user_pass_hash = $this->get_user_password_hash( $current_user->ID );
if ( empty( $user_pass_hash ) ) {
wp_die( 'Single Sign On failed. Your password hash was empty. Try changing your Wordpress password.' );
}
$hash = $this->hash( implode( '||', [ $current_user->ID, (int) $expires, $user_pass_hash ] ) );
if ( empty( $hash ) ) {
wp_die( 'Single Sign On failed. The network needs a secure salt.' );
}
$next_url = add_query_arg( [
MMSSO_HASH_QUERY_VAR => $hash,
MMSSO_USER_ID_QUERY_VAR => $current_user->ID,
MMSSO_EXPIRES_QUERY_VAR => $expires,
], $return_url );
wp_redirect( $next_url );
exit();
}
/*
* Final step, used on the target site.
*/
public function receive_auth() : void {
$keys = [ MMSSO_HASH_QUERY_VAR, MMSSO_USER_ID_QUERY_VAR, MMSSO_EXPIRES_QUERY_VAR ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
foreach ( $keys as $key ) {
if ( empty( $_GET[ $key ] ) ) {
return;
} // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
$final_destination = remove_query_arg( $keys );
if ( is_user_logged_in() ) {
wp_redirect( $final_destination );
exit();
}
$user_id = (int) $_GET[ MMSSO_USER_ID_QUERY_VAR ]; // phpcs:ignore:WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended
$expires = (int) $_GET[ MMSSO_EXPIRES_QUERY_VAR ]; // phpcs:ignore:WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended
$received_hash = sanitize_text_field( $_GET[ MMSSO_HASH_QUERY_VAR ] ); // phpcs:ignore:WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.NonceVerification.Recommended
if ( $expires < time() ) {
wp_die( 'Your Single Sign On link has expired. Please return to the dashboard and try again.' );
}
$user_pass_hash = $this->get_user_password_hash( $user_id );
$expected_hash = $this->hash( implode( '||', [ $user_id, $expires, $user_pass_hash ] ) );
if ( empty( $expected_hash ) ) {
wp_die( 'Single Sign On failed. The network needs a secure salt.' );
}
if ( ! hash_equals( $expected_hash, $received_hash ) ) {
wp_die( 'Single Sign On has found an error in the URL that you are trying to use.' );
}
if ( ! apply_filters( 'MMSSO_receive_auth_user_can', user_can( $user_id, 'read' ), $user_id ) ) {
wp_die( 'Single Sign On is trying to log you in, but your user account is not authorized for this site. Please contact a network admin and ask them to add you to this site.' );
}
wp_set_auth_cookie( $user_id, true );
// Just so that we don't leave the user on a URL with a bunch of our parameters.
wp_redirect( $final_destination );
exit();
}
/**
* @param int $uid
*
* @return string|null
*/
protected function get_user_password_hash( int $uid ) : ?string {
global $wpdb;
$hash = $wpdb->get_var( $wpdb->prepare( "SELECT user_pass FROM $wpdb->users WHERE ID = %d",
$uid ) ); // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.user_meta__wpdb__users
return empty( $hash ) ?
$hash :
substr( $hash, 0, -2 ); // It's a bit safer to use only part of the password hash
}
/**
* Create a secure hash that can only be recreated from this WordPress' secret salt.
*
* @param string $thing
*
* @return false|string
*/
protected function hash( string $thing ) {
if ( ! function_exists( 'hash' ) ) {
return false;
}
if ( ! defined( 'AUTH_SALT' ) || empty( AUTH_SALT ) ) {
return false;
}
return hash_hmac( 'sha256', $thing, AUTH_SALT );
}
}
Multisite_Multidomain_Single_Sign_On::get_instance();