Quick Start

Adding action to controller

Next step is to add yii\authclient\AuthAction to a web controller and provide a successCallback implementation, which is suitable for your needs. Typically, final controller code may look like following:

use app\components\AuthHandler;

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'auth' => [
                'class' => 'yii\authclient\AuthAction',
                'successCallback' => [$this, 'onAuthSuccess'],
            ],
        ];
    }

    public function onAuthSuccess($client)
    {
        (new AuthHandler($client))->handle();
    }
}

Note that it's important for auth action to be public accessible, so make sure it's not denied by access control filter.

Where AuthHandler implementation could be like this:

<?php
namespace app\components;

use app\models\Auth;
use app\models\User;
use Yii;
use yii\authclient\ClientInterface;
use yii\helpers\ArrayHelper;

/**
 * AuthHandler handles successful authentication via Yii auth component
 */
class AuthHandler
{
    /**
     * @var ClientInterface
     */
    private $client;

    public function __construct(ClientInterface $client)
    {
        $this->client = $client;
    }

    public function handle()
    {
        $attributes = $this->client->getUserAttributes();
        $email = ArrayHelper::getValue($attributes, 'email');
        $id = ArrayHelper::getValue($attributes, 'id');
        $nickname = ArrayHelper::getValue($attributes, 'login');

        /** @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 = $auth->user;
                $this->updateUserInfo($user);
                Yii::$app->user->login($user, Yii::$app->params['user.rememberMeDuration']);
            } else { // signup
                if ($email !== null && User::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(6);
                    $user = new User([
                        'username' => $nickname,
                        'github' => $nickname,
                        'email' => $email,
                        'password' => $password,
                        // 'status' => User::STATUS_ACTIVE // make sure you set status properly
                    ]);
                    $user->generateAuthKey();
                    $user->generatePasswordResetToken();

                    $transaction = User::getDb()->beginTransaction();

                    if ($user->save()) {
                        $auth = new Auth([
                            'user_id' => $user->id,
                            'source' => $this->client->getId(),
                            'source_id' => (string)$id,
                        ]);
                        if ($auth->save()) {
                            $transaction->commit();
                            Yii::$app->user->login($user, Yii::$app->params['user.rememberMeDuration']);
                        } else {
                            Yii::$app->getSession()->setFlash('error', [
                                Yii::t('app', 'Unable to save {client} account: {errors}', [
                                    'client' => $this->client->getTitle(),
                                    'errors' => json_encode($auth->getErrors()),
                                ]),
                            ]);
                        }
                    } else {
                        Yii::$app->getSession()->setFlash('error', [
                            Yii::t('app', 'Unable to save user: {errors}', [
                                'client' => $this->client->getTitle(),
                                'errors' => json_encode($user->getErrors()),
                            ]),
                        ]);
                    }
                }
            }
        } else { // user already logged in
            if (!$auth) { // add auth provider
                $auth = new Auth([
                    'user_id' => Yii::$app->user->id,
                    'source' => $this->client->getId(),
                    'source_id' => (string)$attributes['id'],
                ]);
                if ($auth->save()) {
                    /** @var User $user */
                    $user = $auth->user;
                    $this->updateUserInfo($user);
                    Yii::$app->getSession()->setFlash('success', [
                        Yii::t('app', 'Linked {client} account.', [
                            'client' => $this->client->getTitle()
                        ]),
                    ]);
                } else {
                    Yii::$app->getSession()->setFlash('error', [
                        Yii::t('app', 'Unable to link {client} account: {errors}', [
                            'client' => $this->client->getTitle(),
                            'errors' => json_encode($auth->getErrors()),
                        ]),
                    ]);
                }
            } else { // there's existing auth
                Yii::$app->getSession()->setFlash('error', [
                    Yii::t('app',
                        'Unable to link {client} account. There is another user using it.',
                        ['client' => $this->client->getTitle()]),
                ]);
            }
        }
    }

    /**
     * @param User $user
     */
    private function updateUserInfo(User $user)
    {
        $attributes = $this->client->getUserAttributes();
        $github = ArrayHelper::getValue($attributes, 'login');
        if ($user->github === null && $github) {
            $user->github = $github;
            $user->save();
        }
    }
}

successCallback method is called when user was successfully authenticated via external service. Via $client instance we can retrieve information received. In our case we'd like to:

  • If user is guest and record found in auth then log this user in.
  • If user is guest and record not found in auth then create new user and make a record in auth table. Then log in.
  • If user is logged in and record not found in auth then try connecting additional account (save its data into auth table).

Note: different Auth clients may require different approaches while handling authentication success. For example: Twitter does not allow returning of the user email, so you have to deal with this somehow.

Auth client basic structure

Although, all clients are different they share same basic interface yii\authclient\ClientInterface, which governs common API.

