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
- A custom (application) admin module is required to setup JWT .
- Understand API Users which are explained in Headless Guide Section.
- Configure the luya\admin\components\Jwt component.
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:
- 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 Authentication 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.
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:
'components' => [
'jwt' => [
'class' => 'luya\admin\components\Jwt',
'key' => 'MySecretJwtKey',
'apiUserEmail' => '[email protected]',
'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:
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.
/**
* 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:
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 :
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:
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.
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.