-
Notifications
You must be signed in to change notification settings - Fork 1
/
pvtl-sso.php
350 lines (298 loc) · 9.13 KB
/
pvtl-sso.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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
<?php
/**
* Plugin Name: PVTL SSO
* Plugin URI: https://github.com/pvtl/wordpress-pvtl-sso-plugin
* Description: SSO for Pivotal Agency staff, to login to WordPress with minimal effort
* Author: Pivotal Agency
* Author URI: http://pivotal.agency
* Text Domain: pvtl-sso
* Domain Path: /languages
* Version: 1.1.12
*
* @package PVTL_SSO
*/
namespace App\Plugins\Pvtl;
/**
* Pivotal Agency Single Sign On Plugin
*/
class PvtlSso {
/**
* The name of the plugin (for cosmetic purposes).
*
* @var string
*/
protected $plugin_name = 'PVTL SSO';
/**
* The username/password to kick things off.
*
* @var string
*/
protected $intercept_when = 'pvtladmin';
/**
* The URL to access SSO application.
*
* @var string
*/
protected $fetch_token_url = 'https://sso.pvtl.io/sso/create_token.php';
/**
* The URL to verify the token.
*
* @var string
*/
protected $verify_token_url = 'https://sso.pvtl.io/sso/check_token.php';
/**
* User's email.
*
* @var string
*/
protected $user_email = '';
/**
* User's full name.
*
* @var string
*/
protected $user_name = '';
/**
* User's first name.
*
* @var string
*/
protected $user_firstname = '';
/**
* User's last name.
*
* @var string
*/
protected $user_lastname = '';
/**
* User's nickname/display name.
*
* @var string
*/
protected $user_nickname = '';
/**
* Constructor
*/
public function __construct() {
// Add the filters to WP.
add_filter( 'authenticate', array( $this, 'if_pvtl_go_sso' ), 20, 3 );
add_filter( 'wp_loaded', array( $this, 'check_wplogin_token' ) );
}
/**
* Redirect to SSO if user is attempting to authenticate as a Pivotal user
*
* @param null|WP_User|WP_Error $user - the user object if successful.
* @param string $username - username used to login.
* @param string $password - password used to login.
*/
public function if_pvtl_go_sso( $user, $username = '', $password = '' ) {
// Is a Pivotal user. Redirect to SSO app.
if ( $this->intercept_when === $username && $this->intercept_when === $password ) {
$return_url = apply_filters( 'pvtl_sso_return_url', wp_get_referer() ?: wp_login_url() );
header(
sprintf(
'location: %s?return=%s',
$this->fetch_token_url,
$return_url
)
);
exit();
}
// Not a Pivotal user, continue on your merry way.
return $user;
}
/**
* If the current URL wp-login.php with a token, verify that token to login
* - eg. /wp/wp-login.php?token=123ABC
*
* @return void
*/
public function check_wplogin_token() {
// We have a token on wp-login.php. Verify the token.
if ( ! empty( $_GET['token'] ) && $this->is_wp_login() ) {
$this->verify_sso_token( $_GET['token'] );
}
}
/**
* When an SSO token is returned to wp-login.php, verify & authenticate
*
* @param string $token - the token to auth with.
*/
private function verify_sso_token( $token ) {
if ( empty( $token ) ) {
return $this->set_error( 'Token is missing' );
}
// Send the token to our remote SSO server to verify.
$response = wp_remote_post(
$this->verify_token_url,
array(
'body' => array(
'token_hash' => urlencode( $token ),
'domain' => $_SERVER['HTTP_HOST'],
'ip' => $_SERVER['REMOTE_ADDR'],
'useragent' => $_SERVER['HTTP_USER_AGENT'],
),
)
);
// Decode the response.
$body = ( ! is_wp_error( $response ) && ! empty( $response['body'] ) )
? json_decode( $response['body'] )
: null;
if ( empty( $body ) ) {
return $this->set_error( 'SSO application failed to respond' );
}
// Success at SSO.
if ( ! empty( $body->member->email ) && true === $body->success ) {
// Keep this data accessible across methods, regardless of the case (it's used in both cases).
$this->user_email = $body->member->email;
$this->user_name = $body->member->name ?: 'Pivotal Agency';
$exploded_name = explode( ' ', $this->user_name, 2 );
$this->user_firstname = $exploded_name[0] ?: 'Pivotal';
$this->user_lastname = $exploded_name[1] ?: 'Agency';
$this->user_nickname = sprintf(
'%s %s (Pivotal Agency)',
$this->user_firstname,
substr( $this->user_lastname, 0, 1 )
);
// If the user exists, this'll be a WP_User object, otherwise it'll be empty.
$user = get_user_by( 'email', $this->user_email );
// Create user when the user doesn't yet exist.
if ( empty( $user ) || ! ( $user instanceof \WP_User ) ) {
$user = $this->create_user();
}
// An unknown error has occured if $user still doesn't exist.
if ( empty( $user ) || ! ( $user instanceof \WP_User ) ) {
return $this->set_error( 'Cannot find nor create user' );
}
// Login and redirect to the dashboard.
return $this->login_as_user( $user );
}
// Wasn't successful at SSO - Show SSO error message on wp-login.php.
return $this->set_error( $body->message );
}
/**
* Based on a WP_User object, login as that user.
*
* @param WP_User $user - the user object.
* @return void|bool - redirects to the dashboard.
*/
private function login_as_user( $user ) {
if ( empty( $user ) || ! ( $user instanceof \WP_User ) ) {
return $this->set_error( 'User is missing from login_as_user()' );
}
// We'll rotate the password, to prevent users manually changing it to get past SSO.
$password = $this->rotate_password( $user->ID );
// Login.
wp_set_auth_cookie( $user->ID, true );
// Update the user on each login, to keep the user's data up to date.
if ( ! $this->update_user( $user->ID ) ) {
return false; // Error message was set in the update_user() call.
}
// Make sure any other (eg. plugin's) hooks are fired after we're logged in (eg. logging).
apply_filters( 'authenticate', $user, $user->user_login, $password );
// Redirect to dashboard.
// If something didn't go right, it'll just return to wp-login.php. No biggy.
wp_redirect( $_GET['redirect_to'] ?? admin_url() );
exit();
}
/**
* Create a new user.
*
* @return WP_User|bool $user - the user object.
*/
private function create_user() {
if ( empty( $this->user_email ) || empty( $this->user_firstname ) || empty( $this->user_lastname ) ) {
return $this->set_error( 'User email/name is missing for create_user()' );
}
$password = wp_generate_password( 24 );
// Create a unique username
// - Some security plugins require email & username to be unique (can't be email)
// - We make it super unique to prevent extra logic in checking if a username exists.
$username = sprintf(
'pvtl-%s-%s',
preg_replace(
'/[^a-z]/',
'',
strtolower( $this->user_firstname . substr( $this->user_lastname, 0, 1 ) )
),
time()
);
// Create the user.
$id = wp_create_user( $username, $password, $this->user_email );
$user = new \WP_User( $id );
if ( empty( $user->ID ) || ! ( $user instanceof \WP_User ) ) {
return $this->set_error( 'User does not exist after creating' );
}
// Set the role to admin.
$user->set_role( 'administrator' );
return $user;
}
/**
* Keep the user's data up to date with SSO
*
* @param int $user_id - The user's ID.
* @return void|bool
*/
private function update_user( $user_id ) {
if ( empty( $user_id ) ) {
return $this->set_error( 'User ID missing in update_user()' );
}
$id_of_updated_user = wp_update_user(
array(
'ID' => $user_id,
'first_name' => $this->user_firstname,
'last_name' => $this->user_lastname,
'nickname' => $this->user_nickname,
'display_name' => $this->user_nickname,
'user_url' => 'https://www.pivotalagency.com.au',
)
);
if ( ! is_int( $id_of_updated_user ) ) {
return $this->set_error( 'Could not update user' );
}
return true;
}
/**
* Changes a user's password to something strong and unique.
*
* @param int $user_id - The user's ID.
* @return null|str
*/
private function rotate_password( $user_id ) {
$password = wp_generate_password( 24 );
wp_set_password( $password, $user_id ); // Unfortunately doesn't return anything to check against.
return $password;
}
/**
* Checks if the current page is wp-login.php
* - Seem convoluted, but taken from a highly voted stackoverflow answer.
*
* @return bool
*/
private function is_wp_login() {
$abs_path = str_replace( array( '\\', '/' ), DIRECTORY_SEPARATOR, ABSPATH );
$included_files = get_included_files();
$page_now = $GLOBALS['pagenow']; // phpcs:ignore
$is_wp_login = ( ( in_array( $abs_path . 'wp-login.php', $included_files ) || in_array( $abs_path . 'wp-register.php', $included_files ) ) || ( isset( $page_now ) && 'wp-login.php' === $page_now ) || '/wp-login.php' === $_SERVER['PHP_SELF'] );
return apply_filters( 'pvtl_sso_is_wp_login', $is_wp_login );
}
/**
* Set an error message for wp-login.php
*
* @param str $message - The error message.
* @return bool
*/
private function set_error( $message ) {
global $error;
if ( empty( $error ) ) {
$error = $message; // phpcs:ignore
} else {
$error = sprintf( '%s, %s', $error, $message ); // phpcs:ignore
}
return false;
}
}
if ( ! defined( 'ABSPATH' ) ) {
exit(); // Exit if accessed directly.
}
new PvtlSso();