Pragmatism in the real world

Custom OAuth2 authentication in Apiiglity

I have a client that’s writing an Apigility API that needs to talk to a database that’s already in place. This also includes the users table that is to be used with Apigility’s OAuth2 authentication.

Getting Apigility’s OAuth2 integration to talk to a specific table name is quite easy. Simply add this config:

'storage_settings' => array(
    'user_table' => 'user',
),

To the relevant adapter within zf-mvc-auth => authentication config.

However, if you want to use different column names, that’s a bit trickier as they are hardcoded in the OAuth2\Storage\Pdo class. To get Apigility’s OAuth2 components to look at the correct columns, you create your own OAuth2 Adapter. I chose to extend ZF\OAuth2\Adapter\PdoAdapter which extends OAuth2\Storage\Pdo and go from there.

ZF\OAuth2\Adapter\PdoAdapter extends the base class to add bcrypt hashing. This is good, so it’s a good place to start from. I created a new module, MyAuth to hold my adapter and its factory. The adapter looks like this:

<?php
namespace MyAuth;

use ZF\OAuth2\Adapter\PdoAdapter;

/**
 * Custom extension of PdoAdapter to validate against the WEB_User table.
 */
class OAuth2Adapter extends PdoAdapter
{
    public function __construct($connection, $config = array())
    {
        $config = [
            'user_table' => 'legacy_user'
        ];

        return parent::__construct($connection, $config);
    }

    public function getUser($username)
    {
        $sql = sprintf(
            'SELECT * from %s where email_address=:username',
            $this->config['user_table']
        );
        $stmt = $this->db->prepare($sql);
        $stmt->execute(array('username' => $username));

        if (!$userInfo = $stmt->fetch(\PDO::FETCH_ASSOC)) {
            return false;
        }

        // the default behavior is to use "username" as the user_id
        return array_merge(array(
            'user_id' => $username
        ), $userInfo);
    }

    public function setUser($username, $password, 
        $firstName = null, $lastName = null)
    {
        // do not store in plaintext, use bcrypt
        $this->createBcryptHash($password);

        // if it exists, update it.
        if ($this->getUser($username)) {
            $sql = sprintf(
                'UPDATE %s SET pwd=:password, firstname=:firstName,
                    surname=:lastName WHERE username=:username',
                $this->config['user_table']
            );
            $stmt = $this->db->prepare($sql);
        } else {
            $sql = sprintf(
                'INSERT INTO %s (email_address, pwd, firstname, surname)
                    VALUES (:username, :password, :firstName, :lastName)',
                $this->config['user_table']
            );
            $stmt = $this->db->prepare($sql);
        }

        return $stmt->execute(compact('username', 'password', 'firstName',
            'lastName'));
    }

    protected function checkPassword($user, $password)
    {
        return $this->verifyHash($password, $user['pwd']);
    }
}

This code for getUser and setUser() is lifted directly from OAuth2\Storage\Pdo and all I’ve done is changed the column names. In this case I have email_address for my username, and pwd for the password column. Similar, I wrote my own checkPassword based on ZF\OAuth2\Adapter\PdoAdapter, again changing the array key to check to 'pwd'.

Now that we have the actual work done, we need to wire it into Apigility.

Firstly we need a factory so that the DIC can instantiate our adapter:

<?php
namespace MyAuth;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Db\Adapter\Driver\Pdo\Pdo as PdoDriver;

