Search
Close this search box.

How to Single Sign-On to Multiple WordPress Sites

When working with large organisations, it is inevitable that they are going to have more than one WordPress website.

I’m often asked whether it is possible for a user to sign in to a site just once but get access to multiple WordPress sites.

In essence, they are looking to share login session information across multiple sites.

We call this Single Sign-On or SSO for short.

Here is how to successfully implement WordPress Single Sign-On where users sign in once then get access to multiple sites without having to log in again.

Before we jump into the solutions, we should first understand how users log in to WordPress.

For this post, we are considering two types of user interactions with our website – a non-logged-in user and a logged-in user.

Users without a login, customarily called “visitors”, can navigate around the public pages of your website, without having to enter any identifiable information.

That’s cool.  Those pages are open to the public, and we’re happy for visitors to read them, and they don’t need any special permissions to do that.

When users need to do more stuff like edit content, purchase items, checkout or take lessons we want to hold them accountable and to make sure they can only do the things they are allowed to do.

So we let them log in to our website, accessing the premium content that regular visitors can’t access.

After a user logs in, we want them to be able to navigate around our WordPress site or their member dashboard, without having to re-enter their login details on each page refresh.

We call this state a session.

Sessions

A session will usually last until the user logs out, closes all related browser tabs or if the session has been idle for a set amount of time.

WordPress stores login session information in three cookies.

Cookies are small data files stored on your local computer by your web browsers.

The browser can read, update and delete cookies using the JavaScript language.

According to the WordPress codex, three cookies are stored on your computer to track session information.

When you initially log in, the cookie wordpress_[hash] is set to store your login credentials.

This cookie allows access to everything in the /wp-admin/ folder.

That’s pretty much everything you edit in WordPress.

After you have successfully logged in, the browser stores the cookie wordpress_logged_in_[hash] which indicates when you were logged in, and who you are.

WordPress also sets some wp-settings-{time}-[UID] cookies.

The UID is your user ID from the database table and these cookies store preferences in the admin dashboard area.

In Chrome, we can check which cookies are in use by clicking in the padlock area just to the left of the address bar.

Or to see more advanced cookie details, open Chrome Developer Tools <ctrl>+<shift>+i and click on the Application tab, then Cookies.

Chrome Developer Tools - Cookies View

The WordPress Auth Cookie

The auth cookie used by WordPress when you log in stores some interesting data.

We’ll look into them just now.

Cookie Parts

Cookie ID

The auth cookie ID is defined in the file wp-includes/default-constants.php.

It starts with “wordpress_” and then what follows is a hash of the site URL.

The username value is taken straight from the database and is the ID assigned to your registration details when stored in the wp_users database table.

Here is the code in wp-includes/default-constants.php which defines the AUTH_COOKIE and COOKIEHASH variables.

	if ( !defined('AUTH_COOKIE') )
define('AUTH_COOKIE', 'wordpress_' . COOKIEHASH);
if ( !defined( 'COOKIEHASH' ) ) {
$siteurl = get_site_option( 'siteurl' );
if ( $siteurl )
define( 'COOKIEHASH', md5( $siteurl ) );
else
define( 'COOKIEHASH', '' );
}

Let’s now take a look at the value stored in the cookie using the previous image above as a reference.

Note that the value “%7C” is a delimiter and I’ve obscured some of the final hash value as this is from my login session.

Username

The first value inside the cookie is the username of the user logging into WordPress.

Expiry Time

Expiry Time is a Unix timestamp value containing the date and time which the cookie expires on.

The expiry DateTime is set when the cookie is created and defaults to two days.

If you have ticked the “Remember Me” checkbox on the login screen, the default expiry DateTime will be 14 days.

You can find the code in file wp-includes/pluggable.php in the function wp_set_auth_cookie.

