Authentication III: Return Of The Middleware
Our Story So Far
We've seen how to use middleware for simple authentication as well as a more complicated model-based authentication and authorization scheme. For most applications the former is far too simplistic, and if you're working on a reasonably complicated project, the latter has one large flaw: you have to remember to manually check for authentication and authorization, in every method that requires them. Eventually somebody will forget to add that one critical line when they're writing a new method, and then you have a problem.
One potential fix, since OX is really just Moose under the covers, is to use an
around method modifier in your controller classes. While that approach can be made to work, it's not the best solution. Instead, let's return to middleware -- custom middleware, to be precise.
A More Elegant Approach, For A More Civilized Application
We'll use the same
OXauth demo application as the earlier, model-based authentication example, but change it to use a custom middleware. On the application level, the change is pretty minor, we just add an additional
wrap_if statement to the
router block, that applies the authentication middleware if the request path matches
/admin. We also add a
name attribute for the
login action -- we'll see how and where that gets used shortly.
(The code for this version of the application is available in the OXauth repo in the
wrap_if line is given the PSGI request environment as an argument. We look to see if the path of the current request matches the part of the application that requires authentication. If that inline
sub returns a true value, the middleware will be included. Any arguments to the middleware will be resolved as Bread::Board services -- so this middleware will receive the
All the real action happens in the custom middleware, so let's look at that now.
The Middleware Is Strong In This One
"Custom middleware" sounds pretty daunting, but in the end it's not. You write a MooseX::NonMoose class that extends Plack::Middleware, and provide a
call callback method, which will get passed the PSGI environment. Writing the middleware as a Moose class means we can define attributes (
model, in the case below) which are provided via OX's use of Bread::Board service resolution.
The line that uses the
uri_for method is the reason why we added a
name attribute to the
/login route -- that allows us to have a consistent stable identifier for that routing even if the path part or the controller method that it invokes is renamed. (See the
uri_for entry earlier in the calendar for more information.)
The logic here is very simple -- if there's a
user_id key set in the session, we load up that user from the database. If there's a user in the session or if we're trying to load the login URL, we call the wrapped PSGI application instance, passing it the environment, and return the result -- which will allow for further routing or middleware application to happen.
If we don't have a user and aren't trying to login, we instead generate a redirect to the login URL.
All the code for the
logout methods in
OXauth::Controller::Auth remains exactly the same as in the earlier example (with one tiny addition -- the
logout method clears the
user key in the session hash in addition to
It's A Trap
The more observant may have noticed that the middleware code didn't actually need to check for whether the path matched the login URL -- because the middleware was only wrapped when the path matched
/admin, and the login URL (at
/login) would never be the path while the middleware was executing. We can easily make both checks necessary by adjusting the router block:
Since you're undoubtably using
uri_for in your templates as well, those will also automatically reflect this change, and the application will continue to function identically, with no other code changes needed. This should emphasize how the flexible mapping between URL routes and methods called on resolved services, along with the name-based lookups available via the
uri_for method, makes it trivial to reorganize your URL organization scheme or your code, independently of each other.
I Find Your Lack Of Authorization Disturbing
The earlier model-based example included an authorization component which this example lacks. Extending the middleware to support that would not be difficult, however. One approach would be to have a 'config' service that defines which roles have access to which paths, and then add that service as an additional parameter to the middleware, which could then add in a permissions check once a user was loaded. At that point it would probably be easier to include your custom authentication middleware unconditionally (i.e., via a normal
wrap statement) and use the
config service to determine both whether authentication was needed, and if so, what authorizations were also required.