How to integrate Duo Two Factor Authentication with CakePHP3

How to integrate Duo Two Factor Authentication with CakePHP3

So recently I wanted to integrate Duo two-factor authentication into one of the client’s projects but I didn't find much documentation support on CakePhp3. After struggling for a couple of days to figure out how to integrate DUO 2FA with CakePhp3, I achieved success in integration and so decided to write this blog so that I can genuinely contribute and offer a glimpse to all those developers who are looking out for similar blogs to resolve their issues.

 

However, before jumping on to the solution right away, let’s first understand what two-factor authentication is all about.

 

What is Two-Factor Authentication?

Two-factor authentication (commonly abbreviated as 2FA) is a security system that requires two separate, distinct forms of identification to access something.

 

It is a specific type of multi-factor authentication (MFA) that strengthens access security by requiring two methods (also referred to as authentication factors) to verify your identity. These factors can include:

  • Things you know (a personal identification number (PIN), a password, answers to "secret questions" or a specific keystroke pattern)
  • Things you have (such as a text with a code sent to your smartphone or other devices, or a smartphone authenticator app)
  • Things you are (biometric pattern of a fingerprint, face, retina scan, or a voice print)

 

2FA protects against phishing, social engineering, and password brute-force attacks. It secures your logins from attackers exploiting weak or stolen credentials.

 

By integrating two-factor authentication with your applications, attackers are unable to access your accounts without possessing your physical device needed to complete the second factor.

 

While 2FA does improve security, it is not foolproof. These following instructions will guide you through how to do two factor authentication with Cake PHP.

 

DUO SDK Workflow

 

1. Call health_check() Create a Client() object
 

try {

      $duo_client = new Client(

      $duoconfig['client_id'],

      $duoconfig['client_secret'],

      $duoconfig['api_hostname'],

      $duoconfig['redirect_uri']

        );

     } catch (DuoException $e) {

       throw new ErrorException("*** Duo config error. Verify the values in duo.conf are correct ***\n" . $e->getMessage());

 }

2. Call health_check()

 try {

      $duo_client->healthCheck();                        

      } catch (DuoException $e) {

        $logger->error($e->getMessage());

      if ($duo_failmode == "OPEN") {

          $this->Flash->error(__('Login Successful, but 2FA Not Performed. Confirm Duo client/secret/host values are correct'));

          return $this->redirect($this->Auth->redirectUrl(['controller' => 'prequals','action' => 'index']));

      } else {

        $this->Flash->error(__('2FA Unavailable. Confirm Duo client/secret/host values are correct'));

        return  $this->redirect($this->Auth->logout());

        }

    }

3. Call generate_state()

$duostate = $duo_client->generateState();

 

4. Call create_auth_url()

$prompt_uri = $duo_client->createAuthUrl($user[0]->username, $duostate);

 

5. Redirect the Client

$this->redirect($prompt_uri);

 

6. Wait for the Redirect from Duo back to your Redirect URI. 

It redirects you to redirect URI mentioned in duo config. For example: https://yourdomain.com/duo-callback

 

7. Validate the state Parameter

  $state=$this->request->query['state'];

 $saved_state = $this->Auth->user('duostate');

 

 if ($state != $saved_state) {

     $this->Flash->error(__('Duo state does not match saved state'));

     return $this->redirect($this->Auth->redirectUrl(['controller'=>'users','action' => 'login']));

}

8. Call exchange_authorization_code_for_2fa_result()

if ($state == $saved_state) {

         try {

              $decoded_token =  $duo_client->exchangeAuthorizationCodeFor2FAResult(

            $code,

              $username

              );

              } catch (DuoException $e) {

          $this->Flash->error(__('Error decoding Duo result. Confirm device clock is correct.'));

          return $this->redirect($this->Auth->redirectUrl(['controller'=>'users','action' => 'login']));

       }                

}

Steps to integrate DUO 2FA into your web application

  1. Sign up to DUO and generate a Web SDK.
    You can either follow the below steps or refer to DUO official documentation

    1. Create an account on DUO
    2. Login to the DUO admin panel
    3. Under applications and navigate to Applications. Click Protect an Application and locate the 2FA-only entry for Web SDK in the applications list.
    4. Click Protect to the far-right to configure the application and get your Client ID, Client Secret, and API hostname. You'll need this information to complete your setup. See Protecting Applications for more information about protecting applications in Duo and additional application options.
      Previously, the Client ID was called the "Integration key" and the Client secret was called the "Secret key".
       
  2. Add the duo-universal Dependency to your Project.
    In order to communicate with the Duo 2FA service, we will need to link the DUO web SDK with the web application.
    There are three ways to do so