if ( $remember ) {
/**
* Filters the duration of the authentication cookie expiration period.
*
* @since 2.8.0
*
* @param int $length Duration of the expiration period in seconds.
* @param int $user_id User ID.
* @param bool $remember Whether to remember the user login. Default false.
*/
$expiration = time() apply_filters( 'auth_cookie_expiration', 14 * DAY_IN_SECONDS, $user_id, $remember );
/*
* Ensure the browser will continue to send the cookie after the expiration time is reached.
* Needed for the login grace period in wp_validate_auth_cookie().
*/
$expire = $expiration ( 12 * HOUR_IN_SECONDS );
} else {
/** This filter is documented in wp-includes/pluggable.php */
$expiration = time() apply_filters( 'auth_cookie_expiration', 2 * DAY_IN_SECONDS, $user_id, $remember );
$expire = 0;
}

After this time, the cookie is no longer valid and you will be asked to log back into the website.

Token

WordPress version 4 and above adds a token value to the cookie for additional security.

The token is generated from a set of functions that aim to better bind the session cookies to a specific user and ensure that they expire correctly.

Hash

The hash is the most interesting part of the cookie value.

Up till this part, the cookie ID, username and expiry time are all predictable and can be worked out by any savvy hacker.

Allowing a hacker to guess values is no good for security which is why the hash value exists.

To add some randomness to the cookie, the value is calculated from a sub-part of the user’s login password hash value that is stored in the wp_user table of the database.

How Does WordPress Store User Passwords?

A great question and worthy of a blog post of its own.

WordPress does not store a user’s password in the database, either in plaintext or encrypted format.

A hash value of the password is created upon user registration and that is stored in the user table.

When a user types their password into the login screen, WordPress hashes that password value and then compares it like for like to the value stored in the database.

By default, WordPress uses the PHP MD5 function to generate a user’s password hash.

Password hashes are stored in this format (that’s not my password hash!):WordPress Password Hash

WordPress allows developers to override the login and registration system, including how passwords are dealt with, which is why the type of encoding function being used is prepended to the stored password hash.

In the case above, $P$B referrs to the PHP MD5 function.

Generating the Cookie Hash Value

Specifically, characters 8 to 12 of the user’s password hash are used as a secret key to generate randomness into the cookie hash value.

This is done by the function wp_generate_auth_cookie in the file wp-includes/pluggable.php.

function wp_generate_auth_cookie( $user_id, $expiration, $scheme = 'auth', $token = '' ) {
$user = get_userdata($user_id);
if ( ! $user ) {
return '';
}

if ( ! $token ) {
$manager = WP_Session_Tokens::get_instance( $user_id );
$token = $manager->create( $expiration );
}

$pass_frag = substr($user->user_pass, 8, 4);

$key = wp_hash( $user->user_login . '|' . $pass_frag . '|' . $expiration . '|' . $token, $scheme );

// If ext/hash is not present, compat.php's hash_hmac() does not support sha256.
$algo = function_exists( 'hash' ) ? 'sha256' : 'sha1';
$hash = hash_hmac( $algo, $user->user_login . '|' . $expiration . '|' . $token, $key );

$cookie = $user->user_login . '|' . $expiration . '|' . $token . '|' . $hash;

/**
* Filters the authentication cookie.
*
* @since 2.5.0
* @since 4.0.0 The `$token` parameter was added.
*
* @param string $cookie Authentication cookie.
* @param int $user_id User ID.
* @param int $expiration The time the cookie expires as a UNIX timestamp.
* @param string $scheme Cookie scheme used. Accepts 'auth', 'secure_auth', or 'logged_in'.
* @param string $token User's session token used.
*/
return apply_filters( 'auth_cookie', $cookie, $user_id, $expiration, $scheme, $token );
}

Line 9 generates a manager token instance which helps manage the user session and cookie expiry time.

We can see line 12 grabs characters 8 to 12 of the user’s password hash from the database.

Line 14 generates a secret key using the random characters from line 12.

Then, line 18 generates the random hash value part of the cookie.

Line 20 finally sets the cookie complete cookie value using the username, expiry and hash values.

Because each session cookie is bound to a user, using data from the WordPress wp_users table for that site, each cookie is specifically meant to be used only for login to that site.

Protecting Your Salts

When WordPress generates a hash for security purposes such as cookies, it uses the SALT keys found in your wp-config.php file to generate extra randomness.

The default set of SALT keys look like this:

