Yii2 as full Resource Server In Oauth2 Paradigm

Hi Yiiers,

I am implementing a Yii2 basic template to become a REST API Resource server in the Oauth2 schema, such as:
oauth_overview
.
The client app using Angular17, where after successfully logging in from the Authorization Server, it gets a JWT which will be used as a bearer token to the Yii2 application that I develop now.

In those JWT, there is a sub as a user ID and roles in the form of a string array.
This is those JWT payload.

{
  "roles": [
    "frontend-developer",
    "admin"
  ],
  "id": "8f68d4f824a8c4836244ba8e9ec1d75b5ca13a21",
  "jti": "8f68d4f824a8c4836244ba8e9ec1d75b5ca13a21",
  "iss": "http://localhost:1209",
  "aud": "main-frontend",
  "sub": 10,
  "exp": 1708067453,
  "iat": 1707981053,
  "token_type": "bearer",
  "scope": "openid profile offline_access email username"
}

I use yii\rest\controller to handle resources, not yii\rest\ActiveController for OpenAPI support. So the code looks like this:

class RestController extends Controller
{

    /*
     * Use function apache_request_headers() to see all headers
     * */
    use RestTrait;

}

trait RestTrait
{
    /**

     * @return array
     */
    public function behaviors(): array
    {
        $behaviors = parent::behaviors();

        // remove authentication filter,
        unset($behaviors['authenticator']);

        // add CORS filter
        $behaviors['corsFilter'] = [
            'class' => yii\filters\Cors::class,
            'cors' => [
                // 'Origin' => ['*'], // Already defined in 000.default.conf virtualhost
                // 'Access-Control-Request-Headers' => ['*'], // Already defined in 000.default.conf virtualhost
                // 'Access-Control-Allow-Credentials' => true, // Already defined in 000.default.conf virtualhost
                'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
                'Access-Control-Max-Age' => 86400,
                'Access-Control-Expose-Headers' => ['*'],
            ]
        ];

        // re-add authentication filter

        $behaviors['authenticator'] = [
            'class' => bizley\jwt\JwtHttpBearerAuth::class,
        ];

        // avoid authentication on CORS-pre-flight requests (HTTP OPTIONS method)
        $behaviors['authenticator']['except'] = ['options'];
        return $behaviors;
    }

    public function actionOptions(): bool
    {
        \Yii::$app->response->setStatusCode(200);
        return true;
    }
    
}

Now, The following controller is use of the controller above.

class AgamaController extends RestController
{

    /**
     * {@inheritdoc}
     */
    protected function verbs(): array
    {
        return [
            'index' => ['GET', 'HEAD', 'OPTIONS'],
            'view' => ['GET', 'HEAD', 'OPTIONS'],
            'create' => ['POST'],
            'update' => ['POST', 'PUT'],
            'delete' => ['DELETE'],
        ];
    }

    /**
     * List of Agama, docs by OpenAPI - Swagger
     * @return ActiveDataProvider
     */
    #[OA\Get(
        path: "/v1/agama",
        description: "By default, It will return all Agama records.",
        summary: "Retrieves the collection of Agama resources",
        security: [
            new OA\SecurityScheme(
                ref: "#/components/securitySchemes/AgamaAuth"
            )
        ],
        tags: ["agama"],
        parameters: [
            new OA\QueryParameter(ref: '#/components/parameters/page'),
            new OA\QueryParameter(ref: '#/components/parameters/per-page'),
            new OA\QueryParameter(ref: '#/components/parameters/sort'),
            new OA\QueryParameter(ref: '#/components/parameters/AgamaSearch[id]'),
            new OA\QueryParameter(ref: '#/components/parameters/AgamaSearch[nama]'),
            new OA\QueryParameter(ref: '#/components/parameters/AgamaSearch[alias]'),
        ],
        responses: [
            new OA\Response(
                ref: '#/components/responses/AgamaArray',
                response: HttpCode::OK,
            )
        ]
    )]
    public function actionIndex(): ActiveDataProvider
    {
        // THIS ACTION ONLY FOR `admin` roles to pass. The others must be FORBIDDEN.
        $searchModel = new AgamaSearch();
        return $searchModel->search(Yii::$app->request->queryParams);
    }

}

So to set and get User data in Yii2 as beforeAction from Authenticator, we can use the User model as follows.

class User extends BaseObject implements IdentityInterface
{
    /**
     * {@inheritdoc}
     * @param $token
     * @param null $type
     * @return IdentityInterface|User|null
     * @throws InvalidConfigException
     */
    public static function findIdentityByAccessToken($token, $type = null): User|IdentityInterface|null
    {
        if (Yii::$app->jwt->validate($token)) {
            $user = new static();
            $user->setId( Yii::$app->jwt->parse($token)->claims()->get('sub'));
            $user->setAccessToken($token);

//            $auth = Yii::$app->authManager;
//            $auth->createRole('super-admin');
//            $authorRole = $auth->getRole('super-admin');
//            $auth->assign($authorRole, $user->getId());

            return $user;
        }

        return null;

    }

What we want to achieve is, with data from the JWT payload from the frontend which contains role, we can immediately check whether the user can carry out a certain action in a simple way, of course with Yii2 way.

My Question, What do you suggest to handle the role I get from the JWT payload sent from the frontend.

  1. Is it possible to use an RBAC role, such as Yii::$app->user->can('admin'), even without having a role for the user in the database, or is it necessary to use ` ‘yii\rbac\PhpManager’?
  2. Or use Access Control Filter on a controller that extends the RestController class that I defined above?
  3. Or use checkAccess method ? , then how to implement it in code that extends from yii\rest\controller?

Or do I need to check again on the Authentication server regarding the authorization?

Please guide.
Thank You

Why not? it depends on how you want to implement it. You can throw 403 when user is not allowed and handle that in the code or you can use RBAC extension like MDM’s which have a helper to check actual route than at role level. But may be you do not need to be that granular. It depends on your need

That also should be fine. Again it depends on how granular in access checking do you want to be.

In your case you do not have checkAccess method to override as you do not use ActiveController. Just override beforeAction, which is bad way. In this case stick with Access Control Filter to handle before Action for you!