Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SSO: Add integration with JSON API authorization #6194

Merged
merged 14 commits into from
Jan 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions class.jetpack.php
Original file line number Diff line number Diff line change
Expand Up @@ -5254,10 +5254,22 @@ function add_token_to_login_redirect_json_api_authorization( $redirect_to, $orig
);
}

// Verifies the request by checking the signature
function verify_json_api_authorization_request() {

/**
* Verifies the request by checking the signature
*
* @since 4.6.0 Method was updated to use `$_REQUEST` instead of `$_GET` and `$_POST`. Method also updated to allow
* passing in an `$environment` argument that overrides `$_REQUEST`. This was useful for integrating with SSO.
*
* @param null|array $environment
*/
function verify_json_api_authorization_request( $environment = null ) {
require_once JETPACK__PLUGIN_DIR . 'class.jetpack-signature.php';

$environment = is_null( $environment )
? $_REQUEST
: $environment;

$token = Jetpack_Data::get_access_token( JETPACK_MASTER_USER );
if ( ! $token || empty( $token->secret ) ) {
wp_die( __( 'You must connect your Jetpack plugin to WordPress.com to use this feature.' , 'jetpack' ) );
Expand All @@ -5267,8 +5279,17 @@ function verify_json_api_authorization_request() {

$jetpack_signature = new Jetpack_Signature( $token->secret, (int) Jetpack_Options::get_option( 'time_diff' ) );

if ( isset( $_POST['jetpack_json_api_original_query'] ) ) {
$signature = $jetpack_signature->sign_request( $_GET['token'], $_GET['timestamp'], $_GET['nonce'], '', 'GET', $_POST['jetpack_json_api_original_query'], null, true );
if ( isset( $environment['jetpack_json_api_original_query'] ) ) {
$signature = $jetpack_signature->sign_request(
$environment['token'],
$environment['timestamp'],
$environment['nonce'],
'',
'GET',
$environment['jetpack_json_api_original_query'],
null,
true
);
} else {
$signature = $jetpack_signature->sign_current_request( array( 'body' => null, 'method' => 'GET' ) );
}
Expand All @@ -5277,20 +5298,20 @@ function verify_json_api_authorization_request() {
wp_die( $die_error );
} else if ( is_wp_error( $signature ) ) {
wp_die( $die_error );
} else if ( ! hash_equals( $signature, $_GET['signature'] ) ) {
} else if ( ! hash_equals( $signature, $environment['signature'] ) ) {
if ( is_ssl() ) {
// If we signed an HTTP request on the Jetpack Servers, but got redirected to HTTPS by the local blog, check the HTTP signature as well
$signature = $jetpack_signature->sign_current_request( array( 'scheme' => 'http', 'body' => null, 'method' => 'GET' ) );
if ( ! $signature || is_wp_error( $signature ) || ! hash_equals( $signature, $_GET['signature'] ) ) {
if ( ! $signature || is_wp_error( $signature ) || ! hash_equals( $signature, $environment['signature'] ) ) {
wp_die( $die_error );
}
} else {
wp_die( $die_error );
}
}

$timestamp = (int) $_GET['timestamp'];
$nonce = stripslashes( (string) $_GET['nonce'] );
$timestamp = (int) $environment['timestamp'];
$nonce = stripslashes( (string) $environment['nonce'] );

if ( ! $this->add_nonce( $timestamp, $nonce ) ) {
// De-nonce the nonce, at least for 5 minutes.
Expand All @@ -5301,7 +5322,7 @@ function verify_json_api_authorization_request() {
}
}

$data = json_decode( base64_decode( stripslashes( $_GET['data'] ) ) );
$data = json_decode( base64_decode( stripslashes( $environment['data'] ) ) );
$data_filters = array(
'state' => 'opaque',
'client_id' => 'int',
Expand Down
90 changes: 50 additions & 40 deletions modules/sso.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public function xmlrpc_user_disconnect( $user_id ) {
public function login_enqueue_scripts() {
global $action;

if ( ! in_array( $action, array( 'jetpack-sso', 'login' ) ) ) {
if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
return;
}

Expand All @@ -154,7 +154,7 @@ public function login_enqueue_scripts() {
public function login_body_class( $classes ) {
global $action;

if ( ! in_array( $action, array( 'jetpack-sso', 'login' ) ) ) {
if ( ! Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
return $classes;
}

Expand Down Expand Up @@ -325,7 +325,7 @@ private function wants_to_login() {
// And now the exceptions
$action = isset( $_GET['loggedout'] ) ? 'loggedout' : $action;

if ( 'login' == $action ) {
if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {
$wants_to_login = true;
}

Expand Down Expand Up @@ -358,44 +358,14 @@ function login_init() {
}
}

/**
* If the user is attempting to logout AND the auto-forward to WordPress.com
* login is set then we need to ensure we do not auto-forward the user and get
* them stuck in an infinite logout loop.
*/
if ( isset( $_GET['loggedout'] ) && Jetpack_SSO_Helpers::bypass_login_forward_wpcom() ) {
add_filter( 'jetpack_remove_login_form', '__return_true' );
}

/**
* Check to see if the site admin wants to automagically forward the user
* to the WordPress.com login page AND that the request to wp-login.php
* is not something other than login (Like logout!)
*/
if (
$this->wants_to_login()
&& Jetpack_SSO_Helpers::bypass_login_forward_wpcom()
) {
add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
$this->maybe_save_cookie_redirect();
$reauth = ! empty( $_GET['force_reauth'] );
$sso_url = $this->get_sso_url_or_die( $reauth );
JetpackTracking::record_user_event( 'sso_login_redirect_bypass_success' );
wp_safe_redirect( $sso_url );
exit;
}

if ( 'login' === $action ) {
$this->display_sso_login_form();
} elseif ( 'jetpack-sso' === $action ) {
if ( 'jetpack-sso' === $action ) {
if ( isset( $_GET['result'], $_GET['user_id'], $_GET['sso_nonce'] ) && 'success' == $_GET['result'] ) {
$this->handle_login();
$this->display_sso_login_form();
} else {
if ( Jetpack::is_staging_site() ) {
add_filter( 'login_message', array( 'Jetpack_SSO_Notices', 'sso_not_allowed_in_staging' ) );
} else {
$this->maybe_save_cookie_redirect();
// Is it wiser to just use wp_redirect than do this runaround to wp_safe_redirect?
add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
$reauth = ! empty( $_GET['force_reauth'] );
Expand All @@ -405,6 +375,26 @@ function login_init() {
exit;
}
}
} else if ( Jetpack_SSO_Helpers::display_sso_form_for_action( $action ) ) {

// Save cookies so we can handle redirects after SSO
$this->save_cookies();

/**
* Check to see if the site admin wants to automagically forward the user
* to the WordPress.com login page AND that the request to wp-login.php
* is not something other than login (Like logout!)
*/
if ( Jetpack_SSO_Helpers::bypass_login_forward_wpcom() && $this->wants_to_login() ) {
add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
$reauth = ! empty( $_GET['force_reauth'] );
$sso_url = $this->get_sso_url_or_die( $reauth );
JetpackTracking::record_user_event( 'sso_login_redirect_bypass_success' );
wp_safe_redirect( $sso_url );
exit;
}

$this->display_sso_login_form();
}
}

Expand Down Expand Up @@ -432,17 +422,28 @@ public function display_sso_login_form() {

/**
* Conditionally save the redirect_to url as a cookie.
*
* @since 4.6.0 Renamed to save_cookies from maybe_save_redirect_cookies
*/
public static function maybe_save_cookie_redirect() {
public static function save_cookies() {
if ( headers_sent() ) {
return new WP_Error( 'headers_sent', __( 'Cannot deal with cookie redirects, as headers are already sent.', 'jetpack' ) );
}

setcookie(
'jetpack_sso_original_request',
esc_url_raw( set_url_scheme( $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'] ) ),
time() + HOUR_IN_SECONDS,
COOKIEPATH,
COOKIE_DOMAIN,
false,
true
);

if ( ! empty( $_GET['redirect_to'] ) ) {
// If we have something to redirect to
$url = esc_url_raw( $_GET['redirect_to'] );
setcookie( 'jetpack_sso_redirect_to', $url, time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, true );

} elseif ( ! empty( $_COOKIE['jetpack_sso_redirect_to'] ) ) {
// Otherwise, if it's already set, purge it.
setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
Expand Down Expand Up @@ -730,14 +731,22 @@ function handle_login() {
setcookie( 'jetpack_sso_redirect_to', ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
}

$json_api_auth_environment = Jetpack_SSO_Helpers::get_json_api_auth_environment();

$is_json_api_auth = ! empty( $json_api_auth_environment );
$is_user_connected = Jetpack::is_user_connected( $user->ID );
JetpackTracking::record_user_event( 'sso_user_logged_in', array(
'user_found_with' => $user_found_with,
'user_connected' => (bool) $is_user_connected,
'user_role' => Jetpack::translate_current_user_to_role()
'user_found_with' => $user_found_with,
'user_connected' => (bool) $is_user_connected,
'user_role' => Jetpack::translate_current_user_to_role(),
'is_json_api_auth' => (bool) $is_json_api_auth,
) );

if ( ! $is_user_connected ) {
if ( $is_json_api_auth ) {
Jetpack::init()->verify_json_api_authorization_request( $json_api_auth_environment );
Jetpack::init()->store_json_api_authorization_token( $user->user_login, $user );

} else if ( ! $is_user_connected ) {
$calypso_env = ! empty( $_GET['calypso_env'] )
? sanitize_key( $_GET['calypso_env'] )
: '';
Expand All @@ -756,6 +765,7 @@ function handle_login() {
exit;
}

add_filter( 'allowed_redirect_hosts', array( 'Jetpack_SSO_Helpers', 'allowed_redirect_hosts' ) );
wp_safe_redirect(
/** This filter is documented in core/src/wp-login.php */
apply_filters( 'login_redirect', $redirect_to, $_request_redirect_to, $user )
Expand Down
66 changes: 66 additions & 0 deletions modules/sso/class.jetpack-sso-helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ static function is_match_by_email_checkbox_disabled() {
* default for $api_base due to restrictions with testing constants in our tests.
*
* @since 4.3.0
* @since 4.6.0 Added public-api.wordpress.com as an allowed redirect
*
* @param array $hosts
* @param string $api_base
Expand All @@ -188,6 +189,7 @@ static function allowed_redirect_hosts( $hosts, $api_base = JETPACK__API_BASE )

$hosts[] = 'wordpress.com';
$hosts[] = 'jetpack.wordpress.com';
$hosts[] = 'public-api.wordpress.com';

if (
( Jetpack::is_development_mode() || Jetpack::is_development_version() ) &&
Expand Down Expand Up @@ -258,6 +260,70 @@ static function extend_auth_cookie_expiration_for_sso() {
*/
return intval( apply_filters( 'jetpack_sso_auth_cookie_expirtation', YEAR_IN_SECONDS ) );
}

/**
* Determines if the SSO form should be displayed for the current action.
*
* @since 4.6.0
*
* @param string $action
*
* @return bool Is SSO allowed for the current action?
*/
static function display_sso_form_for_action( $action ) {
/**
* Allows plugins the ability to overwrite actions where the SSO form is allowed to be used.
*
* @module sso
*
* @since 4.6.0
*
* @param array $allowed_actions_for_sso
*/
$allowed_actions_for_sso = (array) apply_filters( 'jetpack_sso_allowed_actions', array(
'login',
'jetpack-sso',
'jetpack_json_api_authorization',
) );
return in_array( $action, $allowed_actions_for_sso );
}

/**
* This method returns an environment array that is meant to simulate `$_REQUEST` when the initial
* JSON API auth request was made.
*
* @since 4.6.0
*
* @return array|bool
*/
static function get_json_api_auth_environment() {
if ( empty( $_COOKIE['jetpack_sso_original_request'] ) ) {
return false;
}

$original_request = esc_url_raw( $_COOKIE['jetpack_sso_original_request'] );

$parsed_url = wp_parse_url( $original_request );
if ( empty( $parsed_url ) || empty( $parsed_url['query'] ) ) {
return false;
}

$args = array();
wp_parse_str( $parsed_url['query'], $args );

if ( empty( $args ) || empty( $args['action'] ) ) {
return false;
}

if ( 'jetpack_json_api_authorization' != $args['action'] ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we have a stricter comparison here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do !==, but I'm not sure it's necessary here since we're comparing strings.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we're comparing strings.

sure sure?

return false;
}

return array_merge(
$args,
array( 'jetpack_json_api_original_query' => $original_request )
);
}
}

endif;
Loading