A week ago we decided to upgrade our Slim 2 Application in Mangoler eCommerce. We developed the APIs in Mangoler with Slim 2 as it is simple and very fast for small and medium applications. Besides, it’s easy to understand too. I went through the Slim 3 documentation and noticed that there were some new changes such as Middleware signature or cutting off the hooks and introducing the dependency container. I wanted to implement simple role-based authentication for our APIs. So the plan was:
- Authenticate user requests with unique token instead of username and password
- Authenticate user request based on their role
The idea is simple. We only want to let specific roles do specific actions. Those actions that need protection will be added to our access list, but for those that do not, we simply ignore it and our middleware will pass them through.
Token-based authentication advantages
When you are using the token instead of a username and password to authenticate a user request, it is more efficient for sequence API calls. The idea is, a user will login once they enter their correct username and password. So you need to authenticate the user every time they request something, such as their order history or changing their settings. Of course, you can do it with validating the username and password in every API call, but this will do a roundtrip to your database as you need to find a user, validate the password against the hash and return the resource.
Therefore, with a token approach, you simply create a token for each user when they login and he will use the token every time they need to get a resource from your server. This token will have an expiry date too.
Implement token and role based authentication
First, add these two fields in your user table to save the user token:
token_char | varchar(16) |
token_expiry | datetime |
Now you need to create your middleware. Middleware gives you the ability to run your code before and after each request/response to manipulate the user request and the response. This is where you can do your authentication. I created my middlewares in a “middleware” folder and I added my “TokenAuth.php” there. You can create yours based on your project structure.
Before I jump into the code, there is one more thing about the middlewares in Slim 3. Slim 3 uses dependency containers for managing the application objects. With this built-in container, you can always have one instance of your object in your container and access it anywhere in your code. This is pretty important as this is the only way you can call your classes in the middleware, based on what I have tried.
You can add your classes when you create your Slim application. This is how your Slim initialization should look like:
$config = [
'settings' => [
'displayErrorDetails' => true,
'determineRouteBeforeAppMiddleware' => true
],
];
$container = new \Slim\Container($config);
$container['UsersCtrl'] = function($c){
return new \Controllers\UsersCtrl;
};
$app = new \Slim\App($container);
The $config is your application configuration. You can initialize it here. One thing to note of importance here is to include the “determineRouteBeforeAppMiddleware”. If you do not include it, you cannot read the current route objects in your middleware. We use this functionality when we want to implement role based control for our APIs.
Now we can create our middleware class and implement the token authentication and roles and user access. Few things will be happening here:
- Create a class construct function to receive the container objects
- Create an API access list based on each role, path, and HTTP method with ACL() function
- Deny access when we deny the user access to specific API with denyAccess() function
- Check user role against the access list with checkUserRole() function
- __invoke() class which must return response object
Here is my complete TokenAuth.php source code:
namespace Middleware;
use Slim\Http\Request;
use Slim\Http\Response;
class TokenAuth
{
private $container;
private $apiVersion;
//roles
private $userRole;
private $adminRole;
public function __construct($container) {
$this->container = $container;
$this->apiVersion = $container['version'];
/**
* roles are not dynamic
* role with higher access level is higher number
**/
$this->userRole = 1;
$this->adminRole = 2;
}
function ACL($path, $method){
//init access list
$accessList = array(
array(
'role' => $this->userRole,
'path' => $this->apiVersion . "/addresses",
'method' => ['GET', 'POST']
),
array(
'role' => $this->adminRole,
'path' => $this->apiVersion . "/users",
'method' => ['GET']
),
array(
'role' => $this->adminRole,
'path' => $this->apiVersion . "/products",
'method' => ['POST']
),
);
//search access list
foreach ($accessList as $value) {
foreach ($value['method'] as $valueMethod) {
if($value['path'] == $path && $valueMethod == $method){
return $value;
}
}
}
}
public function denyAccess(){
http_response_code(401);
exit;
}
public function checkUserRole($accessRule, $_userRole){
if($_userRole == 'user')
$_userRole = $this->userRole;
else if($_userRole == 'admin')
$_userRole = $this->adminRole;
//check the role access
if($_userRole >= $accessRule)
return true;
}
public function __invoke(Request $request, $response, $next)
{
$token = null;
if(isset($request->getHeader('token')[0]))
$token = $request->getHeader('token')[0];
//same format as api route
$route = $request->getAttribute('route');
$path = $route->getPattern();
$method = $request->getMethod();
$accessRule = $this->ACL($path, $method);
if(isset($accessRule) && $token != null){
$checkToken = $this->container->UsersCtrl->validateToken($token);
if($checkToken != null)
{
/**
* accessRule defined by dev
* checkToken retrieve from db
**/
if($this->checkUserRole($accessRule['role'], $checkToken['role'])){
$this->container->UsersCtrl->updateUserToken($token);
}
else
$this->denyAccess();
}
else
{
$this->denyAccess();
}
}
else if(isset($accessRule) && $token == null)
$this->denyAccess();
$response = $next($request, $response);
return $response;
}
}
construct() function
This function initialize your $container object and receive the dependencies. Here, we just assign it to our class variable. Next thing, we create two variables for user roles and a value for each of them. If you are having your user roles in a database, then retrieve it from there. We assign a numeric value for a role because it’s easier to compare. If you have your own solution, go with that.
ACL($path, $method) function
This function keeps and validates your access rules. You can break this function into more pieces for more complicated tasks, but here, it takes two parameters. $path is your API URL and $method is your HTTP method. When you want to use this function, send your API $path and HTTP $method. It will look for your URL and if it finds a match it will return the item, otherwise your route will pass through the middleware.
denyAccess() function
If the user request is unauthorized, this simply exits the request and return a 401 response.
checkUserRole($accessRule, $_userRole) function
This function validates the user role against a user role in your access list. Here, it will first translate our user role (the one we retrieve it from database) to the number and then simply compare it.
__invoke() function
And finally, the invoke function does the last work and bring everything together. We first retrieve the ‘token’ header, and after that, the ‘route’ objects to get the HTTP method and the URL pattern. When we get all these values, call the ACL() function to search for the access rule, and if we find a rule, we send the data to validate the token. If the token is valid, we validate the role and if the role is valid too, then update the user token in the database. I didn’t include the database interactions code here. They are generally SQL updates and select. If your user is a first-time login, you can create the token in the token validation function.
I hope you can use this solution for your next slim app. Of course, there are libraries out there that look into this issue so you can check them out too. Nevertheless, sometimes they get too complicated and I personally like simple solutions for simple problems. Comment here if you have a further question.