define('AUTH_KEY',         'put your unique phrase here');
define('SECURE_AUTH_KEY', 'put your unique phrase here');
define('LOGGED_IN_KEY', 'put your unique phrase here');
define('NONCE_KEY', 'put your unique phrase here');
define('AUTH_SALT', 'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT', 'put your unique phrase here');
define('NONCE_SALT', 'put your unique phrase here');

it is highly recommended that you visit the link in the comments about the SALT keys to generate a new set.

Copy and paste the new values into the wp-config.php file replacing the default ones.

Cookie Conclusion

Cookies are not transferrable between different WordPress sites, and this is why we need to implement an SSO solution.

WordPress Multisite

A multisite operates a bit differently than single WordPress sites and can perform SSO if the administrator of each subsite has the same email address as the network super admin.

But that only works for super admins, not for regular site users.

Again, we need to find a solution for Single Sign-on even in multisite.

The Hammer and Peg Approach

I don’t recommend this method, but I’ll describe it for reference just in case somebody points it out in future comments.

In the previous sections, we have seen that session cookies are bound to a users login details which WordPress stores in the wp_users table in the database.

So, what if we could create multiple WordPress sites that shared common wp_users and wp_usermeta tables?

It is doable but it isn’t a good overall solution.

Like a multisite installation, this method requires that your shared WordPress sites all store their information in the same database.

This limits the options you have to improve database performance for a single site.

There is also a caveat in that the sites must be installed on the same domain or sub-domain for this to work.

I won’t describe the process in detail here as it’s outlined step-by-step in a Stack Exchange post from 2017.

In summary, you install each of your sites into the same database using a different database prefix, remembering not to log in to any of these sites until you complete the entire process.

When installing each site, you specify the cookie path, domain and hash method in the wp-config.php file, overriding the default functions. See below.

define('COOKIE_DOMAIN', '.xyz.com');
define('COOKIEPATH', '/');
define('COOKIEHASH', md5('xyz.com'));

After each site installation, you have to copy a function in all sites to synchronise all the user and user meta to a single site you have chosen as the “main” site where logins will happen.

After the user data has been migrated, you drop the wp_user and wp_usermeta tables from the other sites.

You are left with multiple sites sharing common wp_user and wp_usermeta tables.

The process for doing this with existing sites is more complex and I’ll let you read the Stack Exchange article to get your head around that.

How Does Single Sign-On Work?

Finally, let’s talk about SSO.

In order for two or more different sites to enable users to use Single Sign-on, one site must be nominated as a primary, or server, site and the others as clients.

The primary site is the one that manages user sessions, including cookie information that the browsers need.

If a user tries to log in to a client site, that client will need to pass that information to the primary site for authentication and wait to receive back authenticated user session data allowing the user to log in.

For SSO to work, therefore, login and session data must be passed and processed between the different sites.

This all needs to work securely and seamlessly.

This is a client-server model and there are three industry-standard methods for passing authentication data between a client and server for SSO.

They are called SAML, OAuth and OpenID.  Let’s look at them now.

What is SAML?

Security Assertion Markup Language (SAML, pronounced sam-el) is an open standard for exchanging authentication and authorization data between parties.

SAML uses the XML data structure to exchange information.

Some acronyms that SAML uses:

  • IdP – Identity Provider
    The service which holds the login information
  • SP – Service Provider
    The service requesting an ID check

First released in 2002, SAML had been in use for quite a while.

In 2005 v2.0 was released with much-improved functionality and security and it hasn’t changed much since then.

SAML provides a solution to allow your identity provider and service providers to exist separately from each other.

This centralizes user management and provides access to Software as a Service (SaaS) solutions.

How Does SAML Work?

When a user tries to log in to a SAML enable application,  the service provider requests authorization from the associated identity provider.

The identity provider authenticates the user’s credentials and then returns the authorization for the user to the service provider.

The user is now able to use the application.

SAML 2.0 Flow

LDAP, Microsoft Active Directory and RazDC are all examples of identity providers that hold user security login credentials.

Salesforce and Amazon Web Services are examples of service providers that request access using SAML from an identity provider, to log in users.

Because SAML uses XML to share authentication data it is popular with large organisation and enterprises.

