filsh / yii2-oauth2-server

A wrapper for implementing an OAuth2 Server(https://github.com/bshaffer/oauth2-server-php)
MIT License
333 stars 167 forks source link

How to use this extension with official Yii2 AuthClient ? #155

Open ahmadfadlydziljalal opened 1 year ago

ahmadfadlydziljalal commented 1 year ago

I have 2 apps:

Everything is looked good in every step at Yii2 server side:

 'components' => [
        'urlManager' => [
            'enablePrettyUrl' => true,
            'showScriptName' => false,
            'rules' => [
                '<alias:\w+>' => 'site/<alias>',
                'POST oauth2/<action:\w+>' => 'oauth2/rest/<action>',
            ]
        ],
],
 'modules' => [
        'oauth2' => [
           'class' => 'filsh\yii2\oauth2server\Module',
           'components' => [
              'request' => function () {
                 return \filsh\yii2\oauth2server\Request::createFromGlobals();
              },
              'response' => [
                 'class' => \filsh\yii2\oauth2server\Response::class,
              ],
           ],
            'tokenParamName' => 'accessToken',
            'tokenAccessLifetime' => 3600 * 24,
            'storageMap' => [
                'user_credentials' => 'app\models\User',
            ],
            'grantTypes' => [
                'user_credentials' => [
                    'class' => 'OAuth2\GrantType\UserCredentials',
                ],
                'authorization_code' => [
                   'class' => 'OAuth2\GrantType\AuthorizationCode'
                ],
                'refresh_token' => [
                    'class' => 'OAuth2\GrantType\RefreshToken',
                    //'always_issue_new_refresh_token' => true
                ]
            ],
            'options' => [
                'allow_implicit' => true
            ]
        ],
  ....

But the problem is, when client side successfully login, why userAttributes is null ? In Yii2 client side, I set the SiteController like this:

 /**
     * {@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();
    }

Then, in AuthHandler:

<?php

namespace app\components;

use app\models\Auth;
use app\models\User;
use Exception;
use Yii;
use yii\authclient\ClientInterface;
use yii\helpers\ArrayHelper;
use yii\helpers\Html;
use yii\helpers\StringHelper;
use yii\helpers\VarDumper;

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

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

    /**
     * @return void
     * @throws \yii\base\Exception
     * @throws \yii\db\Exception
     * @throws Exception
     */
    public function handle()
    {

        $attributes = $this->client->getUserAttributes();

        /*
         * for debugging
         * die(Html::tag('pre', VarDumper::dumpAsString($attributes)));
         * */

        $email = ArrayHelper::getValue($attributes, 'email');
        $id = ArrayHelper::getValue($attributes, 'id');
        $nickname = str_replace(" ", "-", ArrayHelper::getValue($attributes, 'name'));

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

        if (Yii::$app->user->isGuest) {

            if ($auth) { // login

                $user = $auth->user;
                $this->updateUserInfo($user);
                Yii::$app->user->login($user, Yii::$app->params['user.rememberMeDuration']);
                Yii::$app->getSession()->setFlash(
                    'success',
                    'Login by ' . ucfirst($this->client->getId()) . ' Welcome! '
                );
            } 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(Yii::$app->params['user.passwordMinLength']);
                    $user = new User([
                        'username' => $nickname,
                        //'github' => $nickname,
                        'email' => $email,
                        'password' => $password,
                    ]);
                    $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->getSession()->setFlash(
                                'success',
                                'Joined as ' . ucfirst($this->client->getId()) . ', and welcome...! '
                            );
                            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' =>  yii\helpers\Json::encode($auth->getErrors()),
                                ]),
                            ]);
                        }
                    } else {

                        Yii::$app->getSession()->setFlash('error', [
                            Yii::t('app', 'Unable to save user: {errors}', [
                                'client' => $this->client->getTitle(),
                                'errors' => yii\helpers\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()) {

                    $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' => yii\helpers\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
     * @return void
     * @throws Exception
     */
    private function updateUserInfo(User $user)
    {
        $attributes = $this->client->getUserAttributes();
        $github = ArrayHelper::getValue($attributes, 'login');
        if ($github) {
            if ($user->github === null) {
                $user->github = $github;
                $user->save();
            }
        }
    }
}

Here is the config that I used in Yii2 client side:

<?php

namespace app\components;

use yii\authclient\OAuth2;

class MyAuthClient extends OAuth2
{

    protected function defaultName()
    {
        return 'my_auth_client';
    }

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

    protected function initUserAttributes()
    {
        return $this->api('userinfo', 'GET');
    }
}
 'components' => [
        'authClientCollection' => [
            'class' => 'yii\authclient\Collection',
            'clients' => [
                'myauth' => [
                    'class' => '\app\components\MyAuthClient',
                    'clientId' => 'testclient',
                    'clientSecret' => 'testpass',
                    'authUrl' => 'http://10.60.36.60:8080/authorize',
                    'tokenUrl' => 'http://10.60.36.60:8080/oauth2/token',
                    'apiBaseUrl' => 'http://10.60.36.60:8080/apis/oauth2/v1'
                ],

            ],
        ],
        ....

And, Here is the info:


app\components\MyAuthClient#1
(
    [yii\base\Component:_events] => []
    [yii\base\Component:_eventWildcards] => []
    [yii\base\Component:_behaviors] => null
    [yii\authclient\BaseClient:_id] => 'myauth'
    [yii\authclient\BaseClient:_name] => null
    [yii\authclient\BaseClient:_title] => null
    [yii\authclient\BaseClient:_userAttributes] => null
    [yii\authclient\BaseClient:_normalizeUserAttributeMap] => null
    [yii\authclient\BaseClient:_viewOptions] => null
    [yii\authclient\BaseClient:_httpClient] => yii\httpclient\Client#2
    (
        [yii\base\Component:_events] => []
        [yii\base\Component:_eventWildcards] => []
        [yii\base\Component:_behaviors] => []
        [baseUrl] => 'http://10.60.36.60:8080/apis/oauth2/v1'
        [formatters] => [
            'urlencoded' => yii\httpclient\UrlEncodedFormatter#3
            (
                [encodingType] => 1
                [charset] => null
            )
        ]
        [parsers] => [
            'json' => yii\httpclient\JsonParser#4
            (
                [asArray] => true
            )
        ]
        [requestConfig] => []
        [responseConfig] => []
        [contentLoggingMaxSize] => 2000
        [yii\httpclient\Client:_transport] => yii\httpclient\StreamTransport#5
        (
            [yii\base\Component:_events] => []
            [yii\base\Component:_eventWildcards] => []
            [yii\base\Component:_behaviors] => null
        )
    )
    [yii\authclient\BaseClient:_requestOptions] => []
    [yii\authclient\BaseClient:_stateStorage] => yii\authclient\SessionStateStorage#6
    (
        [yii\base\Component:_events] => []
        [yii\base\Component:_eventWildcards] => []
        [yii\base\Component:_behaviors] => null
        [session] => yii\web\Session#7
        (
            [yii\base\Component:_events] => []
            [yii\base\Component:_eventWildcards] => []
            [yii\base\Component:_behaviors] => null
            [flashParam] => '__flash'
            [handler] => null
            [*:_forceRegenerateId] => null
            [yii\web\Session:_cookieParams] => [
                'httponly' => true
            ]
            [yii\web\Session:frozenSessionData] => null
            [yii\web\Session:_hasSessionId] => true
        )
    )
    [version] => '2.0'
    [apiBaseUrl] => 'http://10.60.36.60:8080/apis/oauth2/v1'
    [authUrl] => 'http://10.60.36.60:8080/authorize'
    [scope] => null
    [autoRefreshAccessToken] => true
    [parametersToKeepInReturnUrl] => [
        0 => 'authclient'
    ]
    [yii\authclient\BaseOAuth:_returnUrl] => 'http://10.60.36.60:8081/auth?authclient=myauth'
    [yii\authclient\BaseOAuth:_accessToken] => yii\authclient\OAuthToken#8
    (
        [tokenParamKey] => 'access_token'
        [tokenSecretParamKey] => 'oauth_token_secret'
        [createTimestamp] => 1670846067
        [yii\authclient\OAuthToken:_expireDurationParamKey] => null
        [yii\authclient\OAuthToken:_params] => [
            'access_token' => '358d8f2831a027f9e815a5b44f569678445a9429'
            'expires_in' => 86400
            'token_type' => 'Bearer'
            'scope' => null
            'refresh_token' => 'da3f5c8c33cd694479ee4a4fb51ccdd7943cf478'
        ]
    )
    [yii\authclient\BaseOAuth:_signatureMethod] => []
    [clientId] => 'testclient'
    [clientSecret] => 'testpass'
    [tokenUrl] => 'http://10.60.36.60:8080/oauth2/token'
    [validateAuthState] => true
    [enablePkce] => false
)
ahmadfadlydziljalal commented 1 year ago

Maybe we need, how to implement the OpenID ?