Security - Authentication and Authorization

Pionia Logo

This guide assumes you have a basic understanding of how Pionia works. If you are new to Pionia, you can start by going through the API Tutorial guide.

Security is a very important aspect of any application. In Pionia, Security is approached in two ways.

  • Authentication
  • Authorization

Authentication - Authentication Backends.

Currently, Pionia does not dictate on any authentication mechanism but provides a way to implement your own authentication mechanism.

Pionia keeps an open mind on how you want to secure your apis. Most commonly, you will find JWT, OAuth, Basic Auth, and many more helpful. You can implement any of these in Pionia. All Pionia is seeking out is context user object that gets returned in your authentication backends.

With this approach, you can make authentication backends that handle mobile, web, and other platforms separately.

Packages like firebase/php-jwt can be used to implement JWT authentication.

Also, you can visit our jwt authentication sample guide on how to implement JWT authentication in Pionia.

About Authentication Backends

These are classes that are responsible for authenticating the user. They are responsible for verifying the user’s identity and returning the user object.

All authentication backends must extend the Pionia\Core\Interceptions\BaseAuthenticationBackend class and implement the authenticate method. This method receives the request object, this also implies that you have access to your headers, request body, and therefore you can implement any authentication mechanism you want.

Authentication backends must return the Pionia\Core\Helpers\ContextUserObject object if the user is authenticated, otherwise, they should return null.

You can have multiple authentication backends in the same application. Pionia will iterate through all the authentication backends until one of them returns a user object.

Otherwise, it will proceed to process the request without authenticating the user. And as a result, this->mustAuthenticate will be failing if the user is not authenticated.

Creating an Authentication Backend

To quickly bootstrap your authentication backend, you can use the following command.

Example below creates an authentication backend called JwtAuthBackend.

php pionia  gen:auth  jwt

Notice that we only define jwt as the authentication backend name. This is because Pionia will automatically append AuthBackend to the name.

Upon running the above command, Pionia will create a new authentication backend in the app/authentications directory.

<?php

/**
 * This authentication backend is auto-generated from pionia cli.
 * Remember to register your backend in settings.ini
 */

namespace application\authentications;

use Pionia\Core\Helpers\ContextUserObject;
use Pionia\Core\Interceptions\BaseAuthenticationBackend;
use Pionia\Request\Request;

class JwtAuthBackend extends BaseAuthenticationBackend
{
	/**
	 * Implement this method and return your 'ContextUserObject'. You can use Porm here too!
	 */
	public function authenticate(Request $request):? ContextUserObject
	{
		$userObj = new ContextUserObject();

		# your logic here...

		return $userObj;
	}
}

You can now implement your authentication logic in the authenticate method.

To access the headers, you can get them from the headers key on the request like $authHeader = $request->headers->get('Authorization');.

The ContextUserObject is a simple object that holds the user’s information. You can add any information you want to this object.

It contains the following properties:

  • user - The object from your db.
  • authenticated - A boolean value that indicates whether the user is authenticated or not.
  • permissions - An array of permissions that the user has.
  • authExtra - Any other extra data you want to set on the user context. This can store staff like role, user id, etc. You will be able to access this data in your services.

Registering your Authentication Backend

After creating your authentication backend, you need to register it in the settings.ini file under the authentications section. Every backend must be given a unique name. Also, you to note that the order of registration matters. Pionia will iterate through the authentications in the order they are registered.

[authentications]
jwt_auth = application\authentications\JwtAuthBackend

And that’s it! Your authentication backend is now ready to be used in your application.

RBAC - Service Level Authorization

Accessing the set Context Object

To access the context object in your services, you can access it from the $this->auth();.

This returns the ContextUserObject object that you set in your authentication backend.

<?php

class TodoService extends BaseRestService
{
    public function getTodo()
    {
        $user = $this->auth()->user;
        $permissions = $this->auth()->permissions;
        $authExtra = $this->auth()->authExtra;

        // your logic here...
    }
}

Accessing the AuthExtras

AuthExtras are an associative array that you can use to store any extra data you want to access in your services. To access a single item from this array, you can use $this->getAuthExtraByKey($key).

You can also check if a certain extra key was set on the extras array by using $this->authExtraHas($key). This will return a boolean value.

<?php

