sso.php 33 KB

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