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

Add OAuth authentication #47

Merged
merged 46 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
dd0c828
Initial oAuth connect implementation.
iamdharmesh Aug 5, 2024
0c98dbb
UI updates and added loader on connect page.
iamdharmesh Aug 6, 2024
b69e650
Add verify access token logic.
iamdharmesh Aug 6, 2024
33cb1e4
Add encryption class.
iamdharmesh Aug 6, 2024
cec3d88
Show modal to users for blocked-popup.
iamdharmesh Aug 6, 2024
4124d36
Update API class to use access token if it is available.
iamdharmesh Aug 6, 2024
c9cf283
Fix spacing issue.
iamdharmesh Aug 6, 2024
416face
Remove existing login form.
iamdharmesh Aug 6, 2024
796e630
Fix js lint error.
iamdharmesh Aug 6, 2024
5daf4f3
Remove use of string template from js.
iamdharmesh Aug 7, 2024
54d1725
Display error message in case of lists API fail.
iamdharmesh Aug 7, 2024
896076b
Added admin notice for the invalid/revoked token.
iamdharmesh Aug 7, 2024
3f893f2
Display spinner on try-again popup.
iamdharmesh Aug 7, 2024
c520dd3
Increase request timeout to 10 seconds.
iamdharmesh Aug 7, 2024
23112f9
Apply suggestions from code review
iamdharmesh Aug 7, 2024
f4ae7f4
Addressed PR feedback.
iamdharmesh Aug 7, 2024
4073bbb
Add note in readme for the encryption constants.
iamdharmesh Aug 7, 2024
e7bc6f1
readme.txt updates.
iamdharmesh Aug 7, 2024
19e72d1
Wording updates on connect page.
iamdharmesh Aug 7, 2024
1198ebb
Show notice for re-connect incase of token decryption fail.
iamdharmesh Aug 7, 2024
c2c9189
Readme updates
dkotter Aug 7, 2024
ed0edf0
Minor formatting cleanup
dkotter Aug 7, 2024
3afeb88
Use oauth url from the server side.
iamdharmesh Aug 8, 2024
b433be3
Merge branch 'enhancement/9' of github.com:mailchimp/wordpress into e…
iamdharmesh Aug 8, 2024
01deb4b
Upgrade "@10up/cypress-wp-utils" to 0.4.0
iamdharmesh Aug 8, 2024
3aed9d2
Upgrade cypress to 13.13.2
iamdharmesh Aug 8, 2024
1b01593
Added admin tests.
iamdharmesh Aug 8, 2024
74fae4c
Added connect to mailchimp test.
iamdharmesh Aug 8, 2024
d61ab47
Add settings, shortcode and block tests.
iamdharmesh Aug 8, 2024
ee0884e
Added tests for remove CSS and custom styling.
iamdharmesh Aug 8, 2024
5bfe73e
Added some more settings tests.
iamdharmesh Aug 8, 2024
a63ad60
Add logout tests.
iamdharmesh Aug 8, 2024
91f09c2
Updated E2E workflow file.
iamdharmesh Aug 8, 2024
6e7dc08
Readme updates.
iamdharmesh Aug 8, 2024
a1c8102
Trigger E2E tests.
iamdharmesh Aug 8, 2024
554fa37
Add block name in insertBlock command.
iamdharmesh Aug 8, 2024
57c0058
Addressed improvements feedback.
iamdharmesh Aug 9, 2024
ab923bd
Merge branch 'enhancement/9' of github.com:mailchimp/wordpress into e…
iamdharmesh Aug 9, 2024
c633d39
Some improvements in settings tests.
iamdharmesh Aug 9, 2024
1caefec
Fix shortcode form create issue.
iamdharmesh Aug 9, 2024
86e9489
Add retry in run mode.
iamdharmesh Aug 9, 2024
3921ba6
Update E2E workflow to use zip built by generate zip action.
iamdharmesh Aug 9, 2024
4fe1d6e
Try fix connect tests in trunk env.
iamdharmesh Aug 9, 2024
8cb0929
Merge pull request #48 from mailchimp/enhancement/e2e-tests
dkotter Aug 12, 2024
4175774
Add admin notice for the API key deprecation.
iamdharmesh Aug 13, 2024
9de40f0
Update since statements
dkotter Aug 13, 2024
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
12 changes: 12 additions & 0 deletions css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,15 @@ th.mailchimp-connect {
#mc-message {
margin-top: 26px;
}

/**
* Mailchimp OAuth CSS
*/
.mailchimp-sf-oauth-section .oauth-error {
display: block;
color: #db3a1b;
}

