Auth Clients

Yii provides official extension that lets you authenticate and/or authorize using external services via consuming OpenID, OAuth or OAuth2.

Installing extension

In order to install extension use Composer. Either run

composer require --prefer-dist yiisoft/yii2-authclient "*"

or add

"yiisoft/yii2-authclient": "*"

to the require section of your composer.json.

Configuring clients

After extension is installed you need to setup auth client collection application component:

'components' => [
    'authClientCollection' => [
        'class' => 'yii\authclient\Collection',
        'clients' => [
            'google' => [
                'class' => 'yii\authclient\clients\GoogleOpenId'
            ],
            'facebook' => [
                'class' => 'yii\authclient\clients\Facebook',
                'clientId' => 'facebook_client_id',
                'clientSecret' => 'facebook_client_secret',
            ],
            // etc.
        ],
    ]
    ...
]

Out of the box the following clients are provided:

- [[\yii\authclient\clients\Facebook Facebook]].
- Google (via [[yii\authclient\clients\GoogleOpenId OpenID]] and [[yii\authclient\clients\GoogleOAuth
- [[yii\authclient\clients\LinkedIn LinkedIn]].
- [[yii\authclient\clients\Live Microsoft Live]].
- [[yii\authclient\clients\Twitter Twitter]].
- [[yii\authclient\clients\VKontakte VKontakte]].
- Yandex (via [[yii\authclient\clients\YandexOpenId OpenID]] and [[yii\authclient\clients\YandexOAuth

Configuration for each client is a bit different. For OAuth it's required to get client ID and secret key from the service you're going to use. For OpenID it works out of the box in most cases.

Storing authorization data

In order to recognize the user authenticated via external service we need to store ID provided on first authentication and then check against it on subsequent authentications. It's not a good idea to limit login options to external services only since these may fail and there won't be a way for the user to log in. Instead it's better to provide both external authentication and good old login and password.

If we're storing user information in a database the schema could be the following:

CREATE TABLE user (
    id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    username varchar(255) NOT NULL,
    auth_key varchar(32) NOT NULL,
    password_hash varchar(255) NOT NULL,
    password_reset_token varchar(255),
    email varchar(255) NOT NULL,
    status smallint(6) NOT NULL DEFAULT 10,
    created_at int(11) NOT NULL,
    updated_at int(11) NOT NULL
);

CREATE TABLE auth (
    id int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
    user_id int(11) NOT NULL,
    source string(255) NOT NULL,
    source_id string(255) NOT NULL
);

ALTER TABLE auth ADD CONSTRAINT fk-auth-user_id-user-id
FOREIGN KEY user_id REFERENCES user(id);

In the SQL above user is a standard table that is used in advanced application template to store user info. Each user can authenticate using multiple external services therefore each user record can relate to multiple auth records. In the auth table source is the name of the auth provider used and source_id is unique user identificator that is provided by external service after successful login.

Using tables created above we can generate Auth model. No further adjustments needed.

Adding action to controller

Next step is to add [[yii\authclient\AuthAction]] to a web controller. Typically SiteController:

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

    public function onAuthSuccess($client)
    {
       $attributes = $client->getUserAttributes();

        /** @var Auth $auth */
        $auth = Auth::find()->where([
            'source' => $client->getId(),
            'source_id' => $attributes['id'],
        ])->one();

        if (Yii::$app->user->isGuest) {
            if ($auth) { // login
                $user = $auth->user;
                Yii::$app->user->login($user);
            } else { // signup
                if (User::find()->where(['email' => $attributes['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' => $client->getTitle()]),
                    ]);
                } else {
                    $password = Yii::$app->security->generateRandomString(6);
                    $user = new User([
                        'username' => $attributes['login'],
                        'email' => $attributes['email'],
                        'password' => $password,
                    ]);
                    $user->generateAuthKey();
                    $user->generatePasswordResetToken();
                    $transaction = $user->getDb()->beginTransaction();
                    if ($user->save()) {
                        $auth = new Auth([
                            'user_id' => $user->id,
                            'source' => $client->getId(),
                            'source_id' => (string)$attributes['id'],
                        ]);
                        if ($auth->save()) {
                            $transaction->commit();
                            Yii::$app->user->login($user);
                        } else {
                            print_r($auth->getErrors());
                        }
                    } else {
                        print_r($user->getErrors());
                    }
                }
            }
        } else { // user already logged in
            if (!$auth) { // add auth provider
                $auth = new Auth([
                    'user_id' => Yii::$app->user->id,
                    'source' => $client->getId(),
                    'source_id' => $attributes['id'],
                ]);
                $auth->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).

Although, all clients are different they shares 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 OpenID and Google OAuth have same name "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.

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

  • [[yii\authclient\OpenId]]: combination of requiredAttributes and optionalAttributes.
  • [[yii\authclient\OAuth1]] and [[yii\authclient\OAuth2]]: field scope, note that different providers use different formats for the scope.

Getting additional data via extra API calls

Both [[yii\authclient\OAuth1]] and [[yii\authclient\OAuth2]] provide method api(), which can be used to access external auth provider REST API. However this method is very basic and it may be not enough to access full external API functionality. This method is mainly used to fetch the external user account data.

To use API calls, you need to setup [[yii\authclient\BaseOAuth::apiBaseUrl]] according to the API specification. Then you can call [[yii\authclient\BaseOAuth::api()]] method:

use yii\authclient\OAuth2;

$client = new OAuth2;

// ...

$client->apiBaseUrl = 'https://www.googleapis.com/oauth2/v1';
$userInfo = $client->api('userinfo', 'GET');

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,
]) ?>

Creating your own auth clients

You may create your own auth client for any external auth provider, which supports OpenId or OAuth protocol. To do so, first of all, you need to find out which protocol is supported by the external auth provider, this will give you the name of the base class for your extension:

  • For OAuth 2 use [[yii\authclient\OAuth2]].
  • For OAuth 1/1.0a use [[yii\authclient\OAuth1]].
  • For OpenID use [[yii\authclient\OpenId]].

At this stage you can determine auth client default name, title and view options, declaring corresponding methods:

use yii\authclient\OAuth2;

class MyAuthClient extends OAuth2
{
    protected function defaultName()
    {
        return 'my_auth_client';
    }

    protected function defaultTitle()
    {
        return 'My Auth Client';
    }

    protected function defaultViewOptions()
    {
        return [
            'popupWidth' => 800,
            'popupHeight' => 500,
        ];
    }
}

Depending on actual base class, you will need to redeclare different fields and methods.

[[yii\authclient\OpenId]]

All you need is to specify auth URL, by redeclaring authUrl field. You may also setup default required and/or optional attributes. For example:

use yii\authclient\OpenId;

class MyAuthClient extends OpenId
{
    public $authUrl = 'https://www.my.com/openid/';

    public $requiredAttributes = [
        'contact/email',
    ];

    public $optionalAttributes = [
        'namePerson/first',
        'namePerson/last',
    ];
}

[[yii\authclient\OAuth2]]

You will need to specify:

  • Auth URL by redeclaring authUrl field.
  • Token request URL by redeclaring tokenUrl field.
  • API base URL by redeclaring apiBaseUrl field.
  • User attribute fetching strategy by redeclaring initUserAttributes() method.

For example:

use yii\authclient\OAuth2;

class MyAuthClient extends OAuth2
{
    public $authUrl = 'https://www.my.com/oauth2/auth';

    public $tokenUrl = 'https://www.my.com/oauth2/token';

    public $apiBaseUrl = 'https://www.my.com/apis/oauth2/v1';

    protected function initUserAttributes()
    {
        return $this->api('userinfo', 'GET');
    }
}

You may also specify default auth scopes.

Note: Some OAuth providers may not follow OAuth standards clearly, introducing differences, and may require additional efforts to implement clients for.

[[yii\authclient\OAuth1]]

You will need to specify:

  • Auth URL by redeclaring authUrl field.
  • Request token URL by redeclaring requestTokenUrl field.
  • Access token URL by redeclaring accessTokenUrl field.
  • API base URL by redeclaring apiBaseUrl field.
  • User attribute fetching strategy by redeclaring initUserAttributes() method.

For example:

use yii\authclient\OAuth1;

class MyAuthClient extends OAuth1
{
    public $authUrl = 'https://www.my.com/oauth/auth';

    public $requestTokenUrl = 'https://www.my.com/oauth/request_token';

    public $accessTokenUrl = 'https://www.my.com/oauth/access_token';

    public $apiBaseUrl = 'https://www.my.com/apis/oauth/v1';

    protected function initUserAttributes()
    {
        return $this->api('userinfo', 'GET');
    }
}

You may also specify default auth scopes.

Note: Some OAuth providers may not follow OAuth standards clearly, introducing differences, and may require additional efforts to implement clients for.