Rest API authentication & security is crucial for most applications handling sensible information and user-specific data.
In this post we’ll discuss just one, token based authentication with PHP and the Slim micro framework (the logic can be applied to any routes framework or even if you have your own implementation).
First of all, if you still don’t know Slim, check it out, you wont regret it: http://www.slimframework.com/
Prerequisites:
- PHP Knowledge
- Slim basic knowledge
- REST understanding
Ok, so lets assume that you have a login page that will send the username and password to your API endpoint and it should return a token that the user will pass back on successive secured calls.
Our login endpoint can be http://www.webstreaming.com.ar/api/user/login which will receive the username and password, will verify against our DB and if everything is correct and then return something a JSON with the user information and token.
A good thing you can do on your DB is to set an “expiration” date to the token, this way you can verify if the user was inactive for a certain amount of time and the token gets expired, it just improves security.
1 2 3 4 5 6 7 8 9 10 |
CREATE TABLE IF NOT EXISTS `tbl_user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(45) NOT NULL, `password` VARCHAR(45) NOT NULL, `name` VARCHAR(45) NOT NULL, `token` CHAR(16) NULL, `token_expire` DATETIME NULL, PRIMARY KEY (`id`), UNIQUE INDEX `username_UNIQUE` (`username` ASC)) ENGINE = InnoDB |
This is what your PHP login function could look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public function login($username, $password) { if ($username == 'admin' && $password == 'mysecurepass') { //implement your own validation method against your db $arrRtn['user'] = 'Chuck Norris'; //Just return the user name for reference $arrRtn['token'] = bin2hex(openssl_random_pseudo_bytes(8)); //generate a random token $tokenExpiration = date('Y-m-d H:i:s', strtotime('+1 hour'));//the expiration date will be in one hour from the current moment CUser::updateToken($username, $arrRtn['token'], $tokenExpiration); //This function can update the token on the database and set the expiration date-time, implement your own return json_encode($arrRtn); } return false; } |
And this can be your return JSON:
1 2 3 4 |
{ user: "Chuck Norris", token: "1JUcleYpx7eO946uTaFv3ih4wkl9vcL1" } |
Once we have sent our credentials to the API and everything is correct, the front end should get this JSON and you can somehow store the token (cookies, local storage, etc) that will be used on future calls to the API that do need authentication.
After we’ve saved our precious token, we can use it on secured endpoint by simply adding the token to our request header. Let’s just use jQuery as an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
function updateProfile() { /* * lets assume that your browser can handle "local storage" for the sake of simplicity. * You should have saves the token to local storage prior to this. * * localStorage.token=myApiResult.token * * myApiResult would be the JSON we received after the login * **/ var tokenString = localStorage.getItem("token"); //simple ajax call to our API $.ajax({ url: "http://www.webstreaming.com.ar/api/user/", method: "PUT", headers: { Authorization: tokenString }, data: JSON.stringify({ skills: "Turn sand into gold", nickname: "God of War" }), statusCode: { 404: function () { //If the endpoint is not found, we'll end up in here alert("endpoint not found"); }, 200: function () { //Ok, everything worked as expected alert("worked like a charm"); }, 401: function () { //Our token is either expired, invalid or doesn't exist alert("token not valid or expired"); } } }); } |
Check out the headers parameter is being passed an object Authorization that it’s value is the token that we received from the API in the first place. This header parameter will help us pass the token in a secure way.
Now our endpoint http://www.webstreaming.com.ar/api/user will receive a string in the request body (the skills and nickname values) and the Authorization in the header.
To be able to get this header on every call to our API, we can create a Middleware in Slim that will handle this for us.
The purpose of middleware is to inspect, analyze, or modify the application environment, request, and response before and/or after the Slim application is invoked. It is easy for each middleware to obtain references to the primary Slim application, its environment, its request, and its response
A very simple implementation of a Slim Middleware could be the following
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
<?php class TokenAuth extends \Slim\Middleware { public function __construct() { //Define the urls that you want to exclude from Authentication, aka public urls $this->whiteList = array('\/user\/signin', '\/user\/signup', '\/project\/([a-z0-9]+)\/thumb\/([a-z0-9]+).([a-z]+)\/(.*)'); } /** * Deny Access * */ public function deny_access() { $res = $this->app->response(); $res->status(401); } /** * Check against the DB if the token is valid * * @param string $token * @return bool */ public function authenticate($token) { return \Subscriber\Controller\User::validateToken($token); } /** * This function will compare the provided url against the whitelist and * return wether the $url is public or not * * @param string $url * @return bool */ public function isPublicUrl($url) { $patterns_flattened = implode('|', $this->whiteList); $matches = null; preg_match('/' . $patterns_flattened . '/', $url, $matches); return (count($matches) > 0); } /** * Call * * @todo beautify this method ASAP! * */ public function call() { //Get the token sent from jquery $tokenAuth = $this->app->request->headers->get('Authorization'); //We can check if the url requested is public or protected if ($this->isPublicUrl($this->app->request->getPathInfo())) { //if public, then we just call the next middleware and continue execution normally $this->next->call(); } else { //If protected url, we check if our token is valid if ($this->authenticate($tokenAuth)) { //Get the user and make it available for the controller $usrObj = new \Subscriber\Model\User(); $usrObj->getByToken($tokenAuth); $this->app->auth_user = $usrObj; //Update token's expiration \Subscriber\Controller\User::keepTokenAlive($tokenAuth); //Continue with execution $this->next->call(); } else { $this->deny_access(); } } } } |
This middleware implements a public call() method that is executed on every call to our API. In our middleware this method simply retrieves the header Authorization and checks if the token is valid. If the token is valid it returns an instance of the user from the DB and makes it available through the Slim $app, so that way we can use the user object in our controller and we know who is the logged in user. Then we just update the token expiration and then call() the next inner middleware. If the token is not valid, we deny_access().
If you require any specific initialization you can use the constructor of the class.
There is one thing that you need to pay attention, in this example, the middleware will be executed for every API call, so when you call the /login endpoint, you will not be able to continue because you will not have any Authorization header yet, so for the sake of simplicity I did not included any url filtering in the example but you can implement this on your own.
Once we have our middleware ready, we need to add it so Slim knows about it, we do it this way:
1 2 3 4 5 6 7 8 9 |
<?php require_once('/Middleware/TokenAuth.php'); $app = new \Slim\Slim(); $app->config('debug', true); \Slim\Slim::registerAutoloader(); $app->add(new \TokenAuth()); |
We first require our middleware and then we use the $app->add() method to add it to the Slim execution stack.
Now we can then just create our controller that will handle the routes for the user.The class can look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
<?php namespace api\Controller; class User extends ApiController { protected $basePath = '/user'; public function __construct($app) { parent::__construct(); } public function index() { //login endpoint $this->app->post($this->basePath . "/login", array( $this, 'login')); //update endpoint $this->app->put($this->basePath, array( $this, 'updateUser')); } /** * Update method * * Will set the skills and nickname of the current logged in user */ public function updateUser() { //We set the $userAuth in the middleware, its the currently logged in user $userAuth = $this->app->auth_user; $body = json_decode($this->app->request->getBody(), true); $userAuth->setSkills($body['skills']); $userAuth->setNickname($body['nickname']); $userAuth->save(); $this->app->view()->setResponse(array('response' => 'success')); $this->app->render('json'); } /** * Login method * * Will verify the username and password */ public function login() { //Get the body and decode it $body = json_decode($this->app->request->getBody(), true); //Validate the username and password, if valid returns token, else will return false $token = \Subscriber\Controller\User::validateUser($body['email'], $body['password']); if ($token) { //Get the user object $user = \Subscriber\Controller\User::getUserByUsername($body['email']); $this->data = array("token" => $token, 'user' => $user->getName() ); //Set the response $this->app->view()->setResponse($this->data); } else { $this->app->view()->setResponse(null, 409, "Username or password incorrect"); } //Print the response $this->app->render('json'); } } |
The code is pretty much self explanatory, we have two endpoints, one for the login and one for the update.
There several benefits in using this kind of authencation over the typical username+password, lets check some:
- Speed – No need to encode/decode passwords on every API call
- Independency – You can use this logic on mobile apps, web apps, etc. No need for sessions on the server
- Entropy – Token hashes are a very good way to keep your system safe behind a set of characters
To sum up, this is a very reliable method to secure your information even if you are using your private API or if you are giving access to the public. The idea of this post is not to teach how to do it, but to give an idea on how it can be done, the rest is up to you.
Easy to learn, simple yet powerful microframework!
Thank you for this article. What I’ve in mind now is… how to manage token expiration in order to keep a user-friendly front-end? Long expiration date can compromise the token, short expiration period would be annoying. So… how to manage token renew? Thank you in advance.
Well, the simplest thing that comes to my mind is that just regenerate your token every x minutes or n api requests that way you can have tokens that will change and lower the risk, not sure if that’s what you meant.
Sir,
Are this method is secure like oauth2?
Or do you have tutorial to implement oauth2 to slim?
Thank you
Sorry for bad english
Hey Jagad, this is not an OAuth2 implementation. This is just a token based authentication. I don’t have any OAuth2 post but I can surely write one and let you know once its done! Can’t promise when though!
gabo.-
Source code?
Well I just implemented this,
But now if i do login witht he route
$app->post(“/user/login”……)
I get unauthorized error.
How do i exclude certain routes from authentication.
Harpreet, check the TokenAuth class, I’ve update the code so that it reflects a way you can create “public” urls and allow access to everyone.
cheers!
what is CUser
\Subscriber\Controller\User::keepTokenAlive($tokenAuth);
can any one explain
It’s just a method that would update the expiration date of a token so that a if a user is making requests to the api, the the token will not expire.
Thank you for good article.
i am a beginner , how can i work with Slim3 like this good article.
Thank you for writing it, very helpful.
Hello !
Could you give us a complete directory structure of your project please?
It would help us a lot to understand how all the files are organized (index.php, controllers, middleware, etc.).
Thanks a lot, great tutorial!
Tim
Tim, what you would probably want to do is use Composer to maintain libraries (Slim) and that would use it’s default /vendor folder, so the structure I would go with would be like this:
-vendor
—-Slim (this folder will be maintained by composer, do not place your sourcecode inside it)
-api (folder where you will place all your custom API related source)
—-index.php (this file will glue all controllers, middlewares, views and the Slim microframework)
—-controllers
——–UserController.php
——–OtherController.php
—-middleware
——–AuthMiddleware.php
—-views
——–ApiViews.php (Custom views for different rendering, json, xml, etc)
Hope it helps!
It’s a bad way. Because, If you generate a key for each connections. You assume only one device online all time.
Initerworker, on logins, you can just check if you already have a non-expired token in the db and return that same token instead of creating a new one, that way you can have multiple devices at the same time for the same price!
There is a type in login.php at line 10, where
CUser::updateToken($username, $arr[‘token’], $tokenExpiration);
It should be
CUser::updateToken($username, arrRtn[‘token’], $tokenExpiration);
Excellent article!!!!
good catch, updated 😉
The db token column is length 16, but the generation token is length 32.
That gave me an error when trying to save to the db. Since I’m the first one mentioning it, did others just change the db length to 32? Or is there something I’m missing?
Yes, I made this change to DB structure too.
You are correct! Updated the code to reflect this. 😉
Great article!
What happens if the same random token is generated twice? Wouldn’t that be a problem for getByToken() ?
Yes, that will be a problem. It is very difficult for that to happen, the odds are just too low. Anyway, you can verify that a newly generated token does not already exist on the db before you update the row.
Does this still work on Slim 3? They changed the way they use Midleware. It’s giving me an error Fatal error: Class ‘Slim\Middleware’ not found
yes
can you please provide me complete source code..??
so to verify the access token we have to hit the query to the database?
Bilal, you can actually have the tokens on a REDIS or any in-memory storage for performance improvement
Please, can you share a zip with the full code?
Hello!
I would like to know what version of Slim you are using please.
Regards
Eduardo, this was done using Slim V2, the whole concept will work on Slim 3, maybe there is a method that changed its name or signature. Take a look at Slim’s doc to make sure! cheers.
i was wondering about extending the life of a token. If i go and extend it wouldn’t that mean that the actual token changes since it is encoded and if i change the value of the expiration that means the encoded value changes. if that’s the case i would have to return a new token in one of the responses to user so the app knows to replace the old token. I actually went one further to restrict a token to user, ip and device. I create a hash based on the client agent and compare that as well as the ip stored in token with the one using token, that way i prevent someone from hijacking the token. Sometimes user will require new token if his ip changes or he upgrades browser version. The only issue i am still unsure how to find a balance of expiration and renew or extend.
Hello Conmfuser Code.
I am trying and implement the best solution, but at the same time, the simplest, using tokens and login to consume APIs with Slim3, have you achieved something that we can share and show me or guide me step by step?