Intégrer l’API OAuth2 de Google avec CakePHP v3

Nous avons tous déjà vu ce système : au lieu de s’authentifier par un couple login/mot de passe, certains sites proposent de se connecter à l’aide de son compte Google+ ou Facebook.

Ce tutorial va vous expliquer comment utiliser l’API OAuth2 de Google pour proposer cette fonctionnalité à votre site WEB, tout en conservant les mécanismes d’authentification « classique » de CakePHP.

Nous utiliserons la version 3 de CakePHP, cette version étant destinée à être généralisée. Mais la modification vers CakePHP v2.6 est relativement facile. Dans cet article nous ne PARLERONS PAS de l’authentification « classique » par formulaire, la doc officielle est suffisamment détaillée pour cela. Sinon reportez-vous au forum Francophone http://forum.cakephp-fr.org/

Le concept de base est le suivant : lorsque l’utilisateur veut s’authentifier avec son compte Google, nous interrogeons Google avec une Clef API et un Code Projet . En retour, si l’utilisateur s’est bien authentifié chez Google, une action de notre controller est appelée avec les informations de l’utilisateur.

Mise à jour du 22/05/2015 : J’ai rajouté le code pour la déconnexion.

Pré-requis

Un site CakePHP fonctionnel, avec base de donnée paramétrée et accessible.

Un compte développeur Google, ça se passe sur cette page : https://console.developers.google.com/project

Téléchargement et installation de la libraire Google OAuth2

1ere étape, l’installation de la librairie Google qui déclare toutes les classes OAuth2. Par chance, Google a développé une librairie vraiment simple, qui fait (presque) tout le boulot d’authentification.

Comme nous sommes des développeurs poilus, on va intégrer la librairie par le composer de notre installation de CakePHP. De cette façon, avec un simple composer -update nous récupérerons toutes les sources du client PHP.

Rajouter dans votre composer.json cette ligne pour le téléchargement de la librairie Google :

"google/apiclient": "1.0.*@beta"

dans le « require-dev » :

"require-dev": {
"psy/psysh": "@stable",
"cakephp/debug_kit": "~3.0",
"cakephp/bake": "~1.0",
"google/apiclient": "1.0.*@beta"
}

Nous allons ensuite indiquer à notre composer de charger automatiquement la librairie en rajoutant cette ligne :

"Google\\": "./vendor/google/apiclient"

dans le « autoload »

"autoload": {
"psr-4": {
"App\\": "src",
"Google\\": "./vendor/google/apiclient"
}
}

C’est fait, maintenant un simple « composer -update » va télécharger et installer la librairie OAuth2 de Google. Vérifier bien que la librairie s’est bien installée dans /vendor/google.

Création d’une clef API développeur Google

La création de la clef API est une étape obligatoire. Nous allons déclarer à Google notre application pour qu’elle puisse avoir les droits d’interroger le « Cloud » Google. C’est dans cette partie également que nous indiquons à Google la page URL de retour lorsque nous faisons une demande d’authentification.

La déclaration de notre projet se passe sur la console développeur à cette adresse :

https://console.developers.google.com/project

1ere étape, création d’un projet, nous allons indiquer les informations suivantes :

google-console-dev

 

Une fois le projet nous créons une autorisation. En cliquant sur « Ecran autorisation », il suffit juste ensuite de saisir son adresse courriel, et d’indiquer un nom de produit (mettez ce que vous voulez dans le nom du produit) :

google-console-dev-1

 

Une fois l’autorisation créée, nous pourrons générer la clef pour notre application, en cliquant sur « Identifiants » puis « Créer un identifiant client » :

google-console-dev-2

Maintenant, nous allons spécifier que nous utiliserons une application WEB pour interroger le Cloud Google, nous indiquons également notre URL de retour, dans mon cas à moi, je mets « http://localhost:8086/users/google_login » parce que mon serveur est en local sur mon poste, et que mon serveur est configuré pour écouter sur le port 8086. Dans votre cas je vous laisse deviner ce qu’il faut mettre :

google-console-dev-3

Par contre, j’ai placé /users/google_login, mais vous pouvez mettre ce que vous voulez, du moment que cela respectera votre code dans le controller et l’action CakePHP.

