Skip to content
On this page

JWT authentication

available since LUYA admin module version 2.2

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

Prerequisite

  • A custom (application) admin module is required to setup JWT ([[/guide/admin/intro]]).
  • Understand API Users which are explaind in Headless Guide Section ([[concept-headless.md]]).
  • Configure the luya\admin\components\Jwt component.

How it works

As all LUYA admin APIs requerd an authentification are proxied trough LUYA API Users (Read about [[concept.headless.md]]).

The life cycle of the JWT request is described as followed (assuming JWT configuration in the modul is done accordingly):

Get the token:

  • A token is generated by a not secured action, the luya\admin\components\Jwt::generateToken() is a helper to generate the token.
  • Make a request to any LUYA Admin API:

Make Request:

  • The Authentification system will threat JWT auth first.
  • Token will be passed to the luya\admin\baseJwtIdentityInterface::loginByJwtToken() method. Return the user if login is valid.
  • The API User model defined in luya\admin\components\Jwt::$apiUserEmail will be looked up and loggedin.
  • The authenticated API User check permission based on the related groups (API Users can associated with multiple groups or none).

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',
    ],
],
  • Implement the luya\admin\base\JwtIdentityInterface into the given luya\admin\components\Jwt::$identityClass.
  • Generate an Action for Login (generate token) and signup (if needed).
  • Setup the defined luya\admin\components\Jwt::$apiUserEmail API User and grant the needed permissions (none if no admin resources should be accessible).

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 additonal 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 authentification
     */
    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 authentification 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 authentification 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 authentification.
     * 
     * @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 authentification (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.

Instead of define each custom action for the method and the options request its possible to set an options wildcard defintion 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 forwared to the options action we should create this in the controller:

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 authentification.
  • 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. Thefore 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.