Here is an example of a SAML XML assertion (command):

<saml:Assertion
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
ID="b07b804c-7c29-ea16-7300-4f3d6f7928ac"
Version="2.0"
IssueInstant="2004-12-05T09:22:05Z">
<saml:Issuer>https://idp.example.org/SAML2</saml:Issuer>
<ds:Signature
xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
<saml:Subject>
<saml:NameID
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:transient">
3f7b3dcf-1674-4ecd-92c8-1544f346baf8
</saml:NameID>
<saml:SubjectConfirmation
Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml:SubjectConfirmationData
InResponseTo="aaf23196-1773-2113-474a-fe114412ab72"
Recipient="https://sp.example.com/SAML2/SSO/POST"
NotOnOrAfter="2004-12-05T09:27:05Z"/>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Conditions
NotBefore="2004-12-05T09:17:05Z"
NotOnOrAfter="2004-12-05T09:27:05Z">
<saml:AudienceRestriction>
<saml:Audience>https://sp.example.com/SAML2</saml:Audience>
</saml:AudienceRestriction>
</saml:Conditions>
<saml:AuthnStatement
AuthnInstant="2004-12-05T09:22:00Z"
SessionIndex="b07b804c-7c29-ea16-7300-4f3d6f7928ac">
<saml:AuthnContext>
<saml:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
</saml:AuthnContextClassRef>
</saml:AuthnContext>
</saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute
xmlns:x500="urn:oasis:names:tc:SAML:2.0:profiles:attribute:X500"
x500:Encoding="LDAP"
NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.1"
FriendlyName="eduPersonAffiliation">
<saml:AttributeValue
xsi:type="xs:string">member</saml:AttributeValue>
<saml:AttributeValue
xsi:type="xs:string">staff</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>

What is OAuth?

OAuth provides clients with a “secure delegated access” to server resources on behalf of a resource owner.

It is a newer standard that was co-developed by Google and Twitter to enable streamlined internet logins.

You have probably used OAuth without even knowing it.

When a website asks you to log in using your Google or Facebook ID, that’s OAuth working in the background.

Version 1.0 was released in 2006 and the more secure version 2.0 was released in 2012.

The new version provides specific authorization flows for web applications, desktop applications, mobile phones, and smart devices.

OAuth uses the JSON data structure to exchange information.

How Does OAuth Work?

OAuth is an authorization protocol, rather than an authentication protocol.

Using OAuth on its own as an authentication method may be referred to as “pseudo-authentication”.

The following diagrams highlight the differences between using OpenID (specifically designed as an authentication protocol) and OAuth for authentication.

OpenID vs OAuth
From Wikipedia

With OAuth, you don’t type in your user ID and password, rather, you log in to the identity provider (Google for example) then there is a visual prompt to authorise the app or service you want to log in to with the identity provider.

This is a visual-based system presenting a button to confirm the identity provider can perform actions on the user’s behalf.

So with OAuth, you can never be 100% certain that the real user is the one asking for the “key to your home” and hence it is considered less secure than SAML (or OpenID).

What is OpenID?

OpenID is a framework that uses OAuth2 for authorisation.

It was released in 2005 but its adoption by Symantec and Microsoft in 2007 boosted its popularity.

“OpenID Connect” is the name of the current version.

Some acronyms that OpenID uses:

  • Relying Party
    The service requiring an ID check

How Does OpenID Work?

It allows you to choose an identity provider (such as Google), to be your OpenID identity provider.

If a site supports OpenID you can now log in using the credentials from your nominated identity provider, hence you have a single set of security credentials to remember.

You only give your password to your OpenID provider, and then your provider tells the websites you’re visiting that you are who you say you are.

OpenID login form
Source OpenID.net

No website other than your provider ever sees your password plus you can choose which of your data a relying party has access to.

OpenID verification
Source OpenID.net

Authentication vs. Authorisation

In chatting about login systems, “authentication” and “authorisation” seem interchangeable.

At first glance, they appear to mean the same thing but in the context of user logins, they are very different.

Authentication in the context of a user accessing an application tells an application who the current user is and whether or not they are present by entering their secure login details.

