I spend most of my day switching between languages. Sometimes I start the morning with an early dose of LUA, then I get a lot of Python and C++, followed by the yummy dessert that is Node.js. But there’s always room for PHP. So let’s talk about PHP today.
People often think that when you are coding PHP you have to do things “the PHP way”. Well, let me clear it up for you: there’s no such thing as “the PHP way”. If there’s something that defines PHP is its flexibility to be as ugly or as beautiful as you want it to be. With that in mind, what prevents you from taking the lessons learned in one language to another?
That’s the premise I always have when I’m coding. Let me give you a for instance. On Node.js, I normally use express.js as a framework. Check it out, it’s pretty awesome. One of the things I love in express.js, is its route middleware capabilities. When I code in any PHP framework, that’s one of the things I miss the most.
Not many PHP frameworks are flexible enough to support such concepts. One of them is, though. Lithium has many of the things I love about PHP (5.3+ obviously, as I consider anything < 5.3 a waste of my time lately), and some of the things I love about other languages. One of them is Lithium’s addiction to closures. I love it. They allowed me to take the concept of express.js’ route middleware and apply it to my PHP code.
Let’s start with a dummy application skeleton. A users table:
CREATE TABLE `users`( `id` INT NOT NULL AUTO_INCREMENT, `email` VARCHAR(255) NOT NULL, `password` VARCHAR(255) NOT NULL, PRIMARY KEY(`id`) ); INSERT INTO `users`(`email`, `password`) VALUES( 'test@email.com', '$2a$04$U7qYPVYq2YBxqfHL8F2pteERxQYwLTVtAjMIh48Lef9sLSiMVtGHy', 'john@email.com', '$2a$04$U7qYPVYq2YBxqfHL8F2pteERxQYwLTVtAjMIh48Lef9sLSiMVtGHy' );
In your app/config/bootstrap/connections.php
, make sure you uncomment the default database and hook it up to the database owning the table we just created. We’ll also be using sessions, so uncomment the session.php
reference in app/config/bootstrap.php
. You may have noticed that our initial users have a password set to a specific value, which means a specific salt was used (the password hashed there in plain text is ‘password’, minus the quotes.). So go ahead and add the following to your app/config/bootstrap/session.php
file:
use lithium\security\Auth; use lithium\security\Password; $salt = '$2a$04$U7qYPVYq2YBxqfHL8F2pte'; Auth::config(array(¬ 'adapter' => 'Form', 'model' => 'Users', 'filters' => array('password' => function($text) use($salt){ return Password::hash($text, $salt); }), 'validators' => array( 'password' => function($form, $data) { return (strcmp($form, $data) === 0); } ), 'fields' => array('email', 'password') ));
The salt was generated with a call to \lithium\security\Password::salt('bf', 4)
. I always use blowfish for password hashing (and 2^16 iterations in production). If you don’t use blowfish, here’s why you should. Anyway so you may want to store the hash on a better, configurable approach. I opted for a simple variable for this example. Once the salt is defined, I went ahead and configured Auth
to use lithium’s Password::hash()
method for hashing using the generated salt, and telling it how to compare hashed passwords against the database value. Pretty simple.
Let’s now build the Users
model. It won’t have anything in there, really. So just create your app/models/User.php
file with the following contents:
<?php namespace app\models; class Users extends \lithium\data\Model { } ?>
Now the controller. Create a file named app/controllers/UsersController.php
with the following contents:
<?php namespace app\controllers; use lithium\security\Auth; class UsersController extends \lithium\action\Controller { public function login() { if (!empty($this->request->data)) { $user = Auth::check('default', $this->request); if ($user) { $this->redirect(array('action' => 'view', 'id' => $user['id']), array('exit' => true)); } } } public function logout() { Auth::clear('default'); } } ?>
Nothing really complicated there. Don’t forget the view in app/views/users/login.html.php
:
<?php echo $this->form->create(); echo $this->form->field('email'); echo $this->form->field('password', array('type' => 'password')); echo $this->form->submit('Login'); echo $this->form->end(); ?>
That should give you a working login / logout. Add some dummy actions to the UsersController.php
file:
public function view() { echo 'view'; $this->_stop(); } public function edit() { echo 'edit'; $this->_stop(); }
Ok now we are ready to play with some route middleware. What we want to achieve is the following:
- No action named
edit
, on any controller, should be accessible without a logged in user. - When accessing either the
Users::edit
orUsers::view
action, there should be an ID specified as a route parameter, and it should match an existingUser
record. - When accessing the
Users::edit
action, the given user should match the currently logged in user.
These are pretty basic security checks that you would normally put on the controller. Not this time. Edit your app/config/routes.php
file and add the following right below the use
statements found at the beginning of the file:
use lithium\net\http\RoutingException; use lithium\action\Response; use lithium\security\Auth; use app\models\Users;
These are all classes that we will use in our route middleware. Let’s start with the first checkpoint we want to achieve: “No action named edit
, on any controller, should be accessible without a logged in user“. Add the following to the routes.php
file, below the content we just added:
Router::connect('/{:controller}/{:action:edit}/?.*', array(), array( 'continue' => true, 'handler' => function($request) { if (!Auth::check('default')) { return new Response(array('location' => 'Users::login')); } } ));
The first parameter (continue
) ensures that this route definition is treated as a continuation route. This is because we don’t want to interrupt any normal route / parameter processing in this definition. We just wanna “grab” all calls to any edit
action, and check (using Auth
) for a valid user. If none is found, we process the request by returning a Response
, which in the end redirects the user to the login page. If there is indeed a logged in user, the router will continue looking for other route definitions to match the request. So now all edit
actions require a logged in user. Cool.
Next in our list: “When accessing either the Users::edit
or Users::view
action, there should be an ID specified as a route parameter, and it should match an existing User
record.” Add the following to the routes.php
file, below the content we just added:
Router::connect('/{:controller:users}/{:action:edit|view}/{:id:\d*}', array('id' => null), function($request) { if (empty($request->params['id'])) { throw new RoutingException('Missing ID'); } elseif (!Users::first(array('conditions' => array('id' => $request->params['id'])))) { throw new RoutingException('Invalid ID'); } });
We are now getting more serious. In this definition, we are only matching the Users
controller, and actions named either edit
or view
, which may or may not contain an id
parameter. The route handler first checks to make sure the id
parameter is given (if not, a RoutingException
is thrown.) If the parameter is specified, it is used to find a matching User
record with the given ID. If none is found, yet another RoutingException
is thrown (you may wish to do something different here, like ensuring a 404 status). If the user is found, the route is not handled, which means some other route definition will handle it (the default route, in this case.)
The final checkpoint we have is: “When accessing the Users::edit
action, the given user should match the currently logged in user.” So add the following to the routes.php
file, below the content we just added:
Router::connect('/{:controller:users}/{:action:edit}/{:id:\d+}', array('id' => null), function($request) { $user = Auth::check('default'); if ($user['id'] != $request->params['id']) { throw new RoutingException('You can only edit your own account'); } return $request; });
This defines a specific match to the Users::edit
action with a set id
parameter. We use that parameter to make sure it matches the ID of the logged in user. If it doesn’t match, we throw a RoutingException
. If it does match, we return the request as we have successfully processed it.
You can now try accessing the edit and view actions using different scenarios: with a logged in user, while being logged out, editing a user which is not the current logged in user, etc. Everything should be nicely protected. And yet our controller code remained untouched. Nice, huh? That’s routing middleware for you.
No related posts.
AgBorkowski wrote:
hi, ur solution is nice but i think more transparent and “beuty” is put A&A process to dispatcher
bootstrap/action.php
Dispatcher::applyFilter(‘_callable’, function($self, $params, $chain) {
….
more code is on https://github.com/agborkowski/li3_access
Link
mariano wrote:
I agree, and I in fact use route filtering for access check, but this was another approach worth discussing
Link
matths wrote:
if it’s routing what you’re interested in, then also check out the Slim micro framework.
I have a lot of hosting situation, where I can’t use PHP >= 5.3, so Slim easily falls back to 5.2. As much as I like closures in JS, it’s still very early for closures in the PHP world.
But your article is a really nice read! Keep on doing!
Link