Authentication in Maypole

There are two ways to control authentication to an action in Maypole.

The first is to create an authenticate method in the relevant model class; if you want everyone to be able to mess with beers but only certain people to be able to edit the breweries list, then your should write BeerDB::Brewery::authenticate. The second way is to provide authentication globally in your driver class.

Maypole will check authentication in that order - first, in the model class, then globally. If there's no method found in the model class hierarchy (with inheritance) then Maypole checks for BeerDB::authenticate. There is no fallback; if you provide a table-specific authentication mechanism and that method fails, then the global authentication method is not called.

In either case, you need to decide whether or not a request is permissible, and to do that, you need to examine the request to see what the user is trying to do. Maypole passes you the Maypole request object to examine:

sub authenticate {
    my ($self, $r) = @_;
    return OK;
}

(This might feel a bit weird if you're overriding authenticate in your handler class, because $r is $self. But if you don't worry about that, everything will be fine.)

It's up to you what you decide to do if you don't want to authenticate the request. You could just deny access, returning an Apache 403, and letting the server sort it out; to do this, return FORBIDDEN. (If things don't work whether you return OK or FORBIDDEN, make sure you're importing the status code constants from the Apache::Constants module.) Or you could use the Maypole framework itself to send the user elsewhere.

For instance, one common set-up is to not allow the user to do anything unless they're logged in. We'll pretend that we've already implemented a get_user method to determine if they are or not - this populates $r->{user} with an appropriate object. If $r->{user} isn't there, they're not coming in! We set the template to login, which pops up a login form and has them try again.

sub authenticate {
    my ($self, $r) = @_;
    $r->get_user;
    return OK if $r->{user};
    $r->template("login");
    return OK;
}

Or we could allow them to only do specific things if they're not logged in. Such as register an account, for instance, so they actually can log in:

sub authenticate {
    my ($self, $r) = @_;
    $r->get_user;
    return OK if $r->{user};
    return OK if $r->action eq "register" and $r->table eq "user";
    $r->template("login");
    return OK;
}

Maybe we want to be really polite and not just completely derail them to force them to log in, but allow them to continue where they were going once they successfully submit credentials. Actually, this is really trivial. All we need to do is reconstruct the URL in our template, and have the login form post the credentials back to that URL.

When Maypole goes through the authentication phase for the second time, hopefully this time the username and password will be right, we'll be able to get a user object, and we'll sail straight into the right action: (I didn't realise it was going to be that easy, either. We're learning together, here.)

[% SET args = request.args.join("/"); %]
  <h2> You need to log in </h2>  
  <FORM ACTION="[% base %]/[%request.table%]/[% request.action %]/[%args%]">
Username: <INPUT TYPE="text" NAME="username"> <BR>
Password: <INPUT TYPE="password" NAME="password"> <BR>
</FORM>

Now, what if we want to tell them that they got the password wrong? Assuming that our get_user method returns some useful status code, we can pass a message onto the template:

sub authenticate {
    my ($self, $r) = @_;
     $r->{template_args}{error} = "Bad username or password"
          unless $r->get_user;
    return OK if $r->{user};
    return OK if $r->action eq "register" and $r->table eq "user";
    $r->template("login");
    return OK;
}

Which gets picked up like so:

[% SET args = request.args.join("/"); %]
  <h2> You need to log in </h2>  
  <FORM ACTION="[% base %]/[%request.table%]/[% request.action %]/[%args%]">

[% IF error %]
   <FONT COLOR="#FF0000"> [%error %] <FONT>
[% END %] 

Username: <INPUT TYPE="text" NAME="username"> <BR>
Password: <INPUT TYPE="password" NAME="password"> <BR>
</FORM>

How get_user actually works is up to you; here's one I made earlier:

sub get_user {
    my $r = shift;
    my $ar = $r->{ar};
    my $sid;
    my %jar = Apache::Cookie->new($ar)->parse;
    if (exists $jar{sessionid}) { $sid = $jar{sessionid}->value(); }
    $sid = undef unless $sid; # Clear it, as 0 is a valid sid.
    my %session = ();
    my $new = !(defined $sid);
    my ($uid, $user);

    if ($new) {
        # Go no further unless login credentials are right.
        ($uid, $r->{user}) = $r->check_credentials;
        return 0 unless $uid;
    }
    tie %session, 'Apache::Session::File', $sid, {
        Directory     => "/tmp/sessions",
        LockDirectory => "/tmp/sessionlock",
    };
    if ($new) {
        # Store the userid, and bake the cookie
        $session{uid} = $uid;
        $ar->log->debug("Returning session ".$session{_session_id}." to the cookie jar");
        my $cookie = Apache::Cookie->new($ar,
            -name => "sessionid",
            -value => $session{_session_id},
            -path => "/"
        );
        $cookie->bake();
    } else {
        # Grab the user object from the session data.
        $r->{user} = Flox::User->retrieve($session{uid});
    }
    untie %session;
    return 1;
}

The referenced check_credentials routine just looks up the username and password provided in $r->{params} and returns the ID and object of a Flox::User entry corresponding to the right user. This is probably overkill, but it works for me - you can use it as the Maypole::Authentication::UserSessionCookie module.