Skip to content

JWT authentication

available since LUYA admin module version 2.2

The LUYA admin provides a basic JWT generator including an out of the box authentication system which can proxy requests trough LUYA admin API User and those permission system.

Prerequisite

How it works

As all LUYA admin APIs requerd an authentication are proxied trough LUYA API Users. The life cycle of the JWT request is described as followed (assuming JWT configuration in the module is done accordingly):

Get the token:

Make Request:

The image shows the above descriped cycle.

luya-proxy

Setup

  • Create an API User in the admin UI which will handle the JWT requests as Proxy User.
  • Configure the luya\admin\components\Jwt component in your config:
php
'components' => [
    'jwt' => [
        'class' => 'luya\admin\components\Jwt',
        'key' => 'MySecretJwtKey',
        'apiUserEmail' => 'jwtapiuser@luya.io',
        'identityClass' => 'app\modules\myadminmodule\models\User',
    ],
],

The User which contains user data:

php
class User extends \luya\admin\ngrest\base\NgRestModel implements luya\admin\base\JwtIdentityInterface
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'user';
    }

    /**
     * @inheritdoc
     */
    public static function ngRestApiEndpoint()
    {
        return 'api-user';
    }

    // ....... other ngrest models specific content ........... //

    /* JwtIdentityInterface */

    public function getId()
    {
        return $this->id;
    }

    public static function loginByJwtToken(\Lcobucci\JWT\Token\Plain $token)
    {
        // $userId = $token->claims()->get('uid');
        return self::findOne(['jwtToken' => $token->toString()]);
    }
}

An NgRest API with additional login, signup and me actions.

php
/**
 * User Controller.
 * 
 * The example assumes that app\modules\myapimodule\models\User implements the luya\admin\base\JwtIdentityInterface
 */
class UserController extends \luya\admin\ngrest\base\Api
{
    /**
     * @var array Define methods which does not require authentication
     */
    public $authOptional = ['login', 'signup'];

    /**
     * @var string The path to the model which is the provider for the rules and fields.
     */
    public $modelClass = 'app\modules\myapimodule\models\User';

    /**
     * Make user login and return the user with the fresh generated JWT token which is stored in the user.
     * 
     * > No authentication needed.
     */
    public function actionLogin()
    {
        $model = new User();
        $model->scenario = User::SCENARIO_LOGIN;
        if ($model->load(Yii::$app->request->post(), '') && $model->validate()) {
            $user = User::find()->where(['email' => $model->email])->one();
            if ($user && Yii::$app->security->validatePassword($model->password, $user->password)) {
                if ($user->updateAttributes(['jwtToken' => Yii::$app->jwt->generateToken($user)])) {
                    return $user;
                }
        
            } else {
                $model->addError('email', 'Unable to find the given email or password is wrong.');
            }
        }

        return $this->sendModelError($model);
    }

    /**
     * Allow users to signup which will create a new user.
     * 
     * > No authentication needed.
     *
     * @return User
     */
    public function actionSignup()
    {
        $model = new User();
        if ($model->load(Yii::$app->request->post(), '') && $model->save()) {
            return $model;
        }

        return $this->sendModelError($model);
    }

    /**
     * Returns the currently logged in JWT authenticated user.
     *
     * > This method requires authentication.
     * 
     * @return User
     */
    public function actionMe()
    {
        return Yii::$app->jwt->identity;
    }
}

If a successfull JWT authentication is made the luya\admin\components\Jwt -> $identity contains the luya\admin\components\Jwt -> $identityClass object implementing luya\admin\base\JwtIdentityInterface .

CORS Preflight Request

When working with cross domain requests, each XHR request to the API will make an option request or also known as preflight request. The luya\admin\ngrest\base\Api controllers provide an out of the box solution which works for common CRUD operations (add, view, list, edit, delete). This can be enabled by setting luya\admin\Module -> $cors to true. For further CORS config options use luya\traits\ApplicationTrait -> $corsConfig.

When working with custom actions you might need to configure the option request for the given method. Therefore you need to configure the API with the following setup: create an URL rule for options request, define the option and make sure the option is available without authentication (its common that option request won't have authentication headers).

Create the URL rule for the option request, which defines where the option action should be looked up:

php
public $apiRules = [
    'my-api-name' => [
        'extraPatterns' => [
            'OPTIONS index' => 'options',
        ]
    ]
];

The above example will forward all OPTIONS request made to the my-api-name API on the index action to the options action. 'OPTIONS index' => 'options' index is the requested action, and options is the action to forward.

TIP

Instead of defining each custom action for the method and the options request it's possible to set an options wildcard definition like OPTIONS <action:[a-zA-Z0-9\-]+>' => 'options'. Full example

'GET type' => 'type',
'GET agenda' => 'agenda',
'GET years-range' => 'years-range',
'OPTIONS <action:[a-zA-Z0-9\-]+>' => 'options',

As the options request is forwarded to the options action we should create this in the controller using luya\admin\ngrest\base\actions\OptionsAction :

php
public function actions()
{
    return [
        'options' => luya\admin\ngrest\base\actions\OptionsAction:class,
    ];
}

Now the controller has an options action. In order to ensure that the options action does not required permission add the action name to the luya\traits\RestBehaviorsTrait -> $authOptional array:

php
public $authOptional = ['options'];

Permissions

A few principals regarding permissions:

  • Unless an action is masked as luya\traits\RestBehaviorsTrait -> $authOptional every action requires authentication.
  • If the group of the defined luya\admin\components\Jwt -> $apiUserEmail API user has no permissions, only your custom actions are accessible.
  • When accessing NgRest API actions like update, create, list or view (detail) and permission is granted the actions are logged with the configured API User.
  • As permission is proxied trough API Users, a valid API User token could access those informations as well.

User Based CheckAccess

Its a common task to check the permission for a certain user id, whether the user can update/delete an item or not. Therefore the luya\admin\base\RestActiveController -> checkAccess() method can be extended by some JWT user id based actions.

php
public function checkAccess($action, $model = null, $params = [])
{
    parent::checkAccess($action, $model, $params);

    // see if JWT user performs this action
    if (Yii::$app->jwt->identity && ($action == 'delete' || $action == 'update')) {
        // if jwt user id is not equal the models user id, throw forbidden exception.
        if (Yii::$app->jwt->identity->id != $model->user_id) {
            throw new ForbiddenHttpException("Unable to delete/update this item due to permission restrictions.");
        }
    }
}

The above method assume that the $model has a column with user_id, adjust this to match the user id column.