class TodoService extends BaseRestService
{
    public function getTodo()
    {
        $role = $this->getAuthExtraByKey('role');
        $userId = $this->getAuthExtraByKey('userId');

        // your logic here...
    }
}

Protecting your Services

The entire service can be protected by setting the $serviceRequiresAuth property to true. This will ensure that all actions in the service are secure and only authenticated users can access them.

<?php

class TodoService extends BaseRestService
{
    protected $serviceRequiresAuth = true;

    public function getTodo()
    {
        // your logic here...
    }
}

Protecting your Actions

You can also protect individual actions in two ways.

Setting the $actionsRequiringAuth property

You can set the $actionsRequiringAuth property to an array of actions that require authentication.


class TodoService extends BaseRestService
{
    protected $actionsRequiringAuth = ['getTodo'];

    public function getTodo() // accessing this action will require authentication
    {
        // your logic here...
    }
}

Using the mustAuthenticate method

You can also use the mustAuthenticate method in your action to check if the user is authenticated. This method will throw an exception if the user is not authenticated.


class TodoService extends BaseRestService
{
    public function getTodo()
    {
        $this->mustAuthenticate(); // user must be authenticated to go beyond this point

        // your logic here...
    }
}

Setting the $authMessage property will set a custom message that will be thrown if the user is not authenticated.


class TodoService extends BaseRestService
{
    protected $authMessage = 'You must be authenticated to use any service in the todo context.';

    public function getTodo()
    {
        $this->mustAuthenticate(); // user must be authenticated to go beyond this point

        // your logic here...
    }
}

Authorization

Authorization is the process of determining whether a user has the necessary permissions to access a certain resource.

In Pionia, authorization is done using permissions. Permissions are authorization rules that you can set on your context object in your authentication backend.

Permissions - Authorities

There are three ways to check if a user has a certain permission[s]/authority[ies].

Using the $actionPermission service property.

This can be used both in the normal and in generic services. It defines an associative array that defines permission list per action.


public array $actionPermissions = [
    'create' => ['create_todo'],
    'update' => ['update_todo'],
    'delete' => ['delete_todo']
];

From Version 1.1.4, this can also take up strings like below

public array $actionPermissions = [
    'create' => 'create_todo',
    'update' => 'update_todo',
    'delete' => 'delete_todo'
];

Permissions create_todo, update_todo are permissions you set on the context user object from your authentication backends. If these are not defined, the service won’t be accessed.

The above implies that to access

Using the can method

You can check if a user has a certain permission by using the can method that is available on the service.

<?php

class TodoService extends BaseRestService
{
    public function getTodo()
    {
        $this->can('view-todo'); // user must have the permission to go beyond this point
    }
}

If the user does not have the permission, the can method will throw an exception and the action will not be executed.

Using the canAll method

can checks only one permission, however, you can check multiple permissions by using the canAll method.

<?php

class TodoService extends BaseRestService
{
    public function getTodo()
    {
        $this->canAll(['view-todo', 'edit-todo']); // user must have both permissions to go beyond this point
    }
}

Using the canAny method

The two methods mentioned earlier check if the user has the permission. However, you might want to check if the user has any one of the permissions. You can do this by using the canAny method.


class TodoService extends BaseRestService
{
    public function getTodo()
    {
        $this->canAny(['view-todo', 'edit-todo']); // user must have any of the permissions to go beyond this point
    }
}

Custom Authorization Messages

The above methods take a second parameter $message which is a custom message that will be thrown if the user does not have the permission.


class TodoService extends BaseRestService
{
    public function getTodo()
    {
        $this->can('view-todo', 'You do not have permission to view todo'); // user must have the permission to go beyond this point
    }
}

All the authentication and authorization should happen early in the an action. This is because, if the user is not authenticated or does not have the necessary permissions, the action should not be executed.

Remember, all authentication backends run after middlewares. This is because middlewares can be used to sanatize the request, and therefore, it is important to run them before the authentication backends.

Custom Return Codes.

By default, Pionia will return a 401 return code if the user is not authenticated and a 403 return code if the user does not have the necessary permissions.

You can override these permissions by defining the UNAUTHENTICATED_CODE and the UNAUTHORIZED_CODE in the settings.ini under the SERVER section.


[SERVER]
; other settings
UNAUTHENTICATED_CODE = 10
UNAUTHORIZED_CODE = 11
; other settings

Using the above example, 10 and 11 will override the default 401 and 403 return codes respectively.