Yii2: Authentication with Google and Facebook


This is continuation from https://takdekeje.kuceng.my/2018/08/yii2-validate-email-after-registration.html.

Objective:
  1. Register and login with Google and Facebook account.
  2. Link existing account with Google and Facebook.
  3. Unlink  existing account from Google and Facebook.
You may truncate both tables to start fresh.

Part 1: Initial setup

  1. Install AuthClient Extension.
    composer require "yiisoft/yii2-authclient:*"
  2. Create new migration to update DB
    yii migrate/create authclient
  3. Edit the migration file:
    <?php
    
    use yii\db\Migration;
    
    /**
     * Class m180926_083206_auth
     */
    class m180926_083206_auth extends Migration
    {
        /**
         * {@inheritdoc}
         */
        public function safeUp()
        {
    
        }
    
        /**
         * {@inheritdoc}
         */
        public function safeDown()
        {
            echo "m180926_083206_auth cannot be reverted.\n";
    
            return false;
        }
    
        
    
    // Use up()/down() to run migration code without a transaction. public function up() { $this->createTable('auth', [ 'id' => 'bigint UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT', 'uid' => 'bigint UNSIGNED NOT NULL', 'source' => $this->string()->notNull(), 'source_id' => $this->string()->notNull(), ]); } public function down() { $this->dropTable('auth'); }
    }
  4. Run the migration.

    yii migrate up
    Migration error? Don't blindly copy paste the whole code. Understand it, then you can modify the blue part.
  5. Generate model for table auth using gii. 
  6. Create component AuthHandler
    <?php
    namespace app\components;
    
    use app\models\Auth;
    use app\models\Users;
    use Yii;
    use yii\authclient\ClientInterface;
    use yii\helpers\ArrayHelper;
    
    /**
     * AuthHandler handles successful authentication via Yii auth component
     */
    class AuthHandler
    {
        /**
         * @var ClientInterface
         */
        private $client;
        private $client_id;
    
        public function __construct(ClientInterface $client)
        {
            $this->client = $client;
            $this->client_id = $client->id;
        }
    
        public function handle()
        {
            $attributes = $this->client->getUserAttributes();
            
            switch($this->client_id) {
                case 'google':
                    $email = ArrayHelper::getValue($attributes, 'emails.0.value');
                    $id = ArrayHelper::getValue($attributes, 'id');
                    break;
                default:
                    $email = ArrayHelper::getValue($attributes, 'email');
                    $id = ArrayHelper::getValue($attributes, 'id');
            }
            
            /* @var Auth $auth */
            $auth = Auth::find()->where([
                'source' => $this->client->getId(),
                'source_id' => $id,
            ])->one();
    
            if (Yii::$app->user->isGuest) {
                if ($auth) { // login
                    /* @var User $user */
                    $user = Users::find()->where(['id' => $auth->uid])->one();
                    Yii::$app->user->login($user);
                } else { // signup
                    if ($email !== null && Users::find()->where(['email' => $email])->exists()) {
                        Yii::$app->getSession()->setFlash('error', [
                            Yii::t('app', "User with the same email as in {client} account already exists but isn't linked to it. Login using email first to link it.", ['client' => $this->client->getTitle()]),
                        ]);
                    } else {
                        $password = Yii::$app->security->generateRandomString(8);
                        $user = new Users([
                            'email' => $email,
                            'password' => $password,
                            'password_repeat' => $password,
                            'confirmed_at' => date('Y-m-d H:i:s'),
                        ]);
                        
                        $transaction = Users::getDb()->beginTransaction();
    
                        if ($user->save()) {
                            $auth = new Auth([
                                'uid' => $user->id,
                                'source' => $this->client->getId(),
                                'source_id' => (string)$id,
                            ]);
                            if ($auth->save()) {
                                $transaction->commit();
                                Yii::$app->user->login($user);
                                $user->last_login_at = date('Y-m-d H:i:s');
                                $user->last_login_ip = \Yii::$app->request->userIP;
                                $user->save(0);
                                Yii::$app->getSession()->setFlash('success', 'Thank you, registration is now complete. If you want to login directly, this is your password: '.$password);
                            } else {
                                Yii::$app->getSession()->setFlash('error', 'Unable to save '.$this->client->getTitle().' account: '.json_encode($auth->getErrors()));
                            }
                        } else {
                            Yii::$app->getSession()->setFlash('error', 'Unable to save user: '.json_encode($user->getErrors()));
                        }
                    }
                }
            } else { // user already logged in
                if (!$auth) { // add auth provider
                    $auth = new Auth([
                        'uid' => Yii::$app->user->id,
                        'source' => $this->client->getId(),
                        'source_id' => (string)$attributes['id'],
                    ]);
                    if ($auth->save()) {
                        Yii::$app->getSession()->setFlash('success', 'Successfully linked '.$this->client->getTitle().' account.');
                    } else {
                        Yii::$app->getSession()->setFlash('error', 'Unable to link '.$this->client->getTitle().' account: '.json_encode($auth->getErrors()));
                    }
                } else { // there's existing auth
                    Yii::$app->getSession()->setFlash('error', 'Unable to link '.$this->client->getTitle().' account. There is another user using it.');
                }
            }
        }
    }
  7. Modify SiteController
    <?php
    
    namespace app\controllers;
    
    use Yii;
    use yii\filters\AccessControl;
    use yii\web\Controller;
    use yii\web\Response;
    use yii\filters\VerbFilter;
    use app\models\LoginForm;
    use app\models\ContactForm;
    
    use app\components\AuthHandler;
    class SiteController extends Controller { /** * {@inheritdoc} */ public function behaviors() { return [ 'access' => [ 'class' => AccessControl::className(), 'only' => ['logout'], 'rules' => [
    [ // auth action need to be public accessible 'actions' => ['auth'], 'allow' => true, 'roles' => ['*'], ],[
    'actions' => ['logout'], 'allow' => true, 'roles' => ['@'], ], ], ], 'verbs' => [ 'class' => VerbFilter::className(), 'actions' => [ 'logout' => ['post'], ], ], ]; } /** * {@inheritdoc} */ public function actions() { return [ 'error' => [ 'class' => 'yii\web\ErrorAction', ], 'captcha' => [ 'class' => 'yii\captcha\CaptchaAction', 'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null, ],
    'auth' => [ 'class' => 'yii\authclient\AuthAction', 'successCallback' => [$this, 'onAuthSuccess'], ],
    ]; }
    public function onAuthSuccess($client) { (new AuthHandler($client))->handle(); return $this->redirect(["site/index"]); }
    /** * Displays homepage. * * @return string */ public function actionIndex() { return $this->render('index'); } /** * Login action. * * @return Response|string */ public function actionLogin() { if (!Yii::$app->user->isGuest) { return $this->goHome(); } $model = new LoginForm(); if ($model->load(Yii::$app->request->post()) && $model->login()) { return $this->goBack(); } $model->password = ''; return $this->render('login', [ 'model' => $model, ]); } /** * Logout action. * * @return Response */ public function actionLogout() { Yii::$app->user->logout(); return $this->goHome(); } /** * Displays contact page. * * @return Response|string */ public function actionContact() { $model = new ContactForm(); if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) { Yii::$app->session->setFlash('contactFormSubmitted'); return $this->refresh(); } return $this->render('contact', [ 'model' => $model, ]); } /** * Displays about page. * * @return string */ public function actionAbout() { return $this->render('about'); } }
