A client project stymied me for a little bit this week. One facet of the application revolves around user account creation and authentication. I'm using Catalyst for the web side and DBIC for the database side.
For now, I want to be able to create a user account, sanitize the information, and then immediate log in the user and redirect to another URI which requires user authentication. I use CatalystX::SimpleLogin to provide login/logout actions, as well as the DBIC password and session cookie options for Catalyst::Plugin::Authentication. With minimal changes to my database as originally planned, user passwords get hashed with Digest::SHA1 for an additional layer of paranoia.
CatalystX::SimpleLogin
provides chained actions which will enforce login/logout to protect other actions which require authentication. I have a two base actions in the User controller which perform setup of the relevant Resultset, among other things:
sub base :Chained('/') :PathPart('users') :CaptureArgs(0)
{
my ( $self, $c ) = @_;
$c->stash( users_rs => $c->model( 'DB::User' ) );
}
sub authbase :Chained('/login/required') :PathPart('users') :CaptureArgs(0)
{
my ( $self, $c ) = @_;
$c->stash( users_rs => $c->model( 'DB::User' ) );
}
The action to create a user chains off of base
, so it does not require authentication:
sub add :Chained('base') :PathPart('add') :Args(0)
{
...
}
After validating various parameters, it finally attempts to create the new user in the database:
my $newuser = try
{
$users_rs->create( \%user_params )
}
catch
{
$c->stash(
user => \%user_params,
errors => $_,
);
return;
};
return unless $newuser;
At this point, $newuser
represents the new user. Here comes
the fun part:
# remove any credentials and force new authentication
$c->logout();
$c->authenticate({
username => $params->{username},
password => $params->{password},
});
If there's an existing authorized user session, this code invalidates it,
then authenticates with the new user's data. (An earlier version of this code
used user data from $newuser
itself, but I couldn't explain to
myself why the SHA-1 password from $newuser->password
should
work.) There shouldn't be an existing user session, but
$c->logout()
is a cheap way to avoid trouble.
# force simple auth to reauthenticate
$c->change_session_id;
$c->visit('/login/login');
If there's an existing session, the id should change to reflect the new
authentication properties. Again, this is a cheap way to avoid trouble. The
really interesting part of the code is the second line, which performs the
login action, using the current-but-updated credentials, and immediately
returns to the current action. This makes CatalystX::SimpleLogin
do the right thing on the next request without the user knowing anything about
a login form.
# and display the profile page
$c->res->redirect( $c->uri_for( 'profile', $newuser->id ) );
$c->detach();
return;
To avoid the double-POST problem, this action finally redirects to the
"Hooray, you've created a new user!" action. This simple HTTP redirect ends up
in the profile
action:
sub profile :Chained('authbase') :Path :Args(1)
{
...
}
Because of the chain to authbase
, anyone who visits the
/usrs/profile/id
URI must log in, whether manually
through the CatalystX::SimpleLogin
form or automatically after
having created a new account.
At some point in the future I may need to change the application to perform email validation or admin approval of new accounts, but for now it seemed easiest to assume that users who create new accounts want to use those new accounts automatically.