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 24 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
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> Add a Mailchimp signup form widget to your WordPress site.

[![Support Level](https://img.shields.io/badge/support-active-green.svg?label=Support)](#support-level) [![GPL-2.0-or-later License](https://img.shields.io/github/license/mailchimp/wordpress?label=License)](https://github.com/mailchimp/wordpress/blob/develop/LICENSE.md) ![WordPress Plugin Version](https://img.shields.io/wordpress/plugin/v/mailchimp?label=Version) ![WordPress Minimum](https://img.shields.io/wordpress/plugin/wp-version/mailchimp?label=WordPress%20minimum) ![PHP Minimum](https://img.shields.io/wordpress/plugin/required-php/mailchimp?label=PHP%20minimum) ![WordPress Tested Up To](https://img.shields.io/wordpress/plugin/tested/mailchimp?label=WordPress) [![E2E Cypress Tests](https://github.com/mailchimp/wordpress/actions/workflows/e2e.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/e2e.yml) [![PHP Compatibility](https://github.com/mailchimp/wordpress/actions/workflows/php-compat.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/php-compat.yml) [![PHP Linting](https://github.com/mailchimp/wordpress/actions/workflows/phpcs.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/phpcs.yml) [![JS Linting](https://github.com/mailchimp/wordpress/actions/workflows/eslint.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/eslint.yml)
[![Support Level](https://img.shields.io/badge/support-active-green.svg?label=Support)](#support-level) [![GPL-2.0-or-later License](https://img.shields.io/github/license/mailchimp/wordpress?label=License)](https://github.com/mailchimp/wordpress/blob/develop/LICENSE.md) ![WordPress Plugin Version](https://img.shields.io/wordpress/plugin/v/mailchimp?label=Version) ![WordPress Minimum](https://img.shields.io/wordpress/plugin/wp-version/mailchimp?label=WordPress%20minimum) ![PHP Minimum](https://img.shields.io/wordpress/plugin/required-php/mailchimp?label=PHP%20minimum) ![WordPress Tested Up To](https://img.shields.io/wordpress/plugin/tested/mailchimp?label=WordPress) [![E2E Cypress Tests](https://github.com/mailchimp/wordpress/actions/workflows/e2e.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/e2e.yml) [![PHP Compatibility](https://github.com/mailchimp/wordpress/actions/workflows/php-compat.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/php-compat.yml) [![PHP Linting](https://github.com/mailchimp/wordpress/actions/workflows/phpcs.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/phpcs.yml) [![JS Linting](https://github.com/mailchimp/wordpress/actions/workflows/eslint.yml/badge.svg)](https://github.com/mailchimp/wordpress/actions/workflows/eslint.yml)

## Overview

Expand All @@ -18,6 +18,21 @@ WordPress.com compatibility is limited to Business tier users only. [How to add

![Configuring extra fields on your Signup Form (optional)](https://github.com/mailchimp/wordpress/blob/develop/.wordpress-org/screenshot-4.jpg?raw=true)

## Access Token Encryption

Starting in version 1.6.0, authentication has changed to use OAuth. As part of this process, we retrieve an access token that can be used to make API requests. To provide a high-level of security, this access token is encrypted before being stored in the WordPress database. In order to ensure this access token can be decrypted when used, the plugin relies on certain security constants that should remain unchanged.

With no additional configuration, we use the standard `LOGGED_IN_KEY` and `LOGGED_IN_SALT` constants that are normally set in your site's `wp-config.php` file. Some sites make use of security plugins that rotate these constants on a periodic basis. When this happens, we won't be able to decrypt the access token and you’ll need to reconnect your Mailchimp account to generate a new access token.

To prevent such issues, it is recommended to define two additional constants in your site's `wp-config.php` file: `MAILCHIMP_SF_ENCRYPTION_KEY` and `MAILCHIMP_SF_ENCRYPTION_SALT`. These constants should consist of a combination of characters, preferably at least 32 characters long. Once set, these values should not be changed. For strong values, you can copy some of the values from [here](https://api.wordpress.org/secret-key/1.1/salt/) and use them. You'll end up with additional code like the following in your `wp-config.php` file:

```php
define( 'MAILCHIMP_SF_ENCRYPTION_KEY', 'put your unique phrase here' );
define( 'MAILCHIMP_SF_ENCRYPTION_SALT', 'put your unique phrase here' );
```

If these constants are added after you've already authenticated with Mailchimp, you will need to reconnect your account. To avoid this, you can copy the values from `LOGGED_IN_KEY` and `LOGGED_IN_SALT` (if they exist) to `MAILCHIMP_SF_ENCRYPTION_KEY` and `MAILCHIMP_SF_ENCRYPTION_SALT` respectively.

## Frequently Asked Questions

### Can I have multiple forms on one page?
Expand All @@ -41,9 +56,9 @@ This section describes how to install the plugin and get started using it.

### Advanced

If you have a custom coded sidebar or bells and whistles that prevent enabling widgets through the WordPress GUI, complete these steps instead.
If you have a custom coded sidebar or bells and whistles that prevent enabling widgets through the WordPress GUI, complete these steps instead.

WordPress v2.8 or higher:
WordPress v2.8 or higher:
` [mailchimpsf_form] `

If you are adding it inside a php code block, pop this in:
Expand Down
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;
}
273 changes: 273 additions & 0 deletions includes/class-mailchimp-admin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?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 {

/**
* 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' ) );

add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_page_scripts' ) );
}

/**
* Start the OAuth process.
*
* This function is called via AJAX.
*
* It starts the OAuth process by the calling the OAuth middleware
* server and sending the response to the front-end.
*/
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
* a temporary token back to the OAuth server.
*/
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'] ) ) {
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();

// Clean up the old data.
delete_option( 'mailchimp_sf_access_token' );
delete_option( 'mailchimp_sf_auth_error' );
delete_option( 'mc_datacenter' );

update_option( 'mailchimp_sf_access_token', $data_encryption->encrypt( $access_token ) );
update_option( 'mc_datacenter', sanitize_text_field( $data_center ) );
update_option( 'mc_user', $this->sanitize_data( $user ) );
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() {
if (
! get_option( 'mailchimp_sf_auth_error', false ) ||
! current_user_can( 'manage_options' ) ||
! get_option( 'mailchimp_sf_access_token', '' )
) {
return;
}

// Display a notice if the access token is invalid/revoked.
?>
<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
}

/**
* Sanitize variables using sanitize_text_field.
*
* Arrays are sanitized recursively, non-scalar values are ignored.
*
* @param string|array $data Data to sanitize.
* @return string|array
*/
public function sanitize_data( $data ) {
if ( is_array( $data ) ) {
return array_map( array( $this, 'sanitize_data' ), $data );
} else {
return is_scalar( $data ) ? sanitize_text_field( $data ) : $data;
}
}

/**
* Enqueue scripts/styles for the Mailchimp admin page
*
* @param string $hook_suffix The current admin page.
* @return void
*/
public function enqueue_admin_page_scripts( $hook_suffix ) {
if ( 'toplevel_page_mailchimp_sf_options' !== $hook_suffix ) {
return;
}

wp_enqueue_style( 'mailchimp_sf_admin_css', MCSF_URL . 'css/admin.css', array( 'wp-jquery-ui-dialog' ), true );
wp_enqueue_script( 'showMe', MCSF_URL . 'js/hidecss.js', array( 'jquery' ), MCSF_VER, true );
wp_enqueue_script( 'mailchimp_sf_admin', MCSF_URL . 'js/admin.js', array( 'jquery', 'jquery-ui-dialog' ), MCSF_VER, true );

wp_localize_script(
'mailchimp_sf_admin',
'mailchimp_sf_admin_params',
array(
'ajax_url' => esc_url( admin_url( 'admin-ajax.php' ) ),
'oauth_url' => esc_url( $this->oauth_url ),
'oauth_start_nonce' => wp_create_nonce( 'mailchimp_sf_oauth_start_nonce' ),
'oauth_finish_nonce' => wp_create_nonce( 'mailchimp_sf_oauth_finish_nonce' ),
'oauth_window_name' => esc_html__( 'Mailchimp For WordPress OAuth', 'mailchimp' ),
'generic_error' => esc_html__( 'An error occurred. Please try again.', 'mailchimp' ),
'modal_title' => esc_html__( 'Login Popup is blocked!', 'mailchimp' ),
'modal_button_try_again' => esc_html__( 'Try again', 'mailchimp' ),
'modal_button_cancel' => esc_html__( 'No, cancel!', 'mailchimp' ),
)
);
}
}
Loading
Loading