Cliquez ensuite sur « Créer un identifiant client ». Vous devriez voir apparaitre les informations confidentielles qui seront déclarées dans notre application CakePHP :

google-console-dev-4

Notez bien ces informations car elles sont capitales dans la suite de l’article. Voilà le plus dur est fait ! 🙂 Passons maintenant à la configuration de CakePHP.

Paramétrage Authentification CakePHP

Préparer la base de données

Nous allons créer une table pour stocker à la fois les utilisateurs qui seront déclarés en interne dans notre application et les utilisateurs qui se connecteront avec Google. Il est donc nécessaire, si on veut faire les choses bien, de créer une table avec des éléments présents dans le profil fournit par Google.

Par expérience, cette table fait parfaitement l’affaire :

+------------+--------------+------+-----+---------+----------------+

| Field      | Type         | Null | Key | Default | Extra          |

+------------+--------------+------+-----+---------+----------------+

| id         | int(11)      | NO   | PRI | NULL    | auto_increment |

| first_name | varchar(60)  | YES  |     | NULL    |                |

| last_name  | varchar(60)  | YES  |     | NULL    |                |

| email      | varchar(80)  | YES  | MUL | NULL    |                |

| password   | varchar(64)  | YES  |     | NULL    |                |

| social_id  | varchar(45)  | YES  |     | NULL    |                |

| link       | varchar(255) | YES  |     | NULL    |                |

| avatar     | varchar(100) | YES  |     | NULL    |                |

| created    | datetime     | YES  |     | NULL    |                |

| updated    | datetime     | YES  |     | NULL    |                |

+------------+--------------+------+-----+---------+----------------+

Les plus aguerris d’entre vous auront constaté que nous n’utiliserons pas le champ « username » pour authentifier nos utilisateurs. Nous emploierons plutôt le champ « email » pour simplifier la compatibilité entre l’authentification interne et l’authentification Google.

id, first_name, last_name, email, password, created, modified : tout ça c’est connu.

Quelques explications pour les autres champs :

  • social_id : permet d’identifier le numéro unique donné par Google
  • link : lien vers le profil Google de l’utilisateur
  • avatar : lien vers l’image de l’utilisateur Google

AppController et paramétrage du composant Auth

Paraîtrons maintenant notre composant « Auth », si vous avez bien suivi le tuto dans la documentation officielle, vous devriez avoir ceci :

/**
 * Initialization hook method.
 *
 * Use this method to add common initialization code like loading components.
 *
 * @return void
 */