So, authentication is all about the user and their presence with the application.

You really need authentication to happen before letting a user access their private information.

This ensures they are who they say they are when accessing web sites and applications.

Authorisation cannot provide this assurance, nor does it say how the user proved their presence or even if they’re still there.

The authorisation is based on a trusted token that at some point in the past, a user allowed a trusted connection to be used.

So we tend to see and use authorisation for client applications, or services that regularly access other systems, but not for real people.

SAML vs. OAuth

OAuth uses a similar methodology as SAML to share login information.

SAML provides more precise control to enterprises to keep their SSO logins more secure, whereas OAuth is better on mobile and uses JSON.

Because OAuth is a visual-based system and doesn’t require the user to type in login credentials, it is considered less secure and at risk of phishing attacks.

In April–May 2017, about one million users of Gmail were targeted by an OAuth-based phishing attack, receiving an email purporting to be from a colleague, employer or friend wanting to share a document on Google Docs.

Those who clicked on the link within the email were directed to sign in and allow a potentially malicious third-party program called “Google Apps” access their “email account, contacts and online documents”.

Within “approximately one hour”, the phishing attack was stopped by Google, who advised those who had given “Google Apps” access to their email to revoke such access and change their passwords.

– Source Wikipedia

I consider SAML a better solution and more secure than OAuth.

Single Sign-On Plugins for WordPress

With all this based in mind let’s finally look at some Single Sign-on solutions for WordPress.

SAML SP Single Sign On – SSO

The plugin SAML SP Single Sign-On – SSO by miniOrange is one of the most popular SSO plugins for WordPress.

miniorange-saml-20-single-sign-on

The plugin turns your WordPress site into a SAML 2.0 Service Provider (a client).

It supports a heap of well-known Identity Providers such as Google Apps, ADFS, Azure AD, Okta, Salesforce, Shibboleth, SimpleSAMLphp, OpenAM, Centrify, Ping, RSA, IBM, Oracle, OneLogin, Bitium, WSO2, NetIQ etc.

The free plugin only works for single WordPress installations, not multisite.

If you don’t want to use an external IdP then the plugin is compatible with the WordPress OAuth Server plugin which we will cover next.

The free plugin allows unlimited connections over SAML.

Also, user registration sync on your site and the ability to map your user profile fields to those from your chosen IdP.

The premium (paid) version gives you a year’s worth of plugin updates, support and the following extra features:

  • Support for SAML Single Logout (Works only if your IdP supports SLO).
  • Auto-redirect to your IdP for authentication without showing them your WordPress site’s login page.
  • Advanced Attribute Mapping to map your IdP attributes to your WordPress site attributes like Username, Email, First Name, Last Name, Group/Role, Display Name.
  • Advanced Role Mapping to assign WordPress roles your users based on the group/role sent by your IdP.
  • Short Code (PHP or HTML) to place the login link wherever you want on the site.
  • Reverse-proxy Support.
  • Select Binding Type to select HTTP-Post or HTTP-Redirect binding type to use for sending SAML Requests.
  • Support for Integrated Windows Authentication (IWA)
  • Step-by-step Guides.
  • WordPress Multi-site Support
  • Multiple SAML IdPs Support
miniOrange setup screen

Share Logins Pro

Another plugin which can share logins across multiple sites is Share Logins Pro from Codexpert.

share logins pro

The documentation on the site does not specify whether it uses SAML, OAuth or some proprietary method of authentication.

Interestingly, the plugin does not require you to appoint a “master” site, rather you must configure all sites that you want the plugin to work with.

You don’t need an SSL certificate either as data is encrypted before being transmitted using the Ncrypt library.

The free plan only works with two sites synchronising user logins and logouts.

It does not sync user creations, updates or deletions.

If you need to sync more sites, you’ll need to up the license key to a paid plan for 2 (with complete user sync), 5 or unlimited sites.  Prices, of course, go up for more sites.

Update Sept 7, 2020:
It has been brought to my attention that the current version of the plugin as of update date, uses the same default initialisation vector (IV) and the payload is never signed.