class OAuth2AdapterFactory implements FactoryInterface
{
    /**
     * Create service
     *
     * @param ServiceLocatorInterface $serviceLocator
     * @return OAuth2Adapter
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $connection = $serviceLocator->get('DB\Master');
        if (!$connection->getDriver() instanceof PdoDriver) {
            throw new \RuntimeException("Need a PDO connection!");
        }

        $pdo = $connection->getDriver()->getConnection()->getResource();
        return new OAuth2Adapter($pdo);
    }  
}

This is fairly standard code. Note that the DB\Master is the name of the database connection that is set up in the Apigility admin. I’ve been a bit lazy and assume that it’s a PDO based adapter. If it isn’t, it’ll blow up, so if you’re not using PDO, then it won’t work as is!

To register your new authentication adapter with Apigility, create a config file in config/autoload and call it myauth.global.php or something:

<?php
return [
    'zf-mvc-auth' => [
        'authentication' => [
            'adapters' => [
                'MyAuth' => [
                    'adapter' => 'ZF\\MvcAuth\\Authentication\\OAuth2Adapter',
                    'storage' => [
                        'storage' => 'MyAuth\OAuth2Adapter',
                        'route' => '/oauth',
                    ],
                ],
            ],
        ],
    ],
];

The adapter is called MyAuth and is now available to select in the API configuration pages of the admin:

Myauth apigility

To sum up

All in all, it’s really easy to write custom OAuth 2 authentication for Apigility as it’s a very flexible platform. I’ve simply changed the column names here, but it would be easy enough to write an adapter against a different storage system altogether, though you would have to override more methods and possibly start from a more appropriate base class.

7 thoughts on “Custom OAuth2 authentication in Apiiglity

  1. Hi, I want also to use custom OAuth2 Authentication with MongoAdapter. I put configuration in local.php as

    return array(
        'zf-mvc-auth' => array(
            'authentication' => array(
                'adapters' => array(
                    'oauth2_mongo' => array(
                        'adapter' => 'ZF\\MvcAuth\\Authentication\\OAuth2Adapter',
                        'storage' => array(
                            'adapter' => 'mongo',
                            'dsn' => 'mongodb://localhost:27017/mybootstrapwebapp',
                            'database' => 'mybootstrapwebapp',
                            'route' => '/oauth',
                            'storage' => 'Authentication\\OAuthAdapter\\Mongo', //Custom adapter
                        ),
                        //Other configuration
                        'allow_implicit' => true,
                        'storage_settings' => array(
                			'user_table' => 'user',
                			'profile_user_table' => 'profile_user',
            			),
                    ),
                ),
            ),
        ),
    );
    

    But I still have

    {
        "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
        "title": "invalid_grant",
        "status": 401,
        "detail": "Invalid username and password combination"
    }
    

    as response.
    Can you help me ?

    My MongoAdapter code is :

    services = $services;
        }
    
        public function getUserDetails($username)
        {
            if ($user = $this->getUser($username)) {
                $user['user_id'] = $user['_id']->{'$id'};
            }
            return $user;
        }
    	
    	public function getUser($username)
        {
            $result = $this->collection('user_table')->findOne(array('email' => $username));
    
            return is_null($result) ? false : $result;
        }
    	
    } 
    

    And my factory :

    namespace Authentication\Factory\OAuth;
    
    use ZF\OAuth2\Factory\MongoAdapterFactory as VendorMongoAdapterFactory;
    use Zend\ServiceManager\ServiceLocatorInterface;
    use Authentication\OAuthAdapter\Mongo;
    use ZF\OAuth2\Controller\Exception;
    
    class MongoAdapterFactory extends VendorMongoAdapterFactory
    {
        /**
         * @param ServiceLocatorInterface $services
         * @throws \ZF\OAuth2\Controller\Exception\RuntimeException
         * @return Authentication\OAuthAdapter\Mongo
         */
        public function createService(ServiceLocatorInterface $services)
        {
            $config  = $services->get('Config');		
            return new Mongo(parent::getMongoDb($services), parent::getOauth2ServerConfig($config), $services);
        }
    }
    

    In my module configuration Module.php :

    public function getServiceConfig() {
    		return array (
    		
    				'factories' => array(
    					//OAuth2
    					'Authentication\\OAuthAdapter\\Mongo' => 'Authentication\Factory\OAuth\MongoAdapterFactory',),
    			);
    	}
    
    
  2. i run this:
    http://127.0.0.19:8888/oauth

    added below json in body row data:
    {
    "grant_type": "password",
    "username": "testuser",
    "password": "testpass",
    "client_id": "testclient",
    "client_secret": "testpass"
    }

    I am getting this error :

    {
    "type": "http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html",
    "title": "invalid_grant",
    "status": 401,
    "detail": "Invalid username and password combination"
    }

    can anyone help here

  3. Hi Rob, many thanks for this topic.
    I have a question about setting a specific user table for authentication:
    adding string
    'storage_settings' => array(
    'user_table' => 'user',
    ),
    to 'zf-mvc-auth' array config did not work for me;
    I figured out that I had to add that string to 'zf-oauth2' array config instead.
    Did I miss something or my solution is correct too?
    Thanks for answering.
    Regards,
    Davide.

    PS: maybe this can help both johnson and Hasina

  4. Hi Rob, thank you for this article.
    Unfortunatly, as a newbie to ZF i'm not able to get it running. Where do you saved the two files to? I did not manage to get the factory recognized.

    Thanks for answering!

    Regards, Sören

  5. Hi Rob,

    Thanks for this but like Soren I'm getting an "Unable to resolve service "MyAuth\OAuth2Adapter" to a factory" error. I'm using ZF3 so have updated the factory to use the __invoke function but still the same error. I've defined the factory for MyAuth\OAuth2Adapter in every place possible (module.config.php, Module.php, myauth.global.php, hell even in global.php) but I still get this error. Any idea how to register this factory correctly? Thanks.

Comments are closed.