public function initialize()
        {
            $this->loadComponent('Flash');
            $this->loadComponent('Auth', [
                'authenticate'         => [
                    'Form' => [
                        'fields' => [
                            'username' => 'email',
                            'password' => 'password'
                        ]
                    ]
                ],
                'authError'            => __('Vous ne possédez pas l\'autorisation d\'accéder à cette page'),
                'authorize'            => ['Controller'],
                'unauthorizedRedirect' => [
                    'controller' => 'Users',
                    'action'     => 'forbidden'
                ],
                'loginRedirect'        => [
                    'controller' => 'Dashboards',
                    'action'     => 'index'
                ],
                'logoutRedirect'       => [
                    'controller' => 'Users',
                    'action'     => 'login'
                ]
            ]);

nous indiquons au composant Auth que le champ utilisé pour l’authentification « username » est en fait le champ « email » dans notre table. Le loginRedirect pointe vers une action d’un controller interne qui sera appelé une fois l’authentification réussie, mettez ce que vous voulez du moment que l’action est valide.

Création et paramétrage du UsersController

Nous attaquons maintenant le dur du code. C’est dans le UsersController que nous initierons les requêtes vers les serveurs Google et que nous lirons la réponse de Google. C’est également dans ce controller que nous déclarerons nos clefs secrètes générées tout à l’heure.

Déclarons nos clefs :

define('GOOGLE_OAUTH_CLIENT_ID', '713787601734-nsdpv93lku7js2fkt9srkj5ona0qkbem.apps.googleusercontent.com');
define('GOOGLE_OAUTH_CLIENT_SECRET', 'xxxxxxxxxxxxxxxx');
define('GOOGLE_OAUTH_REDIRECT_URI', 'http://localhost:8086/users/google_login');

J’utilise une constante PHP, mais vous êtes libre d’utiliser des variables dans la classe UsersController.

Ensuite nous importons les 2 classes clientes de base Google :

use \Cake\Network\Exception;
use Cake\Event\Event;
use Cake\Utility\Text;
use Google_Client;
use Google_Service_Oauth2;

D’où l’intérêt de les mettre dans l’autoload de notre composer.json.

Puis nous allons créer l’action de login vers Google. C’est cette action qui sera appelée quand l’utilisateur cliquera sur le bouton « s’authentifier avec Google+ » :

        public function googlelogin()
        {
            $client = new Google_Client();
            $client->setClientId(GOOGLE_OAUTH_CLIENT_ID);
            $client->setClientSecret(GOOGLE_OAUTH_CLIENT_SECRET);
            $client->setRedirectUri(GOOGLE_OAUTH_REDIRECT_URI);

            $client->setScopes(array(
                "https://www.googleapis.com/auth/userinfo.profile",
                'https://www.googleapis.com/auth/userinfo.email'
            ));
            $url = $client->createAuthUrl();
            $this->redirect($url);
        }

Explications :

Nous instancions la classe Google_Client dans $client. Nous lui déclarons nos paramètres de clefs et l’url de redirection.

Ensuite nous spécifions à Google notre champ de recherche (setScope), pour avoir la liste des champs de recherche disponibles rendez-vous à cette page :

https://developers.google.com/+/api/oauth

Maintenant, nous allons créer la fonction de retour « callback » qui va interpréter la réponse Google, les explications sont données dans les commentaires :

public function google_login()
{
$client = new Google_Client();
/* Création de notre client Google */
            $client->setClientId(GOOGLE_OAUTH_CLIENT_ID);
            $client->setClientSecret(GOOGLE_OAUTH_CLIENT_SECRET);
            $client->setRedirectUri(GOOGLE_OAUTH_REDIRECT_URI);

            $client->setScopes(array(
                "https://www.googleapis.com/auth/userinfo.profile",
                'https://www.googleapis.com/auth/userinfo.email'
            ));
            $client->setApprovalPrompt('auto');

/* si dans l'url le paramètre de retour Google contient 'code' */
            if (isset($this->request->query['code'])) {
// Alors nous authentifions le client Google avec le code reçu
                $client->authenticate($this->request->query['code']);
// et nous plaçons le jeton généré en session
                $this->request->Session()->write('access_token', $client->getAccessToken());
            }

/* si un jeton est en session, alors nous le plaçons dans notre client Google */
            if ($this->request->Session()->check('access_token') && ($this->request->Session()->read('access_token'))) {
                $client->setAccessToken($this->request->Session()->read('access_token'));
            }
/* Si le client Google a bien un jeton d'accès valide */
            if ($client->getAccessToken()) {
// alors nous écrivons le jeton d'accès valide en session
                $this->request->Session()->write('access_token', $client->getAccessToken());
// nous créons une requête OAuth2 avec le client Google paramétré
                $oauth2 = new Google_Service_Oauth2($client);
// et nous récupérons les informations de l'utilisateur connecté
                $user = $oauth2->userinfo->get();
                try {
                    if (!empty($user)) {
// si l'utilisateur est bien déclaré, nous vérifions si dans notre table Users il existe l'email de l'utilisateur déclaré ou pas
                        $result = $this->Users->find('all')
                                              ->where(['email' => $user['email']])
                                              ->first();
                        if ($result) {
// si l'email existe alors nous déclarons l'utilisateur comme authentifié sur CakePHP
                            $this->Auth->setUser($result->toArray());
// et nous redirigeons vers la page de succès de connexion
                            $this->redirect($this->Auth->redirectUrl());
                        } else {
// si l'utilisateur n'est pas dans notre utilisateur, alors nous le créons avec les informations récupérées par Google+
                            $data = array();
                            $data['email'] = $user['email'];
                            $data['first_name'] = $user['givenName'];
                            $data['last_name'] = $user['familyName'];
                            $data['social_id'] = $user['id'];
                            $data['avatar'] = $user['picture'];
                            $data['link'] = $user['link'];
                            $data['uuid'] = Text::uuid();
                            $entity = $this->Users->newEntity($data);
                            if ($this->Users->save($entity)) {
// et ensuite nous déclarons l'utilisateur comme authentifié sur CakePHP
                                $data['id'] = $entity->id;
                                $this->Auth->setUser($data);
                                $this->redirect($this->Auth->redirectUrl());
                            } else {
                                $this->Flash->set('Erreur de connection');
// et nous redirigeons vers la page de succès de connexion
                                $this->redirect(['action' => 'login']);
                            }
                        }
                    } else {
// si l'utilisateur n'est pas valide alors nous affichons une erreur
                        $this->Flash->set('Erreur les informations Google n\'ont pas été trouvée');
                        $this->redirect(['action' => 'login']);
                    }
                } catch (\Exception $e) {
                    $this->Flash->set('Grosse erreur Google, ca craint');
                    return $this->redirect(['action' => 'login']);
                }
            }
        }
}