a) Issue the following command:

composer require duosecurity/duo_universal
 

b) add the following to composer under req

"require": {

    "duosecurity/duo_universal_php": "^1.0"

}
 

c) add this https://github.com/duosecurity/duo_universal_php to your vendor folder

Create duo config file under config folder and name it duo.conf
 

; Duo integration config

[duo]

client_id ='DIETGLB6WJMHGFH12ABC'

client_secret ='AMY4RTYyGF6kr1YUgapUFlswNvJ20IgWqBwxqPQR'

api_hostname ='api-f1d12345.duosecurity.com'

redirect_uri = 'https://yourdomain.com/duo-callback'

failmode = closed

 

Note:
Copy the Client ID, Client Secret, and API Hostname values from your Web SDK application into the duo.conf file.

If your login URL is https://yourdomain.com/login then the redirect will be https://yourdomain.com/duo-callback
Although it is not mentioned in the official documentation,  please make sure you use HTTPS for your project as call back returns to the HTTPS URL.
 

  1. Add integration code to AppController.php

<?php

/**

 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)

 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)

 *

 * Licensed under The MIT License

 * For full copyright and license information, please see the LICENSE.txt

 * Redistributions of files must retain the above copyright notice.

 *

 * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)

 * @link      http://cakephp.org CakePHP(tm) Project

 * @since     0.2.9

 * @license   http://www.opensource.org/licenses/mit-license.php MIT License

 */

namespace App\Controller;

 

use Cake\Controller\Controller;

use Cake\Event\Event;

use Cake\ORM\TableRegistry;

use Cake\Core\Configure;

use Cake\Routing\Router;

use Duo\DuoUniversal\Client;

use Duo\DuoUniversal\DuoException;

 

/**

 * Application Controller

 *

 * Add your application-wide methods in the class below, your controllers

 * will inherit them.

 *

 * @link http://book.cakephp.org/3.0/en/controllers.html#the-app-controller

 */

class AppController extends Controller

{

    /**

     * Initialization hook method.

     *

     * Use this method to add common initialization code like loading components.

     *

     * e.g. `$this->loadComponent('Security');`

     *

     * @return void

     */

    public function initialize()

    {

        parent::initialize();

 

        $this->loadComponent('RequestHandler');

 

        $this->loadComponent('Auth', [

        'authorize'=> 'Controller',

        'authenticate' => [

            'Form' => [

                // fields used in login form

                'fields' => [

                    'username' => 'username',

                    'password' => 'password'

                ]

            ]

        ],

        // login Url

        'loginAction' => [

            'controller' => 'Users',

            'action' => 'login'

        ],

        // After login Url

        'loginRedirect' => [

            'controller' => 'Pages',

            'action' => 'index'

        ],

        // where to be redirected after logout

        'logoutRedirect' => [

            'controller' => 'Users',

            'action' => 'login'

        ],

        // if unauthorized user go to an unallowed action he will be redirected to this url

        'unauthorizedRedirect' => [

            'controller' => 'Users',

            'action' => 'login',

        ],

        'authError' => 'Did you really think you are allowed to see that?',

        ]);

 

        $this->Auth->allow(['logout','duoCallback']);

 

    }

 

    public function beforeFilter(Event $event)

    {

        if ($this->request->session()->read('Auth.User')) {

        }

        else{

            $user_query = $this->Users->find('all')->where(['email'=>$userEmailInputFromLoginForm['email'],'is_active'=>1,'is_user'=>1,'is_deleted'=>0])->contain(['UserRoles']);

            

            $user = $user_query->toArray();

            if(!empty($user)){                

                require_once ROOT. DS .'vendor'. DS .'duosecurity'.DS.'duo_universal_php' . DS . 'src'. DS .'Client.php';

                require_once ROOT. DS .'vendor'. DS .'duosecurity'.DS.'duo_universal_php' . DS . 'src'. DS .'DuoException.php';

                    'DuoException.php';

                $duoconfig = parse_ini_file(

                    ROOT . DS . 'config' . DS . 'duo.conf'

                );

                

                try {

                    $duo_client = new Client(

                        $duoconfig['client_id'],

                        $duoconfig['client_secret'],

                        $duoconfig['api_hostname'],

                        $duoconfig['redirect_uri']

                    );

                } catch (DuoException $e) {

                    throw new ErrorException("*** Duo config error. Verify the values in duo.conf are correct ***\n" . $e->getMessage());

                }

                

                $duo_failmode = strtoupper($duoconfig['failmode']);

                try {

                    $duo_client->healthCheck();                        

                } catch (DuoException $e) {

                    $logger->error($e->getMessage());

                    if ($duo_failmode == "OPEN") {

                        # If we're failing open, errors in 2FA still allow for success

                        $this->Flash->error(__('Login Successful, but 2FA Not Performed. Confirm Duo client/secret/host values are correct'));

                        return $this->redirect($this->Auth->redirectUrl(['controller' => 'prequals','action' => 'index']));

                    } else {

                        # Otherwise the login fails and redirect user to the login page

                        $this->Flash->error(__('2FA Unavailable. Confirm Duo client/secret/host values are correct'));

                        return  $this->redirect($this->Auth->logout());

                    }

 

                }

                # Generate random string to act as a state for the exchange.

                # Store it in the session to be later used by the callback.

                $duostate = $duo_client->generateState();

                

                $user[0]['duostate']=$duostate;

 

                # Redirect to prompt URI which will redirect to the client's redirect URI after 2FA

                $prompt_uri = $duo_client->createAuthUrl($user[0]->username, $duostate);                    

                $user = $user[0]; 

 

                $this->Auth->setUser($user);

                $this->redirect($prompt_uri);

            }

            else{

                if($this->request->here != '/users/logout'){

                    $this->Flash->error(__('Invalid User'));

                }

                if($this->request->here != '/' && $this->request->here != '/users/login'){

                    return $this->redirect($this->Auth->redirectUrl(['controller'=>'users','action' => 'login']));

                }

            }

        }

        

    }

 

