PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 9.7.2
Jetpack – WP Security, Backup, Speed, & Growth v9.7.2
16.2-beta 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 All 501 releases
jetpack / modules / sso.php

sso.php in Jetpack – WP Security, Backup, Speed, & Growth 9.7.2, at modules/sso.php

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