Keyring is a WordPress plugin that is specifically designed for other plugin developers in mind (instead of being used by end-user of a WordPress site). It enables these other developers to integrate with and create authenticated requests to several popular remote services (for example Gmail, Facebook, Instagram, etc) or let the developers extend some common standards (for example OAuth1, OAuth2).
In this post, we briefly give a review of Keyring, its overall design architecture, and what it could bring to the table for the developers. Also, we discuss some areas of improvement that we think Keyring could improve on.
Keyring Features
Let’s imagine you want to integrate Gmail into your site, you want to have a feature in one of your site’s pages that allows visitors to send an email using their Gmail account. You then browse through the documentation on the Google Developer site on how to integrate the Gmail account, you found out that it utilizes OAuth 2.0 [1]. In order for you to implement OAuth 2.0 integration, you need to understand how to request a token, how to exchange a request token to the access token. You also need to implement the required endpoint in your site’s backend to be used as a callback endpoint by Google, you also need to be able to sign and verify the request-response from the Google server to your site. After you finished the token exchange part, you then need to include this token throughout all of your API call to the Google endpoint (and possibly need to refresh the token if it is expired).
Instead of having to worry about the specific and detailed flow of OAuth 2.0, Keyring allows us to use a simple mechanism to handle this integration. From within Keyring, each integration to the remote service is encapsulated with what Keyring calls a Service, so in the case of Gmail integration, we will create something like Keyring_Service_Gmail that extends the Keyring’s base Service (or another child class of that).
<?php
/**
* Custom Gmail Keyring service
*/
class Keyring_Service_Gmail extends Keyring_Service_GoogleMail {
const NAME = 'pistachio-google-mail';
const LABEL = 'Google Mail for Pistachio';
const SCOPE = 'https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/userinfo.profile'; // See https://developers.google.com/identity/protocols/googlescopes.
const ACCESS_TYPE = 'offline';
/**
* Get a specific credentials for the Pistachio Gmail
*
* @return array|null
*/
function _get_credentials() {
if (
defined( 'KEYRING__PISTACHIOGMAIL_KEY' )
&&
defined( 'KEYRING__PISTACHIOGMAIL_SECRET' )
) {
return array(
'redirect_uri' => defined( 'KEYRING__PISTACHIOGMAIL_URI' ) ? constant( 'KEYRING__PISTACHIOGMAIL_URI' ) : '', // optional.
'key' => constant( 'KEYRING__PISTACHIOGMAIL_KEY' ),
'secret' => constant( 'KEYRING__PISTACHIOGMAIL_SECRET' ),
);
} else {
return null;
}
}
}
Every Service in Keyring have a standardized flow on how it connects to the remote service:
request_token: the first part of getting the token for the Serviceverify_token: the verification step for the Service’s tokenrequest: actually make the API call that include the token that is needed
Not only for the Gmail / Google case, but this standardized flow is also used for all the Services that Keyring implements. Even in the case of “http-basic”, Keyring also has these flows. Essentially, Keyring implements a Facade [2] for those underlying remote services operations.
Keyring Architectural Limitation
When we were doing a service integration using Keyring, we think that there are some architectural limitations in the Keyring that we think it could improve on.
Every Service is a Singleton
By reading through the documentation of Keyring [3] and actually looking into the source code, we can see that every service is implemented as a Singleton.
// code to get a service by name
$service = Keyring::get_service_by_name( 'pistachio-google-mail' );
// actual code in service.php to initialize the service
static function init() {
static $instance = false;
if ( ! $instance ) {
$class = get_called_class();
$services = Keyring::get_registered_services();
if ( in_array( $class::NAME, array_keys( $services ), true ) ) {
$instance = $services[ $class::NAME ];
} else {
$instance = new $class;
Keyring::register_service( $instance );
}
}
return $instance;
}
And also the way it works in Keyring is that once we add our service into Keyring, the service singleton object is auto-initialized and its fields’ entries are filled when the plugin PHP code is loaded.
This approach in Keyring, makes it challenging when we actually want to create a test (using PHPUnit/WPUnit). In some scenarios, when we want to test our newly created Keyring Service, sometimes we want to assert its behavior when we supply that with different kinds of values (for example if the redirect_uri that we give is valid, and another case if it is invalid).
// example test case
class Test_Keyring_Service extends WP_UnitTestCase {
public function test_valid_redirect() {
// code to configure the redirect_uri with valid uri
// .....
// code to get the service
$service = Keyring::get_service_by_name( 'pistachio-google-mail' );
// other code to assert behaviour
// .....
}
public function test_not_valid_redirect() {
// code to configure the redirect_uri with invalid uri
// .....
// code to get the service
$service = Keyring::get_service_by_name( 'pistachio-google-mail' );
// other code to assert behaviour
// .....
}
}
In Keyring, the field redirect_uri is only populated when the Service is initialized, and because it is a Singleton, the next call of get_service_by_name() will return the already created Singleton object, thus if we want to verify the behavior in different scenarios, we need to make some adjustments to the code (our approach is creating some kind of “setter” function to update the field).
Only One Application Configuration Per One Service
In Keyring, every Service class (classes that extends Keyring’s Service directly or indirectly) can only be configured to use one application detail. In the example of Gmail API, one Service class can only be configured using one OAuth’s applicationkey and secret.
This will create some issues if one would like to configure a Keyring Service (Gmail for example) using multiple application credentials. The vanilla approach in Keyring requires us to create another class to handle this case if needed.
Improvement to Keyring
We think that there are a few areas that can be improved in terms of Keyring’s architectural choice. In this section, we propose some suggestions related to that.
Remove the Singleton For the Service
Instead of forcing the service to have only a Singleton instance, we instead propose for each service to have a “type” and an “identifier”. A “type” in here is similar to what Keyring already have in term of “name” (in vanilla Keyring, the “name” field uniquely identifies the Singleton object). For example in the previous case of our Service (in here we introduce a $service_id field):
class Keyring_Service_Gmail extends Keyring_Service_GoogleMail {
const NAME = 'pistachio-google-mail';
const LABEL = 'Google Mail for Pistachio';
const SCOPE = 'https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/userinfo.profile'; // See https://developers.google.com/identity/protocols/googlescopes.
const ACCESS_TYPE = 'offline';
private $service_id;
public function __construct( $service_id ) {
$this->service_id = $service_id;
}
}
By doing this approach, we enable one Service to have a multiple instance of application (with different application’s OAuth detail, for example).
Introduce a Service Registry
After removing the Singleton usage in the Keyring’s Services, we need another way to query and get those Services. Previously in vanilla Keyring we can use the Keyring::get_service_by_name() function, but after removing the Singleton, we need other mechanism as the Service’s name does not uniquely identify an instance of the Service.
Instead, what we can do is to create a Service Registry (an implementation of Registry pattern [4]). This Service Registry will provide some finders function to find our instance of the Service, and also provide function to add a service into its registry. Example:
<?php
class Service_Registry {
private $registry;
public function __construct() {
$this->registry = array();
}
public function add_service( $keyring_service ) {
$this->registry[ $keyring_service->service_id ] = $keyring_service;
}
public function get_service( $service_id ) {
return $this->registry[ $service_id ];
}
// probably need other functions to find all connected services, or
// all services with a specific service type
}
Benefit of Improvements
The improvement suggestions that we mentioned in the previous section would be beneficial for a web application that want to have multiple integrations with one service provider. This is because we can easily extend one Service class definition to cater to multiple remote service account details.
The approach also helps in terms of testing the codebase. Because we can have one instance of real implementation of the service and another instance of fake/mock implementation of the service. And because in the test case we have the freedom to create the service instances (or their mock) and add/remove them from the Service Registry, the original limitation of having all the class instances loaded on the code load is eliminated.
In terms of maintainability, we argue that the suggested approach also makes the code more maintainable because it has the benefit of easiness to test. Also because we don’t need to create a new class for each similar service integration, the number of custom Keyring service classes that we create is also reduced.
Case When the Improvements are Best Implemented
As we already discussed in the previous section, the improvements that we mentioned are best implemented in cases when we have a web application that needs to have multiple integrations with one service provider. In another case when we only need one integration (one application credential detail) for one service provider, the improvement that we proposed would have a slight overhead in terms of code refactoring (of the Keyring codebase) and also in keeping track of the inherent multiple instances logic in the codebase (probably the developers will ask why we need to have a service_id, for example).
Supporting Multiple Authentication Choices
Another topic question is how do we support two (or multiple) types of authentication in the application.
In one web application, there is an example of authentication choices that are given to the user (using an email address, Gmail, Facebook, etc.). In order for us to support this kind of multiple authentications, we need to have a separate structure that stores data specific to our web application usage. We can treat it as some kind of User data. This User data then should have a field in it that contains the authentication type (a Service provider, for example: Gmail) and also authentication identifier (specific to related the Service provider, for example: Gmail address).
After doing this, then whenever a user is coming back to our web application (for example logging in), they will choose whatever authentication providers that they like. Our web application will match the authentication identifier that the service provider gives with the data that we have, and return the corresponding User data.
<?php
class User {
private $user_id;
private $user_name;
// other fields specific for User need
// ....
// array of Account_Identifier
private $account_identifiers = array();
}
class Account_Identifier {
private $keyring_service_name; // for example: 'gmail'
private $keyring_service_account_identifier; // for example: 'thisismy@email.com'
}
(Note that here we explained in high-level concept using PHP pseudo-code, exact detail implementation will also consider database operation, table structure, performance, etc.)