    /**

     * Before render callback.

     *

     * @param \Cake\Event\Event $event The beforeRender event.

     * @return \Cake\Network\Response|null|void

     */

  

    public function duoCallback() {

        $this->autoRender = false;

        # Get authorization token to trade for 2FA

        $code = $this->request->query["duo_code"];

 

        # Get state to verify consistency and originality

        $state=$this->request->query['state'];

    

        if ($this->request->session()->read('Auth.User')) {

 

            # Retrieve the previously stored state and username from the session

            $username = $this->Auth->user('username');

            $saved_state = $this->Auth->user('duostate');            

            

            if (empty($saved_state) || empty($username)) {

                $this->Flash->error(__('No saved state please login again'));

                return  $this->redirect($this->Auth->logout());

            }

 

            // # Ensure nonce matches from initial request

            if ($state != $saved_state) {

                $this->Flash->error(__('Duo state does not match saved state'));

                return $this->redirect($this->Auth->redirectUrl(['controller'=>'users','action' => 'login']));

            }

            require_once ROOT. DS .'vendor'. DS .'duosecurity'.DS.'duo_universal_php' . DS . 'src'. DS .'Client.php';

            require_once ROOT. DS .'vendor'. DS .'duosecurity'.DS.'duo_universal_php' . DS . 'src'. DS .'DuoException.php';

                'DuoException.php';

            $duoconfig = parse_ini_file(

                ROOT . DS . 'config' . DS . 'duo.conf'

            );

            

            try {

                $duo_client = new Client(

                    $duoconfig['client_id'],

                    $duoconfig['client_secret'],

                    $duoconfig['api_hostname'],

                    $duoconfig['redirect_uri']

                );

            } catch (DuoException $e) {

                throw new ErrorException("*** Duo config error. Verify the values in duo.conf are correct ***\n" . $e->getMessage());

            }

            if ($state == $saved_state) {

                try {

                    $decoded_token = $duo_client->exchangeAuthorizationCodeFor2FAResult(

                        $code,

                        $username

                    );

                } catch (DuoException $e) {

                    $this->Flash->error(__('Error decoding Duo result. Confirm device clock is correct.'));

                    return $this->redirect($this->Auth->redirectUrl(['controller'=>'users','action' => 'login']));

                }                

            }

 

            # Exchange happened successfully so render success page

            return $this->redirect($this->Auth->redirectUrl(['controller' => 'pages','action' => 'index']));

            

        }else{

            $this->Auth->logout();

        }

 

    }

}

     

  1. That’s all. Run your application, enter your credentials, and then it should redirect you to the Duo 2FA screen

how to do two factor authentication with cake php

how to do two factor authentication with cake php form

 

how to do two factor authentication with cake php form submission
 

So there you have it! Here’s how to do two factor authentication with Cake PHP3. 

Shreya Sangaonkar
Shreya Sangaonkar
Tech Lead
Implementing edit records in multiple associated tables in Cakephp 3

Implementing edit records in multiple associated tables in Cakephp 3

Nikhil Kamath
Selenium vs Cypress: What's the Difference?

THE MODERN TOOL: CYPRESS

Deepraj Naik
Quality Risk Analysis Hackathon

Quality Risk Analysis Hackathon

LAVINA FARIA