PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 8.9.4
Jetpack – WP Security, Backup, Speed, & Growth v8.9.4
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 All 500 releases
jetpack / modules / sso.php
sso.php
1,202 lines 36.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 use Automattic\Jetpack\Roles;
4 use Automattic\Jetpack\Status;
5 use Automattic\Jetpack\Tracking;
6 use Automattic\Jetpack\Redirect;
7
8 require_once( JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-helpers.php' );
9 require_once( JETPACK__PLUGIN_DIR . 'modules/sso/class.jetpack-sso-notices.php' );
10
11 /**
12 * Module Name: Secure Sign On
13 * Module Description: Allow users to log in to this site using WordPress.com accounts
14 * Sort Order: 30
15 * Recommendation Order: 5
16 * First Introduced: 2.6
17 * Requires Connection: Yes
18 * Auto Activate: No
19 * Module Tags: Developers
20 * Feature: Security
21 * Additional Search Queries: sso, single sign on, login, log in, 2fa, two-factor
22 */
23
24 class Jetpack_SSO {
25 static $instance = null;
26
27 private function __construct() {
28
29 self::$instance = $this;
30
31 add_action( 'admin_init', array( $this, 'maybe_authorize_user_after_sso' ), 1 );
32 add_action( 'admin_init', array( $this, 'register_settings' ) );
33 add_action( 'login_init', array( $this, 'login_init' ) );
34 add_action( 'delete_user', array( $this, 'delete_connection_for_user' ) );
35 add_filter( 'jetpack_xmlrpc_methods', array( $this, 'xmlrpc_methods' ) );
36 add_action( 'init', array( $this, 'maybe_logout_user' ), 5 );
37 add_action( 'jetpack_modules_loaded', array( $this, 'module_configure_button' ) );
38 add_action( 'login_form_logout', array( $this, 'store_wpcom_profile_cookies_on_logout' ) );
39 add_action( 'jetpack_unlinked_user', array( $this, 'delete_connection_for_user') );
40 add_action( 'wp_login', array( 'Jetpack_SSO', 'clear_cookies_after_login' ) );
41 add_action( 'jetpack_jitm_received_envelopes', array( $this, 'inject_sso_jitm' ), 10, 2 );
42
43 // Adding this action so that on login_init, the action won't be sanitized out of the $action global.
44 add_action( 'login_form_jetpack-sso', '__return_true' );
45 }
46
47 /**
48 * Returns the single instance of the Jetpack_SSO object
49 *
50 * @since 2.8
51 * @return Jetpack_SSO
52 **/
53 public static function get_instance() {
54 if ( ! is_null( self::$instance ) ) {
55 return self::$instance;
56 }
57
58 return self::$instance = new Jetpack_SSO;
59 }
60
61 /**
62 * Add configure button and functionality to the module card on the Jetpack screen
63 **/
64 public static function module_configure_button() {
65 Jetpack::enable_module_configurable( __FILE__ );
66 }
67
68 /**
69 * If jetpack_force_logout == 1 in current user meta the user will be forced
70 * to logout and reauthenticate with the site.
71 **/
72 public function maybe_logout_user() {
73 global $current_user;
74
75 if ( 1 == $current_user->jetpack_force_logout ) {
76 delete_user_meta( $current_user->ID, 'jetpack_force_logout' );
77 self::delete_connection_for_user( $current_user->ID );
78 wp_logout();
79 wp_safe_redirect( wp_login_url() );
80 exit;
81 }
82 }
83
84 /**
85 * Adds additional methods the WordPress xmlrpc API for handling SSO specific features
86 *
87 * @param array $methods
88 * @return array
89 **/
90 public function xmlrpc_methods( $methods ) {
91 $methods['jetpack.userDisconnect'] = array( $this, 'xmlrpc_user_disconnect' );
92 return $methods;
93 }
94
95 /**
96 * Marks a user's profile for disconnect from WordPress.com and forces a logout
97 * the next time the user visits the site.
98 **/
99 public function xmlrpc_user_disconnect( $user_id ) {
100 $user_query = new WP_User_Query(
101 array(
102 'meta_key' => 'wpcom_user_id',
103 'meta_value' => $user_id,
104 )
105 );
106 $user = $user_query->get_results();
107 $user = $user[0];
108
109 if ( $user instanceof WP_User ) {
110 $user = wp_set_current_user( $user->ID );
111 update_user_meta( $user->ID, 'jetpack_force_logout', '1' );
112 self::delete_connection_for_user( $user->ID );
113 return true;
114 }
115 return false;
116 }
117
118 /**
119 * Enqueues scripts and styles necessary for SSO login.
120 */
121 public function login_enqueue_scripts() {
122 global $action;
123
124 if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
125 return;
126 }
127
128 if ( is_rtl() ) {
129 wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login-rtl.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
130 } else {
131 wp_enqueue_style( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.css', JETPACK__PLUGIN_FILE ), array( 'login', 'genericons' ), JETPACK__VERSION );
132 }
133
134 wp_enqueue_script( 'jetpack-sso-login', plugins_url( 'modules/sso/jetpack-sso-login.js', JETPACK__PLUGIN_FILE ), array( 'jquery' ), JETPACK__VERSION );
135 }
136
137 /**
138 * Adds Jetpack SSO classes to login body
139 *
140 * @param array $classes Array of classes to add to body tag
141 * @return array Array of classes to add to body tag
142 */
143 public function login_body_class( $classes ) {
144 global $action;
145
146 if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
147 return $classes;
148 }
149
150 // Always add the jetpack-sso class so that we can add SSO specific styling even when the SSO form isn't being displayed.
151 $classes[] = 'jetpack-sso';
152
153 if ( ! ( new Status() )->is_staging_site() ) {
154 /**
155 * Should we show the SSO login form?
156 *
157 * $_GET['jetpack-sso-default-form'] is used to provide a fallback in case JavaScript is not enabled.
158 *
159 * The default_to_sso_login() method allows us to dynamically decide whether we show the SSO login form or not.
160 * The SSO module uses the method to display the default login form if we can not find a user to log in via SSO.
161 * But, the method could be filtered by a site admin to always show the default login form if that is preferred.
162 */
163 if ( empty( $_GET['jetpack-sso-show-default-form'] ) && Jetpack_SSO_Helpers::show_sso_login() ) {
164 $classes[] = 'jetpack-sso-form-display';
165 }
166 }
167
168 return $classes;
169 }
170
171 public function print_inline_admin_css() {
172 ?>
173 <style>
174 .jetpack-sso .message {
175 margin-top: 20px;
176 }
177
178 .jetpack-sso #login .message:first-child,
179 .jetpack-sso #login h1 + .message {
180 margin-top: 0;
181 }
182 </style>
183 <?php
184 }
185
186 /**
187 * Adds settings fields to Settings > General > Secure Sign On that allows users to
188 * turn off the login form on wp-login.php
189 *
190 * @since 2.7
191 **/
192 public function register_settings() {
193
194 add_settings_section(
195 'jetpack_sso_settings',
196 __( 'Secure Sign On' , 'jetpack' ),
197 '__return_false',
198 'jetpack-sso'
199 );
200
201 /*
202 * Settings > General > Secure Sign On
203 * Require two step authentication
204 */
205 register_setting(
206 'jetpack-sso',
207 'jetpack_sso_require_two_step',
208 array( $this, 'validate_jetpack_sso_require_two_step' )
209 );
210
211 add_settings_field(
212 'jetpack_sso_require_two_step',
213 '', // __( 'Require Two-Step Authentication' , 'jetpack' ),
214 array( $this, 'render_require_two_step' ),
215 'jetpack-sso',
216 'jetpack_sso_settings'
217 );
218
219 /*
220 * Settings > General > Secure Sign On
221 */
222 register_setting(
223 'jetpack-sso',
224 'jetpack_sso_match_by_email',
225 array( $this, 'validate_jetpack_sso_match_by_email' )
226 );
227
228 add_settings_field(
229 'jetpack_sso_match_by_email',
230 '', // __( 'Match by Email' , 'jetpack' ),
231 array( $this, 'render_match_by_email' ),
232 'jetpack-sso',
233 'jetpack_sso_settings'
234 );
235 }
236
237 /**
238 * Builds the display for the checkbox allowing user to require two step
239 * auth be enabled on WordPress.com accounts before login. Displays in Settings > General
240 *
241 * @since 2.7
242 **/
243 public function render_require_two_step() {
244 ?>
245 <label>
246 <input
247 type="checkbox"
248 name="jetpack_sso_require_two_step"
249 <?php checked( Jetpack_SSO_Helpers::is_two_step_required() ); ?>
250 <?php disabled( Jetpack_SSO_Helpers::is_require_two_step_checkbox_disabled() ); ?>
251 >
252 <?php esc_html_e( 'Require Two-Step Authentication' , 'jetpack' ); ?>
253 </label>
254 <?php
255 }
256
257 /**
258 * Validate the require two step checkbox in Settings > General
259 *
260 * @since 2.7
261 * @return boolean
262 **/
263 public function validate_jetpack_sso_require_two_step( $input ) {
264 return ( ! empty( $input ) ) ? 1 : 0;
265 }
266
267 /**
268 * Builds the display for the checkbox allowing the user to allow matching logins by email
269 * Displays in Settings > General
270 *
271 * @since 2.9
272 **/
273 public function render_match_by_email() {
274 ?>
275 <label>
276 <input
277 type="checkbox"
278 name="jetpack_sso_match_by_email"
279 <?php checked( Jetpack_SSO_Helpers::match_by_email() ); ?>
280 <?php disabled( Jetpack_SSO_Helpers::is_match_by_email_checkbox_disabled() ); ?>
281 >
282 <?php esc_html_e( 'Match by Email', 'jetpack' ); ?>
283 </label>
284 <?php
285 }
286
287 /**
288 * Validate the match by email check in Settings > General
289 *
290 * @since 2.9
291 * @return boolean
292 **/
293 public function validate_jetpack_sso_match_by_email( $input ) {
294 return ( ! empty( $input ) ) ? 1 : 0;
295 }
296
297 /**
298 * Checks to determine if the user wants to login on wp-login
299 *
300 * This function mostly exists to cover the exceptions to login
301 * that may exist as other parameters to $_GET[action] as $_GET[action]
302 * does not have to exist. By default WordPress assumes login if an action
303 * is not set, however this may not be true, as in the case of logout
304 * where $_GET[loggedout] is instead set
305 *
306 * @return boolean
307 **/
308 private function wants_to_login() {
309 $wants_to_login = false;
310
311 // Cover default WordPress behavior
312 $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'login';
313
314 // And now the exceptions
315 $action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action;
316
317 if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
318 $wants_to_login = true;
319 }
320
321 return $wants_to_login;
322 }
323
324 function login_init() {
325 global $action;
326
327 $tracking = new Tracking();
328
329 if ( Jetpack_SSO_Helpers::should_hide_login_form() ) {
330 /**
331 * Since the default authenticate filters fire at priority 20 for checking username and password,
332 * let's fire at priority 30. wp_authenticate_spam_check is fired at priority 99, but since we return a
333 * WP_Error in disable_default_login_form, then we won't trigger spam processing logic.
334 */
335 add_filter( 'authenticate', array( 'Jetpack_SSO_Notices', 'disable_default_login_form' ), 30 );
336
337 /**
338 * Filter the display of the disclaimer message appearing when default WordPress login form is disabled.
339 *
340 * @module sso
341 *
342 * @since 2.8.0
343 *
344 * @param bool true Should the disclaimer be displayed. Default to true.
345 */
346 $display_sso_disclaimer = apply_filters( 'jetpack_sso_display_disclaimer', true );
347 if ( $display_sso_disclaimer ) {
348 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'msg_login_by_jetpack' ) );
349 }
350 }
351
352 if ( 'jetpack-sso' === $action ) {
353 if ( isset( $_GET['result'], $_GET['user_id'], $_GET['sso_nonce'] ) && 'success' == $_GET['result'] ) {
354 $this->handle_login();
355 $this->display_sso_login_form();
356 } else {
357 if ( ( new Status() )->is_staging_site() ) {
358 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'sso_not_allowed_in_staging' ) );
359 } else {
360 // Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
361 add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
362 $reauth = ! empty( $_GET['force_reauth'] );
363 $sso_url = $this->get_sso_url_or_die( $reauth );
364
365 // Is this our first SSO Login. Set an option.
366 if ( ! Jetpack_Options::get_option( 'sso_first_login' ) ) {
367 Jetpack_options::update_option( 'sso_first_login', true );
368 }
369
370 $tracking->record_user_event( 'sso_login_redirect_success' );
371 wp_safe_redirect( $sso_url );
372 exit;
373 }
374 }
375 } else if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
376
377 // Save cookies so we can handle redirects after SSO
378 $this->save_cookies();
379
380 /**
381 * Check to see if the site admin wants to automagically forward the user
382 * to the WordPress.com login page AND that the request to wp-login.php
383 * is not something other than login (Like logout!)
384 */
385 if ( Jetpack_SSO_Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) {
386 add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
387 $reauth = ! empty( $_GET['force_reauth'] );
388 $sso_url = $this->get_sso_url_or_die( $reauth );
389 $tracking->record_user_event( 'sso_login_redirect_bypass_success' );
390 wp_safe_redirect( $sso_url );
391 exit;
392 }
393
394 $this->display_sso_login_form();
395 }
396 }
397
398 /**
399 * Ensures that we can get a nonce from WordPress.com via XML-RPC before setting
400 * up the hooks required to display the SSO form.
401 */
402 public function display_sso_login_form() {
403 add_filter( 'login_body_class', array( $this, 'login_body_class' ) );
404 add_action( 'login_head', array( $this, 'print_inline_admin_css' ) );
405
406 if ( ( new Status() )->is_staging_site() ) {
407 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'sso_not_allowed_in_staging' ) );
408 return;
409 }
410
411 $sso_nonce = self::request_initial_nonce();
412 if ( is_wp_error( $sso_nonce ) ) {
413 return;
414 }
415
416 add_action( 'login_form', array( $this, 'login_form' ) );
417 add_action( 'login_enqueue_scripts', array( $this, 'login_enqueue_scripts' ) );
418 }
419
420 /**
421 * Conditionally save the redirect_to url as a cookie.
422 *
423 * @since 4.6.0 Renamed to save_cookies from maybe_save_redirect_cookies
424 */
425 public static function save_cookies() {
426 if ( headers_sent() ) {
427 return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack' ) );
428 }
429
430 setcookie(
431 'jetpack_sso_original_request',
432 esc_url_raw( set_url_scheme( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) ),
433 time() + HOUR_IN_SECONDS,
434 COOKIEPATH,
435 COOKIE_DOMAIN,
436 is_ssl(),
437 true
438 );
439
440 if ( ! empty( $_GET['redirect_to'] ) ) {
441 // If we have something to redirect to
442 $url = esc_url_raw( $_GET['redirect_to'] );
443 setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
444 } elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
445 // Otherwise, if it's already set, purge it.
446 setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
447 }
448 }
449
450 /**
451 * Outputs the Jetpack SSO button and description as well as the toggle link
452 * for switching between Jetpack SSO and default login.
453 */
454 function login_form() {
455 $site_name = get_bloginfo( 'name' );
456 if ( ! $site_name ) {
457 $site_name = get_bloginfo( 'url' );
458 }
459
460 $display_name = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] )
461 ? $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ]
462 : false;
463 $gravatar = ! empty( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] )
464 ? $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ]
465 : false;
466
467 ?>
468 <div id="jetpack-sso-wrap">
469 <?php
470 /**
471 * Allow extension above Jetpack's SSO form.
472 *
473 * @module sso
474 *
475 * @since 8.6.0
476 */
477 do_action( 'jetpack_sso_login_form_above_wpcom' );
478
479 if ( $display_name && $gravatar ) : ?>
480 <div id="jetpack-sso-wrap__user">
481 <img width="72" height="72" src="<?php echo esc_html( $gravatar ); ?>" />
482
483 <h2>
484 <?php
485 echo wp_kses(
486 sprintf( __( 'Log in as <span>%s</span>', 'jetpack' ), esc_html( $display_name ) ),
487 array( 'span' => true )
488 );
489 ?>
490 </h2>
491 </div>
492
493 <?php endif; ?>
494
495
496 <div id="jetpack-sso-wrap__action">
497 <?php echo $this->build_sso_button( array(), 'is_primary' ); ?>
498
499 <?php if ( $display_name && $gravatar ) : ?>
500 <a rel="nofollow" class="jetpack-sso-wrap__reauth" href="<?php echo esc_url( $this->build_sso_button_url( array( 'force_reauth' => '1' ) ) ); ?>">
501 <?php esc_html_e( 'Log in as a different WordPress.com user', 'jetpack' ); ?>
502 </a>
503 <?php else : ?>
504 <p>
505 <?php
506 echo esc_html(
507 sprintf(
508 __( 'You can now save time spent logging in by connecting your WordPress.com account to %s.', 'jetpack' ),
509 esc_html( $site_name )
510 )
511 );
512 ?>
513 </p>
514 <?php endif; ?>
515 </div>
516
517 <?php
518 /**
519 * Allow extension below Jetpack's SSO form.
520 *
521 * @module sso
522 *
523 * @since 8.6.0
524 */
525 do_action( 'jetpack_sso_login_form_below_wpcom' );
526
527 if ( ! Jetpack_SSO_Helpers::should_hide_login_form() ) : ?>
528 <div class="jetpack-sso-or">
529 <span><?php esc_html_e( 'Or', 'jetpack' ); ?></span>
530 </div>
531
532 <a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '1' ) ); ?>" class="jetpack-sso-toggle wpcom">
533 <?php
534 esc_html_e( 'Log in with username and password', 'jetpack' )
535 ?>
536 </a>
537
538 <a href="<?php echo esc_url( add_query_arg( 'jetpack-sso-show-default-form', '0' ) ); ?>" class="jetpack-sso-toggle default">
539 <?php
540 esc_html_e( 'Log in with WordPress.com', 'jetpack' )
541 ?>
542 </a>
543 <?php endif; ?>
544 </div>
545 <?php
546 }
547
548 /**
549 * Clear the cookies that store the profile information for the last
550 * WPCOM user to connect.
551 */
552 static function clear_wpcom_profile_cookies() {
553 if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_name_' . COOKIEHASH ] ) ) {
554 setcookie(
555 'jetpack_sso_wpcom_name_' . COOKIEHASH,
556 ' ',
557 time() - YEAR_IN_SECONDS,
558 COOKIEPATH,
559 COOKIE_DOMAIN,
560 is_ssl()
561 );
562 }
563
564 if ( isset( $_COOKIE[ 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH ] ) ) {
565 setcookie(
566 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
567 ' ',
568 time() - YEAR_IN_SECONDS,
569 COOKIEPATH,
570 COOKIE_DOMAIN,
571 is_ssl()
572 );
573 }
574 }
575
576 /**
577 * Clear cookies that are no longer needed once the user has logged in.
578 *
579 * @since 4.8.0
580 */
581 static function clear_cookies_after_login() {
582 self::clear_wpcom_profile_cookies();
583 if ( isset( $_COOKIE[ 'jetpack_sso_nonce' ] ) ) {
584 setcookie(
585 'jetpack_sso_nonce',
586 ' ',
587 time() - YEAR_IN_SECONDS,
588 COOKIEPATH,
589 COOKIE_DOMAIN,
590 is_ssl()
591 );
592 }
593
594 if ( isset( $_COOKIE[ 'jetpack_sso_original_request' ] ) ) {
595 setcookie(
596 'jetpack_sso_original_request',
597 ' ',
598 time() - YEAR_IN_SECONDS,
599 COOKIEPATH,
600 COOKIE_DOMAIN,
601 is_ssl()
602 );
603 }
604
605 if ( isset( $_COOKIE[ 'jetpack_sso_redirect_to' ] ) ) {
606 setcookie(
607 'jetpack_sso_redirect_to',
608 ' ',
609 time() - YEAR_IN_SECONDS,
610 COOKIEPATH,
611 COOKIE_DOMAIN,
612 is_ssl()
613 );
614 }
615 }
616
617 static function delete_connection_for_user( $user_id ) {
618 if ( ! $wpcom_user_id = get_user_meta( $user_id, 'wpcom_user_id', true ) ) {
619 return;
620 }
621
622 $xml = new Jetpack_IXR_Client( array(
623 'wpcom_user_id' => $user_id,
624 ) );
625 $xml->query( 'jetpack.sso.removeUser', $wpcom_user_id );
626
627 if ( $xml->isError() ) {
628 return false;
629 }
630
631 // Clean up local data stored for SSO
632 delete_user_meta( $user_id, 'wpcom_user_id' );
633 delete_user_meta( $user_id, 'wpcom_user_data' );
634 self::clear_wpcom_profile_cookies();
635
636 return $xml->getResponse();
637 }
638
639 static function request_initial_nonce() {
640 $nonce = ! empty( $_COOKIE[ 'jetpack_sso_nonce' ] )
641 ? $_COOKIE[ 'jetpack_sso_nonce' ]
642 : false;
643
644 if ( ! $nonce ) {
645 $xml = new Jetpack_IXR_Client();
646 $xml->query( 'jetpack.sso.requestNonce' );
647
648 if ( $xml->isError() ) {
649 return new WP_Error( $xml->getErrorCode(), $xml->getErrorMessage() );
650 }
651
652 $nonce = $xml->getResponse();
653
654 setcookie(
655 'jetpack_sso_nonce',
656 $nonce,
657 time() + ( 10 * MINUTE_IN_SECONDS ),
658 COOKIEPATH,
659 COOKIE_DOMAIN,
660 is_ssl()
661 );
662 }
663
664 return sanitize_key( $nonce );
665 }
666
667 /**
668 * The function that actually handles the login!
669 */
670 function handle_login() {
671 $wpcom_nonce = sanitize_key( $_GET['sso_nonce'] );
672 $wpcom_user_id = (int) $_GET['user_id'];
673
674 $xml = new Jetpack_IXR_Client();
675 $xml->query( 'jetpack.sso.validateResult', $wpcom_nonce, $wpcom_user_id );
676
677 $user_data = $xml->isError() ? false : $xml->getResponse();
678 if ( empty( $user_data ) ) {
679 add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
680 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_invalid_response_data' ) );
681 return;
682 }
683
684 $user_data = (object) $user_data;
685 $user = null;
686
687 /**
688 * Fires before Jetpack's SSO modifies the log in form.
689 *
690 * @module sso
691 *
692 * @since 2.6.0
693 *
694 * @param object $user_data WordPress.com User information.
695 */
696 do_action( 'jetpack_sso_pre_handle_login', $user_data );
697
698 $tracking = new Tracking();
699
700 if ( Jetpack_SSO_Helpers::is_two_step_required() && 0 === (int) $user_data->two_step_enabled ) {
701 $this->user_data = $user_data;
702
703 $tracking->record_user_event( 'sso_login_failed', array(
704 'error_message' => 'error_msg_enable_two_step'
705 ) );
706
707 $error = new WP_Error( 'two_step_required', __( 'You must have Two-Step Authentication enabled on your WordPress.com account.', 'jetpack' ) );
708
709 /** This filter is documented in core/src/wp-includes/pluggable.php */
710 do_action( 'wp_login_failed', $user_data->login, $error );
711 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_enable_two_step' ) );
712 return;
713 }
714
715 $user_found_with = '';
716 if ( empty( $user ) && isset( $user_data->external_user_id ) ) {
717 $user_found_with = 'external_user_id';
718 $user = get_user_by( 'id', intval( $user_data->external_user_id ) );
719 if ( $user ) {
720 $expected_id = get_user_meta( $user->ID, 'wpcom_user_id', true );
721 if ( $expected_id && $expected_id != $user_data->ID ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
722 $error = new WP_Error( 'expected_wpcom_user', __( 'Something got a little mixed up and an unexpected WordPress.com user logged in.', 'jetpack' ) );
723
724 /** This filter is documented in core/src/wp-includes/pluggable.php */
725 do_action( 'wp_login_failed', $user_data->login, $error );
726 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_invalid_response_data' ) ); // @todo Need to have a better notice. This is only for the sake of testing the validation.
727 return;
728 }
729 update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
730 }
731 }
732
733 // If we don't have one by wpcom_user_id, try by the email?
734 if ( empty( $user ) && Jetpack_SSO_Helpers::match_by_email() ) {
735 $user_found_with = 'match_by_email';
736 $user = get_user_by( 'email', $user_data->email );
737 if ( $user ) {
738 update_user_meta( $user->ID, 'wpcom_user_id', $user_data->ID );
739 }
740 }
741
742 // If we've still got nothing, create the user.
743 $new_user_override_role = false;
744 if ( empty( $user ) && ( get_option( 'users_can_register' ) || ( $new_user_override_role = Jetpack_SSO_Helpers::new_user_override( $user_data ) ) ) ) {
745 /**
746 * If not matching by email we still need to verify the email does not exist
747 * or this blows up
748 *
749 * If match_by_email is true, we know the email doesn't exist, as it would have
750 * been found in the first pass. If get_user_by( 'email' ) doesn't find the
751 * user, then we know that email is unused, so it's safe to add.
752 */
753 if ( Jetpack_SSO_Helpers::match_by_email() || ! get_user_by( 'email', $user_data->email ) ) {
754
755 if ( $new_user_override_role ) {
756 $user_data->role = $new_user_override_role;
757 }
758
759 $user = Jetpack_SSO_Helpers::generate_user( $user_data );
760 if ( ! $user ) {
761 $tracking->record_user_event( 'sso_login_failed', array(
762 'error_message' => 'could_not_create_username'
763 ) );
764 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'error_unable_to_create_user' ) );
765 return;
766 }
767
768 $user_found_with = $new_user_override_role
769 ? 'user_created_new_user_override'
770 : 'user_created_users_can_register';
771 } else {
772 $tracking->record_user_event( 'sso_login_failed', array(
773 'error_message' => 'error_msg_email_already_exists'
774 ) );
775
776 $this->user_data = $user_data;
777 add_action( 'login_message', array( 'Jetpack_SSO_Notices', 'error_msg_email_already_exists' ) );
778 return;
779 }
780 }
781
782 /**
783 * Fires after we got login information from WordPress.com.
784 *
785 * @module sso
786 *
787 * @since 2.6.0
788 *
789 * @param WP_User|false|null $user Local User information.
790 * @param object $user_data WordPress.com User Login information.
791 */
792 do_action( 'jetpack_sso_handle_login', $user, $user_data );
793
794 if ( $user ) {
795 // Cache the user's details, so we can present it back to them on their user screen
796 update_user_meta( $user->ID, 'wpcom_user_data', $user_data );
797
798 add_filter( 'auth_cookie_expiration', array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
799 wp_set_auth_cookie( $user->ID, true );
800 remove_filter( 'auth_cookie_expiration', array( 'Jetpack_SSO_Helpers', 'extend_auth_cookie_expiration_for_sso' ) );
801
802 /** This filter is documented in core/src/wp-includes/user.php */
803 do_action( 'wp_login', $user->user_login, $user );
804
805 wp_set_current_user( $user->ID );
806
807 $_request_redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( $_REQUEST['redirect_to'] ) : '';
808 $redirect_to = user_can( $user, 'edit_posts' ) ? admin_url() : self::profile_page_url();
809
810 // If we have a saved redirect to request in a cookie
811 if ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
812 // Set that as the requested redirect to
813 $redirect_to = $_request_redirect_to = esc_url_raw( $_COOKIE['jetpack_sso_redirect_to'] );
814 }
815
816 $json_api_auth_environment = Jetpack_SSO_Helpers::get_json_api_auth_environment();
817
818 $is_json_api_auth = ! empty( $json_api_auth_environment );
819 $is_user_connected = Jetpack::is_user_connected( $user->ID );
820 $roles = new Roles();
821 $tracking->record_user_event( 'sso_user_logged_in', array(
822 'user_found_with' => $user_found_with,
823 'user_connected' => (bool) $is_user_connected,
824 'user_role' => $roles->translate_current_user_to_role(),
825 'is_json_api_auth' => (bool) $is_json_api_auth,
826 ) );
827
828 if ( $is_json_api_auth ) {
829 Jetpack::init()->verify_json_api_authorization_request( $json_api_auth_environment );
830 Jetpack::init()->store_json_api_authorization_token( $user->user_login, $user );
831
832 } else if ( ! $is_user_connected ) {
833 wp_safe_redirect(
834 add_query_arg(
835 array(
836 'redirect_to' => $redirect_to,
837 'request_redirect_to' => $_request_redirect_to,
838 'calypso_env' => Jetpack::get_calypso_env(),
839 'jetpack-sso-auth-redirect' => '1',
840 ),
841 admin_url()
842 )
843 );
844 exit;
845 }
846
847 add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
848 wp_safe_redirect(
849 /** This filter is documented in core/src/wp-login.php */
850 apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
851 );
852 exit;
853 }
854
855 add_filter( 'jetpack_sso_default_to_sso_login', '__return_false' );
856
857 $tracking->record_user_event( 'sso_login_failed', array(
858 'error_message' => 'cant_find_user'
859 ) );
860
861 $this->user_data = $user_data;
862
863 $error = new WP_Error( 'account_not_found', __( 'Account not found. If you already have an account, make sure you have connected to WordPress.com.', 'jetpack' ) );
864
865 /** This filter is documented in core/src/wp-includes/pluggable.php */
866 do_action( 'wp_login_failed', $user_data->login, $error );
867 add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'cant_find_user' ) );
868 }
869
870 static function profile_page_url() {
871 return admin_url( 'profile.php' );
872 }
873
874 /**
875 * Builds the "Login to WordPress.com" button that is displayed on the login page as well as user profile page.
876 *
877 * @param array $args An array of arguments to add to the SSO URL.
878 * @param boolean $is_primary Should the button have the `button-primary` class?
879 * @return string Returns the HTML markup for the button.
880 */
881 function build_sso_button( $args = array(), $is_primary = false ) {
882 $url = $this->build_sso_button_url( $args );
883 $classes = $is_primary
884 ? 'jetpack-sso button button-primary'
885 : 'jetpack-sso button';
886
887 return sprintf(
888 '<a rel="nofollow" href="%1$s" class="%2$s"><span>%3$s %4$s</span></a>',
889 esc_url( $url ),
890 $classes,
891 '<span class="genericon genericon-wordpress"></span>',
892 esc_html__( 'Log in with WordPress.com', 'jetpack' )
893 );
894 }
895
896 /**
897 * Builds a URL with `jetpack-sso` action and option args which is used to setup SSO.
898 *
899 * @param array $args An array of arguments to add to the SSO URL.
900 * @return string The URL used for SSO.
901 */
902 function build_sso_button_url( $args = array() ) {
903 $defaults = array(
904 'action' => 'jetpack-sso',
905 );
906
907 $args = wp_parse_args( $args, $defaults );
908
909 if ( ! empty( $_GET['redirect_to'] ) ) {
910 $args['redirect_to'] = urlencode( esc_url_raw( $_GET['redirect_to'] ) );
911 }
912
913 return add_query_arg( $args, wp_login_url() );
914 }
915
916 /**
917 * Retrieves a WordPress.com SSO URL with appropriate query parameters or dies.
918 *
919 * @param boolean $reauth Should the user be forced to reauthenticate on WordPress.com?
920 * @param array $args Optional query parameters.
921 * @return string The WordPress.com SSO URL.
922 */
923 function get_sso_url_or_die( $reauth = false, $args = array() ) {
924 if ( empty( $reauth ) ) {
925 $sso_redirect = $this->build_sso_url( $args );
926 } else {
927 self::clear_wpcom_profile_cookies();
928 $sso_redirect = $this->build_reauth_and_sso_url( $args );
929 }
930
931 // If there was an error retrieving the SSO URL, then error.
932 if ( is_wp_error( $sso_redirect ) ) {
933 $error_message = sanitize_text_field(
934 sprintf( '%s: %s', $sso_redirect->get_error_code(), $sso_redirect->get_error_message() )
935 );
936 $tracking = new Tracking();
937 $tracking->record_user_event( 'sso_login_redirect_failed', array(
938 'error_message' => $error_message
939 ) );
940 wp_die( $error_message );
941 }
942
943 return $sso_redirect;
944 }
945
946 /**
947 * Build WordPress.com SSO URL with appropriate query parameters.
948 *
949 * @param array $args Optional query parameters.
950 * @return string WordPress.com SSO URL
951 */
952 public function build_sso_url( $args = array() ) {
953 $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
954 $defaults = array(
955 'action' => 'jetpack-sso',
956 'site_id' => Jetpack_Options::get_option( 'id' ),
957 'sso_nonce' => $sso_nonce,
958 'calypso_auth' => '1',
959 );
960
961 $args = wp_parse_args( $args, $defaults );
962
963 if ( is_wp_error( $args['sso_nonce'] ) ) {
964 return $args['sso_nonce'];
965 }
966
967 $query = add_query_arg( $args, '' );
968 $query = trim( $query, '?' );
969
970 $url = Redirect::get_url(
971 'wpcom-login',
972 array(
973 'query' => $query,
974 )
975 );
976
977 return $url;
978 }
979
980 /**
981 * Build WordPress.com SSO URL with appropriate query parameters,
982 * including the parameters necessary to force the user to reauthenticate
983 * on WordPress.com.
984 *
985 * @param array $args Optional query parameters.
986 * @return string WordPress.com SSO URL
987 */
988 public function build_reauth_and_sso_url( $args = array() ) {
989 $sso_nonce = ! empty( $args['sso_nonce'] ) ? $args['sso_nonce'] : self::request_initial_nonce();
990 $redirect = $this->build_sso_url( array( 'force_auth' => '1', 'sso_nonce' => $sso_nonce ) );
991
992 if ( is_wp_error( $redirect ) ) {
993 return $redirect;
994 }
995
996 $defaults = array(
997 'action' => 'jetpack-sso',
998 'site_id' => Jetpack_Options::get_option( 'id' ),
999 'sso_nonce' => $sso_nonce,
1000 'reauth' => '1',
1001 'redirect_to' => urlencode( $redirect ),
1002 'calypso_auth' => '1',
1003 );
1004
1005 $args = wp_parse_args( $args, $defaults );
1006
1007 if ( is_wp_error( $args['sso_nonce'] ) ) {
1008 return $args['sso_nonce'];
1009 }
1010
1011 $query = add_query_arg( $args, '' );
1012 $query = trim( $query, '?' );
1013
1014 $url = Redirect::get_url(
1015 'wpcom-login',
1016 array(
1017 'query' => $query,
1018 )
1019 );
1020
1021 return $url;
1022 }
1023
1024 /**
1025 * Determines local user associated with a given WordPress.com user ID.
1026 *
1027 * @since 2.6.0
1028 *
1029 * @param int $wpcom_user_id User ID from WordPress.com
1030 * @return object Local user object if found, null if not.
1031 */
1032 static function get_user_by_wpcom_id( $wpcom_user_id ) {
1033 $user_query = new WP_User_Query( array(
1034 'meta_key' => 'wpcom_user_id',
1035 'meta_value' => intval( $wpcom_user_id ),
1036 'number' => 1,
1037 ) );
1038
1039 $users = $user_query->get_results();
1040 return $users ? array_shift( $users ) : null;
1041 }
1042
1043 /**
1044 * When jetpack-sso-auth-redirect query parameter is set, will redirect user to
1045 * WordPress.com authorization flow.
1046 *
1047 * We redirect here instead of in handle_login() because Jetpack::init()->build_connect_url
1048 * calls menu_page_url() which doesn't work properly until admin menus are registered.
1049 */
1050 function maybe_authorize_user_after_sso() {
1051 if ( empty( $_GET['jetpack-sso-auth-redirect'] ) ) {
1052 return;
1053 }
1054
1055 $redirect_to = ! empty( $_GET['redirect_to'] ) ? esc_url_raw( $_GET['redirect_to'] ) : admin_url();
1056 $request_redirect_to = ! empty( $_GET['request_redirect_to'] ) ? esc_url_raw( $_GET['request_redirect_to'] ) : $redirect_to;
1057
1058 /** This filter is documented in core/src/wp-login.php */
1059 $redirect_after_auth = apply_filters( 'login_redirect', $redirect_to, $request_redirect_to, wp_get_current_user() );
1060
1061 /**
1062 * Since we are passing this redirect to WordPress.com and therefore can not use wp_safe_redirect(),
1063 * let's sanitize it here to make sure it's safe. If the redirect is not safe, then use admin_url().
1064 */
1065 $redirect_after_auth = wp_sanitize_redirect( $redirect_after_auth );
1066 $redirect_after_auth = wp_validate_redirect( $redirect_after_auth, admin_url() );
1067
1068 /**
1069 * Return the raw connect URL with our redirect and attribute connection to SSO.
1070 */
1071 $connect_url = Jetpack::init()->build_connect_url( true, $redirect_after_auth, 'sso' );
1072
1073 add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
1074 wp_safe_redirect( $connect_url );
1075 exit;
1076 }
1077
1078 /**
1079 * Cache user's display name and Gravatar so it can be displayed on the login screen. These cookies are
1080 * stored when the user logs out, and then deleted when the user logs in.
1081 */
1082 function store_wpcom_profile_cookies_on_logout() {
1083 if ( ! Jetpack::is_user_connected( get_current_user_id() ) ) {
1084 return;
1085 }
1086
1087 $user_data = $this->get_user_data( get_current_user_id() );
1088 if ( ! $user_data ) {
1089 return;
1090 }
1091
1092 setcookie(
1093 'jetpack_sso_wpcom_name_' . COOKIEHASH,
1094 $user_data->display_name,
1095 time() + WEEK_IN_SECONDS,
1096 COOKIEPATH,
1097 COOKIE_DOMAIN,
1098 is_ssl()
1099 );
1100
1101 setcookie(
1102 'jetpack_sso_wpcom_gravatar_' . COOKIEHASH,
1103 get_avatar_url(
1104 $user_data->email,
1105 array( 'size' => 144, 'default' => 'mystery' )
1106 ),
1107 time() + WEEK_IN_SECONDS,
1108 COOKIEPATH,
1109 COOKIE_DOMAIN,
1110 is_ssl()
1111 );
1112 }
1113
1114 /**
1115 * Determines if a local user is connected to WordPress.com
1116 *
1117 * @since 2.8
1118 * @param integer $user_id - Local user id
1119 * @return boolean
1120 **/
1121 public function is_user_connected( $user_id ) {
1122 return $this->get_user_data( $user_id );
1123 }
1124
1125 /**
1126 * Retrieves a user's WordPress.com data
1127 *
1128 * @since 2.8
1129 * @param integer $user_id - Local user id
1130 * @return mixed null or stdClass
1131 **/
1132 public function get_user_data( $user_id ) {
1133 return get_user_meta( $user_id, 'wpcom_user_data', true );
1134 }
1135
1136 /**
1137 * Mark SSO as discovered when an SSO JITM is viewed.
1138 *
1139 * @since 6.9.0
1140 *
1141 * @param array $envelopes Array of JITM messages received after API call.
1142 * @param string $message_path The message path to ask for.
1143 *
1144 * @return array $envelopes New array of JITM messages. May now contain only one message, about SSO.
1145 */
1146 public function inject_sso_jitm( $envelopes, $message_path = null) {
1147 /*
1148 * Bail early if:
1149 * - the request does not originate from wp-admin main dashboard.
1150 * - that's not the first time the user uses SSO.
1151 */
1152 if (
1153 'wp:dashboard:admin_notices' !== $message_path
1154 || true !== Jetpack_Options::get_option( 'sso_first_login' )
1155 ) {
1156 return $envelopes;
1157 }
1158
1159 // Update our option to mark that SSO was discovered.
1160 Jetpack_Options::update_option( 'sso_first_login', false );
1161
1162 return $this->prepare_sso_first_login_jitm();
1163 }
1164
1165 /**
1166 * Prepare JITM array for new SSO users
1167 *
1168 * @since 6.9.0
1169 *
1170 * @return array $sso_first_login_jitm array containting one object of information about our message.
1171 */
1172 private function prepare_sso_first_login_jitm() {
1173 // Build our custom SSO JITM.
1174 $discover_sso_message = array(
1175 'content' => array(
1176 'message' => esc_html__( "You've successfully signed in with WordPress.com Secure Sign On!", 'jetpack' ),
1177 'icon' => 'jetpack',
1178 'list' => array(),
1179 'description' => esc_html__( 'Interested in learning more about how Secure Sign On keeps your site safer?', 'jetpack' ),
1180 'classes' => '',
1181 ),
1182 'CTA' => array(
1183 'message' => esc_html__( 'Learn More', 'jetpack' ),
1184 'hook' => '',
1185 'newWindow' => true,
1186 'primary' => true,
1187 ),
1188 'template' => 'default',
1189 'ttl' => 300,
1190 'id' => 'sso_discover',
1191 'feature_class' => 'sso',
1192 'expires' => 3628800,
1193 'max_dismissal' => 1,
1194 'activate_module' => null,
1195 );
1196
1197 return array( json_decode( json_encode( $discover_sso_message ) ) );
1198 }
1199 }
1200
1201 Jetpack_SSO::get_instance();
1202