Inline Authentication Without Redirection

Earlier this week Jay Kuri covered Adding Authentication to your Catalyst App, which showed you how to determine if the user needed to authenticate, and if they did to redirect them to a login controller. Today I'm going to show you an alternate approach, one that doesn't require redirection, that just handles the login inline.

Why would you want to handle the login inline, rather than redirecting to a login page? Have you ever had to login to a web application and fill out a lengthy form, only to find out when you go to submit that your session has timed out and you need to login again? You end up having to do all kinds of contortions to use applications like this, things like opening up another window, logging in, and then going back to the original window to submit the form again. This inline technique can overcome that, by allowing the login form to carry on your submitted form data along with it.

Creating the auth controller

When I use this method, I like to isolate all the authentication pieces into their own controller. We'll start it out the same way any old controller starts out.

    package MyApp::Controller::Auth;
    use strict;
    use warnings;
    use base qw( Catalyst::Controller );

Next we need an action that can check whether the user needs to be authenticated or not.

    sub check_login : Private {
        my ( $self, $c ) = @_;

        if ( $c->user_exists ) { return 1 }

        my $username = delete $c->request->params->{ '__username' };
        my $password = delete $c->request->params->{ '__password' };

        if ( $username && $password ) {
            return 1 if $c->authenticate( {
                username    => $username,
                password    => $password,
            } );
            $c->stash->{ 'error_msg' } = 'Incorrect username or password';
        }

        $c->stash->{ 'template' } = 'auth/login.tt2';
        return 0;
    }

The login template

To go along with your controller, you need a login template, in root/auth/login.tt2.

    <form method="post">
        [% IF error_msg %]
        <span class="error">[% error_msg %]</span>
        [% END %]

        <label for="__username">Username:</label>
        <input type="text" name="__username" size="40" />
        <br />

        <label for="__password">Password:</label>
        <input type="password" name="__password" size="40" />
        <br />

        [% FOREACH p IN Catalyst.request.params.pairs %]
            [% NEXT IF p.key.matches( '^__' ) %]
            <input type="hidden" name="[% p.key %]" value="[% p.value %]" />
        [% END %]

        <input type="submit value="Login" />
    </form>

How to use it

To use this, all you need to do is at any point where you want the user to login, forward them to the check_login action in your auth controller. So for example, if you want to have every page in your application protected, put this in your root controller.

    sub auto : Private {
        my ( $self, $c ) = @_;

        $c->forward( '/auth/check_login' ) || return 0;
        return 1;
    }

How it works

The way this works is by shortcutting the normal dispatch process if the user needs to authenticate. If authentication is required, then the template gets set to the login form, and we add any form data as hidden fields to the login form, then detach to the template processor to render it. When the form is submitted, it gets submitted to the URL the user originally requested, which means you don't have to keep track of what they were requesting in order to redirect them back to it after the login is complete.

When they submit the form, the check_login method is going to see that the username and password fields were populated, and attempt to log them in with those credentials. It will also remove those entries from the $c->request->params hash so they won't conflict with the real form parameters (this is also why they are named with two leading underscores, with the assumption that your real forms won't have fields with those names, if that assumption is incorrect then you may have to modify the field names used here.

If the login is successful, then the dispatch process will continue as normal, passing the request data on to whatever action was supposed to get it in the first place. If they login was not successful, the get the login form again.

AUTHOR

Jason Kohles, <email@jasonkohles.com>

http://www.jasonkohles.com/