.mailchimp-sf-oauth-connect-wrapper {
display: flex;
}
213 changes: 213 additions & 0 deletions includes/class-mailchimp-admin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
<?php
/**
* Class responsible for Admin side functionalities.
*
* @package Mailchimp
*/

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Class MailChimp_Admin
*
* @since x.x.x
*/
class MailChimp_Admin {
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved

/**
* The OAuth base endpoint
*
* @since x.x.x
* @var string
*/
private $oauth_url = 'https://woocommerce.mailchimpapp.com';

/**
* Initialize the class
*/
public function init() {
add_action( 'admin_notices', array( $this, 'admin_notices' ) );
add_action( 'wp_ajax_mailchimp_sf_oauth_start', array( $this, 'start_oauth_process' ) );
add_action( 'wp_ajax_mailchimp_sf_oauth_finish', array( $this, 'finish_oauth_process' ) );
}


/**
* Start the OAuth process.
* This function is called via AJAX.
* It start the OAuth process by the calling the oAuth middleware server and responding the response to the front-end.
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
*/
public function start_oauth_process() {
// Validate the nonce and permissions.
if (
! current_user_can( 'manage_options' ) ||
! isset( $_POST['nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'mailchimp_sf_oauth_start_nonce' )
) {
wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to perform this action.', 'mailchimp' ) ) );
}

// Generate a secret and send it to the OAuth server.
$secret = uniqid( 'mailchimp_sf_' );
$args = array(
'domain' => site_url(),
'secret' => $secret,
);

$options = array(
'headers' => array(
'Content-type' => 'application/json',
),
'body' => wp_json_encode( $args ),
);

$response = wp_remote_post( $this->oauth_url . '/api/start', $options );

// Check for errors.
if ( $response instanceof WP_Error ) {
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
}

// Send the response to the front-end.
if ( 201 === $response['response']['code'] && ! empty( $response['body'] ) ) {
set_site_transient( 'mailchimp_sf_oauth_secret', $secret, 60 * 60 );
$result = json_decode( $response['body'], true );
wp_send_json_success( $result );
} else {
if ( ! empty( $response['response'] ) ) {
$response = $response['response'];
}
wp_send_json_error( $response );
}
}

/**
* Finish the OAuth process.
* This function is called via AJAX.
* This function finishes the OAuth process by the sending temporary token back to the oAuth server.
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
*/
public function finish_oauth_process() {
// Validate the nonce and permissions.
if (
! current_user_can( 'manage_options' ) ||
! isset( $_POST['nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'mailchimp_sf_oauth_finish_nonce' )
) {
wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to perform this action.', 'mailchimp' ) ) );
}

$token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
$args = array(
'domain' => site_url(),
'secret' => get_site_transient( 'mailchimp_sf_oauth_secret' ),
'token' => $token,
);

$options = array(
'headers' => array(
'Content-type' => 'application/json',
),
'body' => wp_json_encode( $args ),
);
$response = wp_remote_post( $this->oauth_url . '/api/finish', $options );

// Check for errors.
if ( $response instanceof WP_Error ) {
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
}

if ( 200 === $response['response']['code'] ) {
// Save the access token and data center.
$result = json_decode( $response['body'], true );
if ( $result && ! empty( $result['access_token'] ) && ! empty( $result['data_center'] ) ) {
// Clean up the old data.
delete_option( 'mailchimp_sf_access_token' );
delete_option( 'mailchimp_sf_auth_error' );
delete_option( 'mc_datacenter' );
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved

delete_site_transient( 'mailchimp_sf_oauth_secret' );

// Verify the token.
$verify = $this->verify_and_save_oauth_token( $result['access_token'], $result['data_center'] );

if ( is_wp_error( $verify ) ) {
// If there is an error, send it back to the front-end.
wp_send_json_error( array( 'message' => $verify->get_error_message() ) );
}

wp_send_json_success( true );
} else {
wp_send_json_error( array( 'message' => esc_html__( 'Invalid response from the server.', 'mailchimp' ) ) );
}
} else {
wp_send_json_error( $response );
}
}

/**
* Verify and save the OAuth token.
*
* @param string $access_token The token to verify.
* @param string $data_center The data center to verify.
* @return mixed
*/
public function verify_and_save_oauth_token( $access_token, $data_center ) {
try {
$api = new MailChimp_API( $access_token, $data_center );
} catch ( Exception $e ) {
$msg = $e->getMessage();
return new WP_Error( 'mailchimp-sf-invalid-token', $msg );
}

$user = $api->get( '' );
if ( is_wp_error( $user ) ) {
return $user;
}

// Might as well set this data if we have it already.
$valid_roles = array( 'owner', 'admin', 'manager' );
if ( isset( $user['role'] ) && in_array( $user['role'], $valid_roles, true ) ) {
$data_encryption = new MailChimp_Data_Encryption();
$access_token = $data_encryption->encrypt( $access_token );

update_option( 'mailchimp_sf_access_token', $access_token );
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
update_option( 'mc_datacenter', $data_center );
update_option( 'mc_user', $user );
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
return true;

} else {
$msg = esc_html__( 'API Key must belong to "Owner", "Admin", or "Manager."', 'mailchimp' );
return new WP_Error( 'mailchimp-sf-invalid-role', $msg );
}
}

/**
* Display admin notices.
*
* @since x.x.x
*/
public function admin_notices() {
// display a notice if the access token is invalid/revoked.
if ( get_option( 'mailchimp_sf_auth_error', false ) && current_user_can( 'manage_options' ) && get_option( 'mailchimp_sf_access_token', '' ) ) {
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
?>
<div class="notice notice-warning is-dismissible">
<p>
<?php
$message = sprintf(
/* translators: Placeholders: %1$s - <a> tag, %2$s - </a> tag */
__( 'Heads up! There may be a problem with your connection to Mailchimp. Please %1$sre-connect%2$s your Mailchimp account to fix the issue.', 'mailchimp' ),
'<a href="' . esc_url( admin_url( 'admin.php?page=mailchimp_sf_options' ) ) . '">',
'</a>'
);

echo wp_kses( $message, array( 'a' => array( 'href' => array() ) ) );
?>
</p>
</div>
<?php
}
}
}
147 changes: 147 additions & 0 deletions includes/class-mailchimp-data-encryption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<?php
/**
* Class responsible for encrypting and decrypting data.
*
* @package Mailchimp
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
*/

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Class MailChimp_Data_Encryption
*
* @since x.x.x
*/
class MailChimp_Data_Encryption {

/**
* Key to use for encryption.
*
* @since x.x.x
* @var string
*/
private $key;

/**
* Salt to use for encryption.
*
* @since x.x.x
* @var string
*/
private $salt;

/**
* Constructor.
*
* @since x.x.x
*/
public function __construct() {
$this->key = $this->get_default_key();
$this->salt = $this->get_default_salt();
}

/**
* Encrypts a value.
*
* If a user-based key is set, that key is used. Otherwise the default key is used.
*
* @since x.x.x
*
* @param string $value Value to encrypt.
* @return string|bool Encrypted value, or false on failure.
*/
public function encrypt( $value ) {
if ( ! extension_loaded( 'openssl' ) ) {
return $value;
}

$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length( $method );
$iv = openssl_random_pseudo_bytes( $ivlen );

$raw_value = openssl_encrypt( $value . $this->salt, $method, $this->key, 0, $iv );
if ( ! $raw_value ) {
return false;
}

return base64_encode( $iv . $raw_value ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}

/**
* Decrypts a value.
*
* If a user-based key is set, that key is used. Otherwise the default key is used.
*
* @since x.x.x
*
* @param string $raw_value Value to decrypt.
* @return string|bool Decrypted value, or false on failure.
*/
public function decrypt( $raw_value ) {
if ( ! extension_loaded( 'openssl' ) || ! is_string( $raw_value ) ) {
return $raw_value;
}

$decoded_value = base64_decode( $raw_value, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode

if ( false === $decoded_value ) {
return $raw_value;
}

$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length( $method );
$iv = substr( $decoded_value, 0, $ivlen );

$decoded_value = substr( $decoded_value, $ivlen );

$value = openssl_decrypt( $decoded_value, $method, $this->key, 0, $iv );
if ( ! $value || substr( $value, - strlen( $this->salt ) ) !== $this->salt ) {
return false;
}

return substr( $value, 0, - strlen( $this->salt ) );
}

/**
* Gets the default encryption key to use.
*
* @since x.x.x
*
* @return string Default (not user-based) encryption key.
*/
private function get_default_key() {
if ( defined( 'MAILCHIMP_SF_ENCRYPTION_KEY' ) && '' !== MAILCHIMP_SF_ENCRYPTION_KEY ) {
return MAILCHIMP_SF_ENCRYPTION_KEY;
}

if ( defined( 'LOGGED_IN_KEY' ) && '' !== LOGGED_IN_KEY ) {
return LOGGED_IN_KEY;
}

// If this is reached, you're either not on a live site or have a serious security issue.
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
return 'vJgwa_qf0u(k!uir[rB);g;DciNAKuX;+q&`A+z&m6kX3Y|$q.U3:Q>!$)6CA+=O';
}

/**
* Gets the default encryption salt to use.
*
* @since x.x.x
*
* @return string Encryption salt.
*/
private function get_default_salt() {
if ( defined( 'MAILCHIMP_SF_ENCRYPTION_SALT' ) && '' !== MAILCHIMP_SF_ENCRYPTION_SALT ) {
return MAILCHIMP_SF_ENCRYPTION_SALT;
}

if ( defined( 'LOGGED_IN_SALT' ) && '' !== LOGGED_IN_SALT ) {
return LOGGED_IN_SALT;
}

// If this is reached, you're either not on a live site or have a serious security issue.
iamdharmesh marked this conversation as resolved.
Show resolved Hide resolved
return '|qhC}/w6q+$V`H>wM:AtNpg/{s)g<k{F:WMcvJJD[K6c_Kb1OEy^Yx7f|$Ovm+X|';
}
}
Loading
Loading