Under the correct conditions and a breach in the site, an attacker could determine the encryption method used and hence gain access to data from the site.

The plugin authors have been notified of this issue, so please check with them that this has been fixed before using it on your sensitive production sites.

WP Remote Users Sync

WP Remote Users Sync WordPress Plugin

This plugin is another great choice to securely sync users between multiple sites.

From the author Alexandre Froger:

WP Remote Users Sync “listens” to changes related to WordPress users, and fires outgoing “User Actions” to registered remote sites.

The registered remote sites with WP Remote Users Sync installed then catch incoming Actions and react accordingly.

There is no “Master Website” or centralized authority: each site is working independently, firing and receiving Actions depending on each site’s configuration.

These User Actions include Login, Logout, Create, Update, Delete, Password, Role and Metadata synchronization, and the list can be extended with custom code.

The plugin implements OAuth2 in the sense that each remote site is its own Identity Provider: with strongly signed, encrypted and IP-validated communication, each remote site is contacted to obtain a uniquely generated token that is then included in the payload of subsequent communications (single-use tokens for Login and Logout).

All communications are encrypted to ensure confidentiality. However, it doesn’t follow the OAuth2 protocol strictly speaking because the payloads do not use the JSON format.

More concretely and specifically regarding Login, by default, WP Remote Users Sync uses the same method as stackexchange websites implement to log in their users on multiple sites (include an HTML  element that loads a remote URL to set the cross-domain third-party authentication cookies silently).

However, because this breaks in iOS browsers and Safari, these browsers use a method similar to how users log in Youtube with their Google Account (explicitly redirecting users to the sites where authentication is needed with an on-screen “Processing…” message).

WP OAuth Server

If you don’t want to use third-party IdP’s such as Google or Facebook, you can set your site up to be an OAuth server

This allows you to connect multiple clients to your designated “master” site.

WP OAuth Server

The plugin WP OAuth Server does what it says and allows you to set up a master server site to connect clients to.

Here’s their promo video:

An OAuth server is particularly useful for authentication with the WP REST API.

The REST API doesn’t yet support OAuth 2.0, but it is on the cards.

The free version does support unlimited clients.

Multisite support is not mentioned in the free version.

You can choose to purchase the pro version which, of course, comes with many more features including OpenID support:

  • All OAuth 2.0 Grant Types (Auth Code, Implicit, Client Credentials, User Credentials, JWT, OpenID Connect)
  • WP REST API User Authentication
  • Unlimited Clients
  • OpenID Discovery
  • Hybrid Application Passwords
  • Built-in Resource Server for custom API development outside of the WP REST API.
  • Restrict all REST API Endpoints to only authenticated users.
  • Live Chat Support in the Admin Area.
  • PHP 7 Compatible

The developer license which gives you unlimited site support, firewall and log activity will set you back USD $499 as of the time of writing.

This is not a cheap option.

miniOrange OAuth Server and Client

Remember we talked about miniOrange’s SAML plugin above?

They also provide OAuth server and client plugins for WordPress and does not work with multisite.

The free server license will support one client site, with various paid levels depending on the features you need.

The client license also comes with various paid levels and a free one for single connections.

This means you are paying for both server and client plugins.

Conclusion

Single Sign-on between WordPress sites is a common practice for large organisations.

It is becoming more common practice for complex websites such as memberships and online learning portals.

If security and bespoke functionality are your main concerns, SAML is the SSO strategy that you should be investing in.

As an authentication method, it gives superior security and user awareness.

SAML’s XML language allows for a superior definition of data.

OAuth works best for client apps when the authorising person isn’t always present.

For simple setups, OAuth is a good starting pathway for implementing SSO.

If you need any help with implementing Single Sign-on between WordPress sites, we are happy to consult with you to work out a strategy.

Looking to level up your WordPress development skills?

WordPress Plugin Development book

I self-taught myself WordPress development, but this book took it to another level.

Each chapter builds upon the next, but you can also use it to jump into specific areas like WP Cron, WP REST endpoints, building a custom plugin repository etc.

If you’re like me and like having reference books at hand, this is the one that sits on my desk all the time.

Was this article helpful?
YesNo