Et voilà, tout est terminé, ne reste plus qu’a créer le formulaire de connexion !

Création du formulaire de Login

On va faire un formulaire simple, avec juste un champ « email », un champ « password » (pour l’authentification interne), un bouton « connecter » et un bouton « se connecter avec Google+ », c’est trivial :

<?php
    $myTemplates = [
        'inputContainer' => '{{content}}',
        'input'          => '<input type="{{type}}" name="{{name}}" {{attrs}}>',

    ];
    $this->Form->templates($myTemplates);
?>

<?= $this->Form->create('user', [
    'role'  => 'form-role',
    'class' => 'form-login'
]);
?>
    <h2 class="form-login-heading"><?= __('Connection') ?></h2>
    <div class="login-wrap">
        <?php echo $this->Flash->render('auth'); ?>
        <?php echo $this->Flash->render(); ?>

        <?= $this->Form->input('email', ['label'       => FALSE,
                                            'div'         => FALSE,
                                            'class'       => 'form-control',
                                            'placeholder' => 'Email'
        ]); ?>
        <br/>
        <?= $this->Form->input('password', ['label'       => FALSE,
                                            'div'         => FALSE,
                                            'class'       => 'form-control',
                                            'placeholder' => __('Mot de passe')
        ]); ?>
        <br/>
        <?= $this->Form->button('<i class="fa fa-lock"></i>' . __(' SE CONNECTER'), ['class' => 'btn btn-theme btn-block']) ?>
        <br/>
        <a class="btn btn-block google btn-danger" href="<?= $this->Url->build(['action' => 'googlelogin']); ?>"> <i
                class="fa fa-google-plus modal-icons"></i> Se connecter avec Google+ </a>
    </div>
<?= $this->Form->end(); ?>

Vous aurez constaté que j’utilise Bootstrap, un « bout d’code » a été écrit sur ce sujet.

Pour la déconnexion, le code est très simple, il faut supprimer la variable de session Google ET utiliser le mécanisme de connexion CakePHP :

        public function logout()
        {
            $this->request->session()
                          ->destroy('access_token');
            $this->Flash->success('Vous êtes bien déconnecté');
            return $this->redirect($this->Auth->logout());
        }

Test

Lançons notre application, la fenêtre de connexion s’affiche :

google+-1

En cliquant sur « se connecte avec Google+ », CakePHP considère que je suis connecté, et avec un petit bout de code on peut même afficher l’avatar du profil 🙂

google+-2
google+-3

 

Voilà, votre application est maintenant Google+ friendly ! si vous rencontrez un soucis, n’hésitez pas à me contacter sur le forum Francophone.

Bon courage pour la mise en oeuvre !

 