Part 2: Google
  1. Create project at https://console.developers.google.com/project
    Note the Project ID
  2. Setup credentials at https://console.developers.google.com/project/[yourProjectId]/apiui/credential
    If you click the link, replace [yourProjectId] with your Project ID in the URL bar!


  3. You need Client ID and Client secret of your project. Click on the project's name
  4. Enable Google+ API at https://console.developers.google.com/project/[yourProjectId]/apiui/api/plus
    Again, if you just click on the link, change [yourProjectId] to your actual Project ID in the URL bar.

    If you got "blank screen", try to click on the main menu APIs & Services, until you can find something to enable APIs and Services. You might see a lot of API library. Look for Google+ API and enable it.
  5. In config/web.php, add authClientCollection under components

    'components' => [
         
    'authClientCollection' => [
              'class' => 'yii\authclient\Collection',
              'clients' => [
                  'google' => [
                      'class' => 'yii\authclient\clients\Google',
                      'clientId' => 'google_client_id',
                      'clientSecret' => 'google_client_secret',
                  ],
              ],
          ]
          // ...   ]
  6. Modify view users/create and site/login to have button to login with Google.
    <?php
    
    use yii\helpers\Html;
    use yii\widgets\ActiveForm;
    
    
    /* @var $this yii\web\View */
    /* @var $model app\models\Users */
    /* @var $form ActiveForm */
    
    $this->title = 'Sign up';
    $this->params['breadcrumbs'][] = $this->title;
    
    ?>
    <div class="row">
        <div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title"><?= Html::encode($this->title) ?></h3>
                </div>
                <div class="panel-body">
                    <?php $form = ActiveForm::begin(); ?>
    
                    <?= $form->field($model, 'email') ?>
    
                    <?= $form->field($model, 'password')->passwordInput() ?>
       
                    <?= $form->field($model, 'password_repeat')->passwordInput() ?>
    
                    <?= Html::submitButton('Sign up', ['class' => 'btn btn-success btn-block']) ?>
    
                    <?php ActiveForm::end(); ?>
                </div>
                
    <?= yii\authclient\widgets\AuthChoice::widget([
                     'baseAuthUrl' => ['site/auth'],
                     'popupMode' => false,
                ]) ?>
            </div>         <p class="text-center">             <?= Html::a('Already registered? Sign in!', ['/site/login']) ?>         </p>     </div> </div>
    <?php
    
    /* @var $this yii\web\View */
    /* @var $form yii\bootstrap\ActiveForm */
    /* @var $model app\models\LoginForm */
    
    use yii\helpers\Html;
    use yii\bootstrap\ActiveForm;
    
    
    $this->title = 'Login';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div class="site-login">
        <h1><?= Html::encode($this->title) ?></h1>
    
        <p>Please fill out the following fields to login:</p>
    
        <?php $form = ActiveForm::begin([
            'id' => 'login-form',
            'layout' => 'horizontal',
            'fieldConfig' => [
                'template' => "{label}\n<div class=\"col-lg-3\">{input}</div>\n<div class=\"col-lg-8\">{error}</div>",
                'labelOptions' => ['class' => 'col-lg-1 control-label'],
            ],
        ]); ?>
    
            <?= $form->field($model, 'email', ['errorOptions' => ['class' => 'help-block' ,'encode' => false]])->textInput(['autofocus' => true]) ?>
    
    
            <?= $form->field($model, 'password')->passwordInput() ?>
    
            <?= $form->field($model, 'rememberMe')->checkbox([
                'template' => "<div class=\"col-lg-offset-1 col-lg-3\">{input} {label}</div>\n<div class=\"col-lg-8\">{error}</div>",
            ]) ?>
    
            <div class="form-group">
                <div class="col-lg-offset-1 col-lg-11">
                    <?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
                    <?= Html::a('Forget Password', ['/users/request'], ['class'=>'btn btn-primary']) ?>
    
                </div>
            </div>
    
        <?php ActiveForm::end(); ?>
    
        
    <?= yii\authclient\widgets\AuthChoice::widget([
         'baseAuthUrl' => ['site/auth'],
         'popupMode' => false,
        ]) ?>
    </div>
Now you can register with your Google account and use it to login. If you got redirect_uri_mismatch error, just go to https://console.developers.google.com/project/[yourProjectId]/apiui/credential to add the redirect URI in Authorized redirect URIs.

Part 3: Link/Unlink Account
  1. Modify SiteController (This is optional - To allow redirect back to Link/Unlink page. You also need to add the URL users/link in Authorize redirect URIs in your Google APIs)
    <?php
    
    namespace app\controllers;
    
    use Yii;
    use yii\filters\AccessControl;
    use yii\web\Controller;
    use yii\web\Response;
    use yii\filters\VerbFilter;
    use app\models\LoginForm;
    use app\models\ContactForm;
    use app\components\AuthHandler;
    
    class SiteController extends Controller
    {
        /**
         * {@inheritdoc}
         */
        public function behaviors()
        {
            return [
                'access' => [
                    'class' => AccessControl::className(),
                    'only' => ['logout'],
                    'rules' => [
                        [
         // auth action need to be public accessible
         'actions' => ['auth'],
         'allow' => true,
         'roles' => ['*'],
    ],[
                            'actions' => ['logout'],
                            'allow' => true,
                            'roles' => ['@'],
                        ],
                    ],
                ],
                'verbs' => [
                    'class' => VerbFilter::className(),
                    'actions' => [
                        'logout' => ['post'],
                    ],
                ],
            ];
        }
    
        /**
         * {@inheritdoc}
         */
        public function actions()
        {
            return [
                'error' => [
                    'class' => 'yii\web\ErrorAction',
                ],
                'captcha' => [
                    'class' => 'yii\captcha\CaptchaAction',
                    'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
                ],
                'auth' => [
                    'class' => 'yii\authclient\AuthAction',
                    'successCallback' => [$this, 'onAuthSuccess'],
                ],
            ];
        }
     
        public function onAuthSuccess($client)
        {
            
    if (isset($_GET) && isset($_GET['return'])) { $return = $_GET['return']; } else { $return = "site/index"; }
    (new AuthHandler($client))->handle(); return $this->redirect([$return]); } /** * Displays homepage. * * @return string */ public function actionIndex() { return $this->render('index'); } /** * Login action. * * @return Response|string */ public function actionLogin() { if (!Yii::$app->user->isGuest) { return $this->goHome(); } $model = new LoginForm(); if ($model->load(Yii::$app->request->post()) && $model->login()) { return $this->goBack(); } $model->password = ''; return $this->render('login', [ 'model' => $model, ]); } /** * Logout action. * * @return Response */ public function actionLogout() { Yii::$app->user->logout(); return $this->goHome(); } /** * Displays contact page. * * @return Response|string */ public function actionContact() { $model = new ContactForm(); if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) { Yii::$app->session->setFlash('contactFormSubmitted'); return $this->refresh(); } return $this->render('contact', [ 'model' => $model, ]); } /** * Displays about page. * * @return string */ public function actionAbout() { return $this->render('about'); } }
  2. Modify UsersController
    <?php
    
    namespace app\controllers;
    
    use Yii;
    use app\models\Users;
    use app\models\UsersSearch;
    use yii\web\Controller;
    use yii\web\NotFoundHttpException;
    use yii\filters\VerbFilter;
    use app\models\ResendForm;
    use app\models\RecoveryForm;
    use yii\helpers\Html;
    use app\models\Token;
    
    use app\models\Auth;
    use app\components\AuthHandler;
    /**  * UsersController implements the CRUD actions for Users model.  */ class UsersController extends Controller {     /**      * {@inheritdoc}      */     public function behaviors()     {         return [             'verbs' => [                 'class' => VerbFilter::className(),                 'actions' => [                     'delete' => ['POST'],                 ],             ],         ];     }     /**      * Lists all Users models.      * @return mixed      */     public function actionIndex()     {         $searchModel = new UsersSearch();         $dataProvider = $searchModel->search(Yii::$app->request->queryParams);         return $this->render('index', [             'searchModel' => $searchModel,             'dataProvider' => $dataProvider,         ]);     }     /**      * Displays a single Users model.      * @param string $id      * @return mixed      * @throws NotFoundHttpException if the model cannot be found      */     public function actionView($id)     {         return $this->render('view', [             'model' => $this->findModel($id),         ]);     }     /**      * Creates a new Users model.      * If creation is successful, the browser will be redirected to the 'view' page.      * @return mixed      */     public function actionCreate()     {         $model = new Users();           if (!empty(Yii::$app->request->post())) {               if ($model->register(Yii::$app->request->post())) {                 return $this->render('welcome', ['email' => $model->email]);                 Yii::$app->end();            } else {                return $this->render('create', [                    'model' => $model,                ]);                Yii::$app->end();           }         }         return $this->render('create', [             'model' => $model,         ]);     }     /**      * Updates an existing Users model.      * If update is successful, the browser will be redirected to the 'view' page.      * @param string $id      * @return mixed      * @throws NotFoundHttpException if the model cannot be found      */     public function actionUpdate($id)     {         $model = $this->findModel($id);         if ($model->load(Yii::$app->request->post()) && $model->save()) {             return $this->redirect(['view', 'id' => $model->id]);         }         return $this->render('update', [             'model' => $model,         ]);     }     /**      * Deletes an existing Users model.      * If deletion is successful, the browser will be redirected to the 'index' page.      * @param string $id      * @return mixed      * @throws NotFoundHttpException if the model cannot be found      */     public function actionDelete($id)     {         $this->findModel($id)->delete();         return $this->redirect(['index']);     }     /**      * Finds the Users model based on its primary key value.      * If the model is not found, a 404 HTTP exception will be thrown.      * @param string $id      * @return Users the loaded model      * @throws NotFoundHttpException if the model cannot be found      */     protected function findModel($id)     {         if (($model = Users::findOne($id)) !== null) {             return $model;         }         throw new NotFoundHttpException('The requested page does not exist.');     }         /**      * Confirms user's account. If confirmation was successful logs the user and shows success message. Otherwise      * shows error message.      *      * @param int    $id      * @param string $code      *      * @return string      * @throws \yii\web\HttpException      */     public function actionConfirm($id, $code)     {         $user = Users::findOne($id);         $user->attemptConfirmation($id, $code);         return $this->redirect(['site/index']);     }     /**      * Displays page where user can request new confirmation token. If resending was successful, displays message.      *      * @return string      * @throws \yii\web\HttpException      */     public function actionResend()     {         /** @var ResendForm $model */         $model = new ResendForm;           if (!empty(Yii::$app->request->post())) {             if ($model->load(\Yii::$app->request->post()) && $model->resend()) {                 return $this->redirect(['site/index']);                 Yii::$app->end();             }         }         return $this->render('resend', [             'model' => $model,         ]);     }         /**      * Shows page where user can request password recovery.      *      * @return string      */     public function actionRequest()     {         /** @var RecoveryForm $model */         $model = new RecoveryForm;         $model->scenario = RecoveryForm::SCENARIO_REQUEST;         if (!empty(Yii::$app->request->post())) {             if ($model->load(\Yii::$app->request->post()) && $model->sendRecoveryMessage()) {                 return $this->redirect(['site/index']);                 Yii::$app->end();             }         }         return $this->render('request', [             'model' => $model,         ]);     }     /**      * Displays page where user can reset password.      *      * @param int    $id      * @param string $code      *      * @return string      * @throws \yii\web\NotFoundHttpException      */     public function actionReset($id, $code)     {         /** @var Token $token */         $token = Token::find()->where(['uid' => $id, 'code' => $code, 'type' => Token::TYPE_RECOVERY])->one();         if (!$token || $token->isExpired()) {             \Yii::$app->session->setFlash(                 'danger',                'Recovery link is invalid or expired. Please try '.Html::a('requesting', ['/users/request']).' a new one.'             );             return $this->redirect(['site/login']);             Yii::$app->end();         }         /** @var RecoveryForm $model */         $model = new RecoveryForm;         $model->scenario = RecoveryForm::SCENARIO_RESET;         if (!empty(Yii::$app->request->post())) {             if ($model->load(\Yii::$app->getRequest()->post()) && $model->resetPassword($token)) {                 \Yii::$app->session->setFlash(                     'success',                     'Password has been changed'                 );                 return $this->redirect(['site/login']);                 Yii::$app->end();            }         }         return $this->render('reset', [             'model' => $model,         ]);     }        
    public function actionLink() {
        return $this->render('link');
    }
    public function actionDisconnect() {
        $client = $_POST['client'];
        $auth = Auth::find()->where(['uid' => Yii::$app->user->id, 'source' => $client])->one();
        if ($auth) {
            if ($auth->delete()) {
                \Yii::$app->session->setFlash(
                    'success',
                    'Successfully disconnect account'
                );
            } else {
                \Yii::$app->session->setFlash(
                    'error',
                    'Failed to disconnect account: '.json_encode($auth->getErrors())
                );
            }
        } else {
            \Yii::$app->session->setFlash(
                'error',
                'Cannot find linked account.'
            );
        }
       
        return $this->redirect(['users/link']);
        Yii::$app->end();
    }
    }
  3. Create view users/link.php
    <?php
    
    use yii\helpers\Html;
    use yii\bootstrap\ActiveForm;
    use yii\authclient\widgets\AuthChoice;
    use app\models\Auth;
    
    $this->title = 'Link Your Account';
    $this->params['breadcrumbs'][] = $this->title;
    ?>
    <div>
        <h1><?= Html::encode($this->title) ?></h1>
    
        <?php 
        $user = \Yii::$app->user->identity;
        $authAuthChoice = yii\authclient\widgets\AuthChoice::begin([
            'baseAuthUrl' => ['site/auth'],
            'popupMode' => false,
        ]) ?>
        <table class="table">
            <?php foreach ($authAuthChoice->getClients() as $client): ?>
                <tr>
                    <td style="width: 32px; vertical-align: middle">
                        <?= Html::tag('span', '', ['class' => 'auth-icon ' . $client->getName()]) ?>
                    </td>
                    <td style="vertical-align: middle">
                        <strong><?= $client->getTitle() ?></strong>
                    </td>
                    <td style="width: 120px">
                        <?php $auth = Auth::find()->where(['uid' => $user->id, 'source' => $client->id])->one(); ?>
                        <?= $auth ?
                            Html::a('Disconnect', ['/users/disconnect'], [
                                'class' => 'btn btn-danger btn-block',
                                'data' => [
                                    'method' => 'post',
                                    'params' => ['client' => $client->id],
                                ],
                            ]) :
                            Html::a('Connect', ['/site/auth', 'authclient' => $client->id, 'return' => 'users/link'], [
                                'class' => 'btn btn-success btn-block',
                            ])
                        ?>
                    </td>
                </tr>
            <?php endforeach; ?>
        </table>
    </div>
    Now add a menu to users/link if user is login in views/layout/main.php, or just simply type users/link in URL bar to go there. You can link/unlink your Google account there. Once you linked your Google account, you can use it to login.
Part 4: Facebook
  1. Register at https://developers.facebook.com/apps


  2. When you back in Dashboard, click Set Up on Facebook Login
  3. Select Web
  4. Go to Settings, Basic
  5. There you can obtain ID and Secret values to put into config.
  6. Now you can add Facebook in authClientCollection in config/web.php
        'components' => [
            'authClientCollection' => [
                'class' => 'yii\authclient\Collection',
                'clients' => [
                    'google' => [
                        'class' => 'yii\authclient\clients\Google',
                        'clientId' => 'google_client_id',
                        'clientSecret' => 'google_client_secret',
                    ],
                    
    'facebook' => [
                        'class' => 'yii\authclient\clients\Facebook',
                        'clientId' => 'facebook_app_id',
                        'clientSecret' => 'facebook_app_secret',
                    ]
    ,
                ],         ]     ],
  7. Under Products, Facebook Login, Settings, make sure you enable "Web OAuth Login" and specify "Valid OAuth Redirect URIs" as "https://example.com/auth?authclient=facebook"
There are other external services you can use to authenticate user. See folder vendor\yiisoft\yii2-authclient\src\clients and read comments inside each file on how to use it.

You can download from github the completed code here: https://github.com/hensem/yii2_login

Credit:
https://code.tutsplus.com/tutorials/how-to-program-with-yii2-authclient-integration-with-twitter-google--cms-23489
https://github.com/yiisoft/yii2-authclient

Comments

Popular posts from this blog

Useful aliases

Enable Search Engine Friendly (Pretty URLs) in Yii2