Each client has some descriptive data, which can be used for different purposes:

  • id - unique client id, which separates it from other clients, it could be used in URLs, logs etc.
  • name - external auth provider name, which this client is match too. Different auth clients can share the same name, if they refer to the same external auth provider. For example: clients for Google and Google Hybrid have same named "google". This attribute can be used inside the database, CSS styles and so on.
  • title - user-friendly name for the external auth provider, it is used to present auth client at the view layer.

Each auth client has different auth flow, but all of them supports getUserAttributes() method, which can be invoked if authentication was successful.

This method allows you to get information about external user account, such as ID, email address, full name, preferred language etc. Note that for each provider fields available may vary in both existence and names.

Defining list of attributes, which external auth provider should return, depends on client type:

Tip: If you are using several clients, you can unify the structure of the attributes, which they return, using yii\authclient\BaseClient::$normalizeUserAttributeMap.

Normalizing user attributes

yii\authclient\BaseClient::$normalizeUserAttributeMap maps each normalized name to a source: a string names a raw attribute, an array follows a nested path, and a callable computes a value. For example, add this to a client configuration:

'normalizeUserAttributeMap' => [
    'about' => 'bio',
    'language' => ['languages', 0, 'name'],
    'contact' => static function (array $attributes) {
        return $attributes['email'] ?? $attributes['name'] ?? null;
    },
],

Here, language reads $attributes['languages'][0]['name']. If any key in that path is missing, the mapping does not assign language. An array such as ['email', 'name'] reads $attributes['email']['name']; it does not try email and then name. Use a callable like the contact example to choose a fallback. Normalization adds values to the attribute array, overwriting any raw value with the same key as a normalized name. Other raw attributes remain available. Mappings are applied in order, so later mappings and callbacks see values assigned by earlier mappings.

Adding widget to login view

There's ready to use yii\authclient\widgets\AuthChoice widget to use in views:

<?= yii\authclient\widgets\AuthChoice::widget([
     'baseAuthUrl' => ['site/auth'],
     'popupMode' => false,
]) ?>

Custom providers, images and visible labels

By default, AuthChoice renders icon links with a title tooltip. Its bundled CSS supplies icons for the built-in provider names. A custom provider name needs its own image or link text.

Use begin() and end() with clientLink() to supply visible labels:

<?php
use yii\authclient\widgets\AuthChoice;
use yii\helpers\Html;

$authChoice = AuthChoice::begin([
    'baseAuthUrl' => ['/site/auth'],
    'popupMode' => false,
    'autoRender' => false,
]);
?>
<ul class="auth-clients">
    <?php foreach ($authChoice->getClients() as $client): ?>
        <li><?= $authChoice->clientLink($client, Html::encode($client->getTitle())) ?></li>
    <?php endforeach; ?>
</ul>
<?php AuthChoice::end(); ?>

For a custom image, pass Html::img('@web/images/provider.svg', ['alt' => $client->getTitle()]) as the link text. To retain a built-in icon and add a visible label below it, use:

$text = Html::tag('span', '', ['class' => 'auth-icon ' . $client->getName()])
    . Html::tag('span', Html::encode($client->getTitle()), ['class' => 'auth-title']);
echo $authChoice->clientLink($client, $text);

clientLink() accepts HTML, so encode provider titles when supplying text yourself. With popupMode disabled, the links navigate normally; no authchoice() JavaScript initialization is needed. Enable popupMode to have the widget register and initialize the popup script.

Using a different client collection

AuthChoice::$clientCollection selects the collection used to render links. The links contain the client ID and point to baseAuthUrl; they do not forward the collection configuration or OAuth scope. Configure the target AuthAction::$clientCollection to use the same collection:

// Controller action configuration:
'repository-auth' => [
    'class' => \yii\authclient\AuthAction::class,
    'clientCollection' => 'repositoryAuthClients',
    'successCallback' => [$this, 'onAuthSuccess'],
],
<?= \yii\authclient\widgets\AuthChoice::widget([
    'baseAuthUrl' => ['/site/repository-auth'],
    'clientCollection' => 'repositoryAuthClients',
    'popupMode' => false,
]) ?>

For independent authorization flows with different scopes, configure separate actions and client configurations. Give clients of the same class distinct IDs across collections to keep their stored authentication state separate. Changing the widget's collection alone does not change the target action's scope.

Note regarding GitHub client

Recently due to potential changes in GitHub callback handling, ensure that authclient=github is present in URL query param in Authorization Callback URL to avoid 404 error thrown when GitHub redirects user back to our app.

Example: Let's say my app is at https://example.com. In order to implement "Login with GitHub" feature into my app, I will create a new Oauth app for my example.com at https://github.com/settings/applications/new. In input field "Authorization callback URL" if I put https://example.com/site/auth, then I will get 404 error thrown by run() method. In order to fix this issue, above query param is required. So putting the "Authorization callback URL" value to https://example.com/site/auth?authclient=github is required.