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 middleware-auth
branch.)
1: | package OXauth; |
That 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 model
service.
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.
1: | package OXauth::Middleware::Auth; |
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 login
and 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 user_id
.)
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:
1: | router as { |
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.