Comments: 6

  1. SpamKey says:

    Très bon tuto ! Le seul (pratiquement) en français et qui marche 😉

    Merci !

  2. Fred says:

    Salut,
    Excellent tuto, je ne l’ai pas encore mis en pratique mais les explications sont claires et détaillées.
    Une question en revanche.
    Si j’ai bien compris, un utilisateur qui se connectera la 1ère fois avec son compte Google devra systématiquement se reconnecter ultérieurement avec son compte Google, puisque la procédure ne crée pas de mot de passe dans la table (ce qui expliquerait que la colonne « password » soit facultative).
    Je ne me trompe pas ?

  3. Alexis says:

    Merci pour l’excellent boulot. J’ai quelques questions.

    – Le champs « social_id » ne risque-t-il pas d’être reconnu comme une foreign key vers la table « socials » (qui n’existe pas) lors du lancement du « bake » ? Est-il souhaitable de l’écrire tel quel ou peut-on lui préférer une autre forme ? Comme par exemple « social_id_google » ?

    – La ligne 55 contient $data[‘uuid’]. Quid de cet identifiant RFC 4122 ? Je ne l’ai pas vu dans le model…

    – Si on garde le nom « social_id », après le lancement du « bake » (model, view et controller), le « form » utilisé par défaut lors de la construction des vues reconnait le champ « social_id » comme une liste de choix. Pour l’en empêcher, il faudrait certainement forcer le type dans le
    use Cake\Database\Schema\Table as Schema;
    class UsersTable extends Table
    {
    protected function _initializeSchema(Schema $schema)
    {
    $schema->columnType(‘social_id’, ‘string’);
    return $schema;
    }
    }
    C’est par purisme. Car ce genre de champs n’est pas à modifier avec le template par défaut utilisé par bake.

  4. Knout says:

    Excellent. ?

  5. carl miguel bartolome says:

    Good day i had a hard time finding the error
    It does not redirect to my dashboard page after login in gmaIl
    BTW im using cakephp 3.6

    public function google_login() {

    $client = new Google_Client();
    $client->setClientId(GOOGLE_OAUTH_CLIENT_ID);
    $client->setClientSecret(GOOGLE_OAUTH_CLIENT_SECRET);
    $client->setRedirectUri(GOOGLE_OAUTH_REDIRECT_URI);

    $client->setScopes(array(
    ‘https://www.googleapis.com/auth/userinfo.profile’,
    ‘https://www.googleapis.com/auth/userinfo.email’
    ));

    $client->setApprovalPrompt(‘auto’);

    if (!empty($this->request->getQuery(‘code’))) {
    $client->authenticate($this->request->getQuery(‘code’));
    $this->request->Session()->write(‘access_token’, $client->getAccessToken());
    }
    if ($this->request->Session()->check(‘access_token’) && ($this->request->Session()->read(‘access_token’))) {
    $client->setAccessToken($this->request->Session()->read(‘access_token’));
    }
    if ($client->getAccessToken()) {
    $this->request->Session()->write(‘access_token’, $client->getAccessToken());
    $oauth2 = new Google_Service_Oauth2($client);
    $user = $oauth2->userinfo->get();
    try {
    if (!empty($user)) {
    $result = $this->Admins->find(‘all’)
    ->where([’email’ => $user[’email’]])
    ->first();
    if ($result) {
    // si l’email existe alors nous déclarons l’utilisateur comme authentifié sur CakePHP
    $this->Auth->setUser($result->toArray());
    // et nous redirigeons vers la page de succès de connexion
    $this->redirect($this->Auth->redirectUrl());
    } else {
    // si l’utilisateur n’est pas dans notre utilisateur, alors nous le créons avec les informations récupérées par Google+
    $data = array();
    $data[’email’] = $user[’email’];
    $data[‘fname’] = $user[‘givenName’];
    $data[‘lname’] = $user[‘familyName’];
    $data[‘social_id’] = $user[‘id’];
    $data[‘avatar’] = $user[‘picture’];
    $data[‘link’] = $user[‘link’];
    $data[‘uuid’] = Text::uuid();
    $entity = $this->Admins->newEntity($data);
    if ($this->Admins->save($entity)) {
    // et ensuite nous déclarons l’utilisateur comme authentifié sur CakePHP
    $data[‘id’] = $entity->id;
    $this->Auth->setUser($data);
    $this->redirect($this->Auth->redirectUrl());
    } else {
    $this->Flash->set(‘Erreur de connection’);
    // et nous redirigeons vers la page de succès de connexion
    $this->redirect([‘action’ => ‘login’]);
    }
    }

    } else {
    $this->Flash->error(‘Unable to retrieve google data’);
    return $this->redirect([‘action’ => ‘login’]);
    }
    } catch (\Exception $e) {
    $this->Flash->error($e);
    return $this->redirect([‘action’ => ‘login’]);
    }
    }
    }

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *