Jacquard 03: Setting Up Authentication

In the last installment, we created Jacquard::Schema::User to describe a Jacquard user, and used Jacquand::Model::KiokuDB (and a helper script) to persist User objects to storage. This time around, we’re going to create our Catalyst app and hook up authentication.

So, first step: create the Catalyst application. The catalyst.pl helper makes this trivial:

$ catalyst.pl Jacquard::Web
created "Jacquard-Web"
created "Jacquard-Web/script"
created "Jacquard-Web/lib"
created "Jacquard-Web/root"
[ couple dozen more lines elided ]

Unfortunately, there’s no easy way to tell it “I’m creating this Cat app as a part of a bigger project” – so after creating it, you have to manually shuffle the files around to get them into the right place. The Catalyst files also come with quite a bit of templated stuff in them that we don’t need (POD skeletons and the like), so cleaning that up and getting the code into line with your personal coding style should be done at this point. Once that’s all done, checkpointing into revision control is a good idea:

$ git ci -m"Create Catalyst application"
[03-catalyst 3c20771] Create Catalyst application
11 files changed, 300 insertions(+), 0 deletions(-)
create mode 100755 jacquard_web.psgi
create mode 100644 lib/Jacquard/Web.pm
create mode 100644 lib/Jacquard/Web/Controller/Root.pm
create mode 100644 root/favicon.ico
create mode 100644 root/static/images/btn_88x31_built.png
create mode 100755 script/jacquard_web_cgi.pl
create mode 100755 script/jacquard_web_create.pl
create mode 100755 script/jacquard_web_fastcgi.pl
create mode 100755 script/jacquard_web_server.pl
create mode 100755 script/jacquard_web_test.pl

Next, we’re going to use CatalystX::SimpleLogin to add authentication to the application, following the outline in the manual. First up, add a number of plugins to the application (in lib/Jacquard/Web.pm):

use Catalyst qw/
-Debug
ConfigLoader
+CatalystX::SimpleLogin
Authentication
Session
Session::Store::File
Session::State::Cookie
Static::Simple
/
;

Create a default View class by running:

./script/jacquard_web_create.pl view TT TT

Then create the Catalyst Model class that will wrap Jacquard::Model::KiokuDB:

package Jacquard::Web::Model::KiokuDB;
use Moose;

use Jacquard::Model::KiokuDB;

BEGIN { extends qw(Catalyst::Model::KiokuDB) }

has '+model_args' => ( default => sub { { extra_args => { create => 1 }}});
has '+model_class' => ( default => 'Jacquard::Model::KiokuDB' );

__PACKAGE__->meta->make_immutable;
1;

We also need to add the configuration for these plugins to the application config file:

---
name: Jacquard::Web
Model::KiokuDB:
dsn: dbi:SQLite:dbname=db/jacquard.db
Plugin::Authentication:
default:
credential:
class: Password
password_type: self_check
store:
class: Model::KiokuDB
model_name: kiokudb

Finally, we can add a method requiring authentication to the root Controller (which is at lib/Jacquard/Web/Controller/Root.pm):

sub hello_user :Local :Does('NeedsLogin') {
my( $self , $c ) = @_;

my $name = $c->user->id;

$c->response->body( "<h2>Hello, $name!</h2>" );
}

(Don’t forget that you need to change the parent class of the controller as well:

BEGIN { extends 'Catalyst::Controller::ActionRole' }

If things aren’t working, make sure you didn’t forget this.)

With all this in place, you should be able to start up the application, by either running ./script/jacquard_web_server.pl or, if you want to get all Plack-y and modern, plackup jacquard_web.psgi, and then browse to the URL the server reports it’s running on. You should see the Catalyst welcome page at that point. Add ‘/hello_user’ to the end of the URL, and you should get prompted for a username and password. Assuming you created an account using the helper script from the last installment, you should be able to give those same values and see a page that says ‘Hello’ and your test account username.

Now, are we done? Not quite, as we don’t have any tests of the authentication code! The following goes in t/lib/Test/Jacquard/Web/Controller/Root.pm:

package Test::Jacquard::Web::Controller::Root;
use parent 'Test::BASE';

use strict;
use warnings;

use Test::Most;
use Test::WWW::Mechanize::Catalyst;

sub fixtures :Tests(startup) {
my $test = shift;

# load test config
$ENV{CATALYST_CONFIG_LOCAL_SUFFIX} = 'test';

$test->{mech} = Test::WWW::Mechanize::Catalyst->new( catalyst_app => 'Jacquard::Web' );
}

sub auth_test :Tests() {
my $test = shift;
my $mech = $test->{mech};

$mech->get_ok( '/' , 'basic request works' );
is( $mech->uri->path , '/' , 'at top page' );

$mech->get_ok( '/hello_user' , 'request hello_user' );
is( $mech->uri->path , '/login' , 'redirect to login' );

$mech->submit_form_ok({
form_id => 'login_form' ,
fields => {
username => 'test_user' ,
password => 'test_password' ,
} ,
button => 'submit' ,
} , 'login' );

is( $mech->uri->path , '/hello_user' , 'redirect to /hello_user' );
$mech->text_contains( 'Hello, test_user!' , 'see expected greeting' );

$mech->get_ok( '/' , 'basic request works' );
is( $mech->uri->path , '/' , 'at home page' );

$mech->get_ok( '/hello_user' , 'request hello_user' );
is( $mech->uri->path , '/hello_user' , 'go directly to hello_user' );
$mech->text_contains( 'Hello, test_user!' , 'still see expected greeting' );

$mech->get_ok( '/logout' );
is( $mech->uri->path , '/' , 'back at home page' );

$mech->get_ok( '/hello_user' , 'request hello_user' );
is( $mech->uri->path , '/login' , 'redirect to login' );
}


1;

If you’re not familiar with Test::Class-style testing, you should review the links I gave in the post on the Jacquard project infrastructure. Even if you’re not that versed in Test::Class, hopefully the sequence above is fairly self-explanatory: we try to access a restricted URL, verify we get bounced to the login URL, then fill out and submit the login form, and verify we get re-directed to the original request. Following that, we logout and verify that we’re once again unable to get to the restricted resource. Fairly simple.

In order to make this work, we need a distinct application config for testing, one that defines a test user and password for us. Catalyst, of course, provides a way to do this – that’s what that line that sets the CATALYST_CONFIG_LOCAL_SUFFIX environment variable is about. The testing config goes into jacquard_test.yaml, and looks like this:

---
Plugin::Authentication:
default:
credential:
class: Password
password_field: password
password_type: clear
store:
class: Minimal
users:
test_user:
password: 'test_password'

With that test file and test config in place, we can run the tests:

$ prove -l -I./t/lib ./t/lib/Test/Jacquard/Web/Controller/Root.pm -v
./t/lib/Test/Jacquard/Web/Controller/Root.pm .. #
# Test::Jacquard::Web::Controller::Root->auth_test

ok 1 - basic request works
ok 2 - at top page
ok 3 - request hello_user
ok 4 - redirect to login
ok 5 - login
ok 6 - redirect to /hello_user
ok 7 - see expected greeting
ok 8 - basic request works
ok 9 - at home page
ok 10 - request hello_user
ok 11 - go directly to hello_user
ok 12 - still see expected greeting
ok 13 - GET /logout
ok 14 - back at home page
ok 15 - request hello_user
ok 16 - redirect to login
1..16
ok
All tests successful.
Files=1, Tests=16, 4 wallclock secs ( 0.04 usr 0.00 sys + 3.67 cusr 0.20 csys = 3.91 CPU)
Result: PASS

and verify they pass. This is also a good point to rerun the whole test suite:

$ prove -l
t/01-run.t .. ok
All tests successful.
Files=1, Tests=26, 6 wallclock secs ( 0.03 usr 0.01 sys + 4.16 cusr 0.27 csys = 4.47 CPU)
Result: PASS

So, at this point, we have a basic data model (which only models users – but that’s about to change!), a KiokuDB-based persistence framework, and we’ve got all that hooked into a Catalyst-based web application and can use the info in our User object to authenticate to the web app. Sounds like a good place to take a break! In our next installment, we’ll pull back from the coding just a bit and think about the best way to structure the data involved in the services we’re going to connect to, and the posts we’re going to read and write to those connections.

As always, the code for Jacquard is available on Github. Patches are welcome; share and enjoy.

Update: The Jacquard story continues with “Extending The Data Model”.