=head1 NAME
-Catalyst::Manual::Tutorial::Authentication - Catalyst Tutorial - Part 4: Authentication
+Catalyst::Manual::Tutorial::Authentication - Catalyst Tutorial - Part 5: Authentication
=head1 OVERVIEW
-This is B<Part 4 of 9> for the Catalyst tutorial.
+This is B<Part 5 of 10> for the Catalyst tutorial.
L<Tutorial Overview|Catalyst::Manual::Tutorial>
=item 3
-L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD>
+L<More Catalyst Basics|Catalyst::Manual::Tutorial::MoreCatalystBasics>
=item 4
-B<Authentication>
+L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD>
=item 5
-L<Authorization|Catalyst::Manual::Tutorial::Authorization>
+B<Authentication>
=item 6
-L<Debugging|Catalyst::Manual::Tutorial::Debugging>
+L<Authorization|Catalyst::Manual::Tutorial::Authorization>
=item 7
-L<Testing|Catalyst::Manual::Tutorial::Testing>
+L<Debugging|Catalyst::Manual::Tutorial::Debugging>
=item 8
-L<AdvancedCRUD|Catalyst::Manual::Tutorial::AdvancedCRUD>
+L<Testing|Catalyst::Manual::Tutorial::Testing>
=item 9
-L<Appendices|Catalyst::Manual::Tutorial::Appendices>
+L<Advanced CRUD|Catalyst::Manual::Tutorial::AdvancedCRUD>
-=back
+=item 10
+L<Appendices|Catalyst::Manual::Tutorial::Appendices>
-=head1 IMPORTANT NOTE
+=back
-Since this tutorial was written, there has been a new Authentication
-API released (Catalyst::Plugin::Authentication version 0.1 and later).
-Some of this tutorial does not work with this API, and requires
-minimal changes. For an example application that uses the new API see
-L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/NewAuthApp/NewAuthApp-0.01.tar.gz>. It
-is recommended that you read this tutorial first, and then download
-the source code linked above to understand the differences.
=head1 DESCRIPTION
Now that we finally have a simple yet functional application, we can
focus on providing authentication (with authorization coming next in
-Part 5).
+Part 6).
This part of the tutorial is divided into two main sections: 1) basic,
cleartext authentication and 2) hash-based authentication.
You can checkout the source code for this example from the catalyst
subversion repository as per the instructions in
-L<Catalyst::Manual::Tutorial::Intro>
+L<Catalyst::Manual::Tutorial::Intro|Catalyst::Manual::Tutorial::Intro>.
+
=head1 BASIC AUTHENTICATION
First, we add both user and role information to the database (we will
add the role information here although it will not be used until the
-authorization section, Part 5). Create a new SQL script file by opening
+authorization section, Part 6). Create a new SQL script file by opening
C<myapp02.sql> in your editor and insert:
--
=head2 Add User and Role Information to DBIC Schema
-This step adds DBIC-based classes for the user-related database tables
-(the role information will not be used until Part 5):
-
-Edit C<lib/MyAppDB.pm> and update the contents to match (only the
-C<MyAppDB =E<gt> [qw/Book BookAuthor Author User UserRole Role/]> line
-has changed):
-
- package MyAppDB;
-
- =head1 NAME
-
- MyAppDB -- DBIC Schema Class
-
- =cut
-
- # Our schema needs to inherit from 'DBIx::Class::Schema'
- use base qw/DBIx::Class::Schema/;
-
- # Need to load the DB Model classes here.
- # You can use this syntax if you want:
- # __PACKAGE__->load_classes(qw/Book BookAuthor Author User UserRole Role/);
- # Also, if you simply want to load all of the classes in a directory
- # of the same name as your schema class (as we do here) you can use:
- # __PACKAGE__->load_classes(qw//);
- # But the variation below is more flexible in that it can be used to
- # load from multiple namespaces.
- __PACKAGE__->load_classes({
- MyAppDB => [qw/Book BookAuthor Author User UserRole Role/]
- });
-
- 1;
-
+Although we could manually edit the DBIC schema information to include
+the new tables added in the previous step, let's use the C<create=static>
+option on the DBIC model helper to do most of the work for us:
+
+ $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
+ create=static components=TimeStamp dbi:SQLite:myapp.db
+ exists "/root/dev/MyApp/script/../lib/MyApp/Model"
+ exists "/root/dev/MyApp/script/../t"
+ Dumping manual schema for MyApp::Schema to directory /root/dev/MyApp/script/../lib ...
+ Schema dump completed.
+ exists "/root/dev/MyApp/script/../lib/MyApp/Model/DB.pm"
+ $
+ $ ls lib/MyApp/Schema/Result
+ Authors.pm BookAuthors.pm Books.pm Roles.pm UserRoles.pm Users.pm
-=head2 Create New "Result Source Objects"
+Notice how the helper has added three new table-specific result source
+files to the C<lib/MyApp/Schema/Result> directory. And, more
+importantly, even if there were changes to the existing result source
+files, those changes would have only been written above the C<# DO NOT
+MODIFY THIS OR ANYTHING ABOVE!> comment and your hand-edited
+enhancements would have been preserved.
-Create the following three files with the content shown below.
+Speaking of "hand-edit ted enhancements," we should now add
+relationship information to the three new result source files. Edit
+each of these files and add the following information between the C<#
+DO NOT MODIFY THIS OR ANYTHING ABOVE!> comment and the closing C<1;>:
-C<lib/MyAppDB/User.pm>:
+C<lib/MyApp/Schema/Result/Users.pm>:
- package MyAppDB::User;
-
- use base qw/DBIx::Class/;
-
- # Load required DBIC stuff
- __PACKAGE__->load_components(qw/PK::Auto Core/);
- # Set the table name
- __PACKAGE__->table('users');
- # Set columns in table
- __PACKAGE__->add_columns(qw/id username password email_address first_name last_name/);
- # Set the primary key for the table
- __PACKAGE__->set_primary_key('id');
-
#
# Set relationships:
#
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
- # 3) Column name in *foreign* table
- __PACKAGE__->has_many(map_user_role => 'MyAppDB::UserRole', 'user_id');
+ # 3) Column name in *foreign* table (aka, foreign key in peer table)
+ __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::Result::UserRoles', 'user_id');
-
- =head1 NAME
-
- MyAppDB::User - A model object representing a person with access to the system.
-
- =head1 DESCRIPTION
-
- This is an object that represents a row in the 'users' table of your application
- database. It uses DBIx::Class (aka, DBIC) to do ORM.
-
- For Catalyst, this is designed to be used through MyApp::Model::MyAppDB.
- Offline utilities may wish to use this class directly.
-
- =cut
-
- 1;
+ # many_to_many():
+ # args:
+ # 1) Name of relationship, DBIC will create accessor with this name
+ # 2) Name of has_many() relationship this many_to_many() is shortcut for
+ # 3) Name of belongs_to() relationship in model class of has_many() above
+ # You must already have the has_many() defined to use a many_to_many().
+ __PACKAGE__->many_to_many(roles => 'map_user_role', 'role');
-C<lib/MyAppDB/Role.pm>:
+C<lib/MyApp/Schema/Result/Roles.pm>:
- package MyAppDB::Role;
-
- use base qw/DBIx::Class/;
-
- # Load required DBIC stuff
- __PACKAGE__->load_components(qw/PK::Auto Core/);
- # Set the table name
- __PACKAGE__->table('roles');
- # Set columns in table
- __PACKAGE__->add_columns(qw/id role/);
- # Set the primary key for the table
- __PACKAGE__->set_primary_key('id');
-
#
# Set relationships:
#
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
- # 3) Column name in *foreign* table
- __PACKAGE__->has_many(map_user_role => 'MyAppDB::UserRole', 'role_id');
-
-
- =head1 NAME
-
- MyAppDB::Role - A model object representing a class of access permissions to
- the system.
-
- =head1 DESCRIPTION
-
- This is an object that represents a row in the 'roles' table of your
- application database. It uses DBIx::Class (aka, DBIC) to do ORM.
-
- For Catalyst, this is designed to be used through MyApp::Model::MyAppDB.
- "Offline" utilities may wish to use this class directly.
-
- =cut
-
- 1;
+ # 3) Column name in *foreign* table (aka, foreign key in peer table)
+ __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::Result::UserRoles', 'role_id');
-C<lib/MyAppDB/UserRole.pm>:
+C<lib/MyApp/Schema/Result/UserRoles.pm>:
- package MyAppDB::UserRole;
-
- use base qw/DBIx::Class/;
-
- # Load required DBIC stuff
- __PACKAGE__->load_components(qw/PK::Auto Core/);
- # Set the table name
- __PACKAGE__->table('user_roles');
- # Set columns in table
- __PACKAGE__->add_columns(qw/user_id role_id/);
- # Set the primary key for the table
- __PACKAGE__->set_primary_key(qw/user_id role_id/);
-
#
# Set relationships:
#
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
# 3) Column name in *this* table
- __PACKAGE__->belongs_to(user => 'MyAppDB::User', 'user_id');
+ __PACKAGE__->belongs_to(user => 'MyApp::Schema::Result::Users', 'user_id');
# belongs_to():
# args:
# 1) Name of relationship, DBIC will create accessor with this name
# 2) Name of the model class referenced by this relationship
# 3) Column name in *this* table
- __PACKAGE__->belongs_to(role => 'MyAppDB::Role', 'role_id');
-
-
- =head1 NAME
-
- MyAppDB::UserRole - A model object representing the JOIN between Users and Roles.
-
- =head1 DESCRIPTION
-
- This is an object that represents a row in the 'user_roles' table of your application
- database. It uses DBIx::Class (aka, DBIC) to do ORM.
-
- You probably won't need to use this class directly -- it will be automatically
- used by DBIC where joins are needed.
-
- For Catalyst, this is designed to be used through MyApp::Model::MyAppDB.
- Offline utilities may wish to use this class directly.
-
- =cut
-
- 1;
+ __PACKAGE__->belongs_to(role => 'MyApp::Schema::Result::Roles', 'role_id');
+
-The code for these three result source classes is obviously very familiar to the C<Book>, C<Author>, and C<BookAuthor> classes created in Part 2.
+The code for these three sets of updates is obviously very similar to
+the edits we made to the C<Books>, C<Authors>, and C<BookAuthors>
+classes created in Part 3.
+
+Note that we do not need to make any change to the
+C<lib/MyApp/Schema.pm> schema file. It simply tells DBIC to load all
+of the Result Class and ResultSet Class files it finds in below the
+C<lib/MyApp/Schema> directory, so it will automatically pick up our
+new table information.
=head2 Sanity-Check Reload of Development Server
-We aren't ready to try out the authentication just yet; we only want to do a quick check to be sure our model loads correctly. Press C<Ctrl-C> to kill the previous server instance (if it's still running) and restart it:
+We aren't ready to try out the authentication just yet; we only want
+to do a quick check to be sure our model loads correctly. Press
+C<Ctrl-C> to kill the previous server instance (if it's still running)
+and restart it:
$ script/myapp_server.pl
+-------------------------------------------------------------------+----------+
| MyApp::Controller::Books | instance |
| MyApp::Controller::Root | instance |
- | MyApp::Model::MyAppDB | instance |
- | MyApp::Model::MyAppDB::Author | class |
- | MyApp::Model::MyAppDB::Book | class |
- | MyApp::Model::MyAppDB::BookAuthor | class |
- | MyApp::Model::MyAppDB::Role | class |
- | MyApp::Model::MyAppDB::User | class |
- | MyApp::Model::MyAppDB::UserRole | class |
+ | MyApp::Model::DB | instance |
+ | MyApp::Model::DB::Author | class |
+ | MyApp::Model::DB::Books | class |
+ | MyApp::Model::DB::BookAuthors | class |
+ | MyApp::Model::DB::Roles | class |
+ | MyApp::Model::DB::Users | class |
+ | MyApp::Model::DB::UserRoles | class |
| MyApp::View::TT | instance |
'-------------------------------------------------------------------+----------'
...
-Again, notice that your "result source" classes have been "re-loaded" by Catalyst under C<MyApp::Model>.
+Again, notice that your "Result Class" classes have been "re-loaded"
+by Catalyst under C<MyApp::Model>.
=head2 Include Authentication and Session Plugins
-Edit C<lib/MyApp.pm> and update it as follows (everything below C<StackTrace> is new):
-
- use Catalyst qw/
- -Debug
- ConfigLoader
- Static::Simple
-
- StackTrace
-
- Authentication
-
- Session
- Session::Store::FastMmap
- Session::State::Cookie
- /;
-
-The C<Authentication> plugin supports
-Authentication while the C<Session> plugins are required to maintain
-state across multiple HTTP requests.
-
-Note that the only required Authentication class is the main
-one. This is a change that occured in version 0.09999_01
-of the C<Authentication> plugin. You B<do not need> to specify a
-particular Authentication::Store or Authentication::Credential plugin.
-Instead, indicate the Store and Credential you want to use in your application
+Edit C<lib/MyApp.pm> and update it as follows (everything below
+C<StackTrace> is new):
+
+ # Load plugins
+ use Catalyst qw/-Debug
+ ConfigLoader
+ Static::Simple
+
+ StackTrace
+
+ Authentication
+
+ Session
+ Session::Store::FastMmap
+ Session::State::Cookie
+ /;
+
+B<Note:> As discussed in MoreCatalystBasics, different versions of
+C<Catalyst::Devel> have used a variety of methods to load the plugins.
+You can put the plugins in the C<use Catalyst> statement if you prefer.
+
+The C<Authentication> plugin supports Authentication while the
+C<Session> plugins are required to maintain state across multiple HTTP
+requests.
+
+Note that the only required Authentication class is the main one. This
+is a change that occurred in version 0.09999_01 of the
+C<Authentication> plugin. You B<do not need> to specify a particular
+Authentication::Store or Authentication::Credential plugin. Instead,
+indicate the Store and Credential you want to use in your application
configuration (see below).
-Note that there are several
-options for L<Session::Store|Catalyst::Plugin::Session::Store>
+Note that there are several options for
+L<Session::Store|Catalyst::Plugin::Session::Store>
(L<Session::Store::FastMmap|Catalyst::Plugin::Session::Store::FastMmap>
is generally a good choice if you are on Unix; try
L<Session::Store::File|Catalyst::Plugin::Session::Store::File> if you
are on Win32) -- consult
L<Session::Store|Catalyst::Plugin::Session::Store> and its subclasses
-for additional information and options (for example to use a
-database-backed session store).
+for additional information and options (for example to use a database-
+backed session store).
=head2 Configure Authentication
-Although C<__PACKAGE__-E<gt>config(name =E<gt> 'value');> is still
-supported, newer Catalyst applications tend to place all configuration
-information in C<myapp.yml> and automatically load this information into
-C<MyApp-E<gt>config> using the
-L<ConfigLoader|Catalyst::Plugin::ConfigLoader> plugin. Here, we need
-to load several parameters that tell
+Although C<__PACKAGE__-E<gt>config(name =E<gt> 'value');> is still
+supported, newer Catalyst applications tend to place all configuration
+information in C<myapp.conf> and automatically load this information
+into C<MyApp-E<gt>config> using the
+L<ConfigLoader|Catalyst::Plugin::ConfigLoader> plugin.
+
+As discussed in Part 3 of the tutorial, Catalyst has recently
+switched from a default config file format of YAML to
+L<Config::General|Config::General> (an apache-like format). In case
+you are using a version of Catalyst earlier than v5.7014, delete the
+C<myapp.yml>, or convert it to .conf format using the TIP in
+L<Catalyst::Manual::MoreCatalystBasics/EDIT THE LIST OF CATALYST PLUGINS>
+then simply follow the directions below to create a new C<myapp.conf>
+file. Although we will use the C<Config::General> format here because
+YAML files can be difficult to cut and paste in certain environments,
+you are free to use any format supported by
+L<Catalyst::Plugin::ConfigLoader|Catalyst::Plugin::ConfigLoader> and
+L<Config::Any|Config::Any> -- Catalyst will transparently handle the
+different formats.
+
+Here, we need to load several parameters that tell
L<Catalyst::Plugin::Authentication|Catalyst::Plugin::Authentication>
-where to locate information in your database. To do this, edit the
-C<myapp.yml> YAML and update it to match:
-
- ---
- name: MyApp
- authentication:
- default_realm: dbic
- realms:
- dbic:
- credential:
- class: Password
- password_field: password
- password_type: self_check
- store:
- class: DBIx::Class
- # This is the model object created by Catalyst::Model::DBIC from your
- # schema (you created 'MyAppDB::User' but as the Catalyst startup
- # debug messages show, it was loaded as 'MyApp::Model::MyAppDB::User').
- # NOTE: Omit 'MyApp::Model' to avoid a component lookup issue in Catalyst 5.66
- user_class: MyApp::Users
- # This is the name of the field in your 'users' table that contains the user's name
- id_field: username
- role_relation: roles
- role_field: rolename
- ignore_fields_in_find: [ 'remote_name' ]
+where to locate information in your database. To do this, edit the
+C<myapp.conf> file and update it to match:
+
+ # rename this file to MyApp.yml and put a : in front of "name" if
+ # you want to use yaml like in old versions of Catalyst
+ name MyApp
+ <authentication>
+ default_realm dbic
+ <realms>
+ <dbic>
+ <credential>
+ # Note: this first definition would be the same as setting
+ # __PACKAGE__->config->{authentication}->{realms}->{dbic}
+ # ->{credential} = 'Password' in lib/MyApp.pm
+ #
+ # Specify that we are going to do password-based auth
+ class Password
+ # This is the name of the field in the users table with the
+ # password stored in it
+ password_field password
+ # We are using an unencrypted password for now
+ password_type clear
+ </credential>
+ <store>
+ # Use DBIC to retrieve username, password & role information
+ class DBIx::Class
+ # This is the model object created by Catalyst::Model::DBIC
+ # from your schema (you created 'MyApp::Schema::Result::User'
+ # but as the Catalyst startup debug messages show, it was
+ # loaded as 'MyApp::Model::DB::Users').
+ # NOTE: Omit 'MyApp::Model' here just as you would when using
+ # '$c->model("DB::Users)'
+ user_class DB::Users
+ </store>
+ </dbic>
+ </realms>
+ </authentication>
Inline comments in the code above explain how each field is being used.
-B<TIP>: Although YAML uses a very simple and easy-to-ready format, it
-does require the use of a consistent level of indenting. Be sure you
-line up everything on a given 'level' with the same number of indents.
-Also, be sure not to use C<tab> characters (YAML does not support them
-because they are handled inconsistently across editors).
-
=head2 Add Login and Logout Controllers
$ script/myapp_create.pl controller Login
$ script/myapp_create.pl controller Logout
-B<NOTE>: You could easily use a single controller here. For example,
-you could have a C<User> controller with both C<login> and C<logout>
-actions. Remember, Catalyst is designed to be very flexible, and leaves
-such matters up to you, the designer and programmer.
-
-Then open C<lib/MyApp/Controller/Login.pm>, locate the C<sub index :
-Private> method (this was automatically inserted by the helpers when we
-created the Login controller above), and delete this line:
+You could easily use a single controller here. For example, you could
+have a C<User> controller with both C<login> and C<logout> actions.
+Remember, Catalyst is designed to be very flexible, and leaves such
+matters up to you, the designer and programmer.
- $c->response->body('Matched MyApp::Controller::Login in Login.');
-
-Then update it to match:
+Then open C<lib/MyApp/Controller/Login.pm>, locate the
+C<sub index :Path :Args(0)> method (or C<sub index : Private> if you
+are using an older version of Catalyst) that was automatically
+inserted by the helpers when we created the Login controller above,
+and update the definition of C<sub index> to match:
=head2 index
=cut
- sub index : Private {
+ sub index :Path :Args(0) {
my ($self, $c) = @_;
# Get the username and password from form
# If the username and password values were found in form
if ($username && $password) {
# Attempt to log the user in
- if ($c->authenticate({ username => $username,
- password => $password} )) {
+ if ($c->authenticate({ username => $username,
+ password => $password } )) {
# If successful, then let them use the application
- $c->response->redirect($c->uri_for('/books/list'));
+ $c->response->redirect($c->uri_for(
+ $c->controller('Books')->action_for('list')));
return;
} else {
# Set an error message
}
This controller fetches the C<username> and C<password> values from the
-login form and attempts to authenticate the user. If successful, it
-redirects the user to the book list page. If the login fails, the user
-will stay at the login page but receive an error message. If the
-C<username> and C<password> values are not present in the form, the
+login form and attempts to authenticate the user. If successful, it
+redirects the user to the book list page. If the login fails, the user
+will stay at the login page and receive an error message. If the
+C<username> and C<password> values are not present in the form, the
user will be taken to the empty login form.
-Note that we could have used something like C<sub default :Private>;
-however, the use of C<default> actions is discouraged because it does
-not receive path args as with other actions. The recommended practice
-is to only use C<default> in C<MyApp::Controller::Root>.
-
-Another option would be to use something like
-C<sub base :Path :Args(0) {...}> (where the C<...> refers to the login
-code shown in C<sub index : Private> above). We are using C<sub base
-:Path :Args(0) {...}> here to specifically match the URL C</login>.
-C<Path> actions (aka, "literal actions") create URI matches relative to
-the namespace of the controller where they are defined. Although
-C<Path> supports arguments that allow relative and absolute paths to be
-defined, here we use an empty C<Path> definition to match on just the
-name of the controller itself. The method name, C<base>, is arbitrary.
-We make the match even more specific with the C<:Args(0)> action
-modifier -- this forces the match on I<only> C</login>, not
+Note that we could have used something like "C<sub default :Path>",
+however, it is generally recommended (partly for historical reasons,
+and partly for code clarity) only to use C<default> in
+C<MyApp::Controller::Root>, and then mainly to generate the 404 not
+found page for the application.
+
+Instead, we are using "C<sub somename :Path :Args(0) {...}>" here to
+specifically match the URL C</login>. C<Path> actions (aka, "literal
+actions") create URI matches relative to the namespace of the
+controller where they are defined. Although C<Path> supports
+arguments that allow relative and absolute paths to be defined, here
+we use an empty C<Path> definition to match on just the name of the
+controller itself. The method name, C<index>, is arbitrary. We make
+the match even more specific with the C<:Args(0)> action modifier --
+this forces the match on I<only> C</login>, not
C</login/somethingelse>.
-Next, update the corresponding method in C<lib/MyApp/Controller/Logout.pm>
-to match:
+Next, update the corresponding method in
+C<lib/MyApp/Controller/Logout.pm> to match:
=head2 index
=cut
- sub index : Private {
+ sub index :Path :Args(0) {
my ($self, $c) = @_;
# Clear the user's state
$c->response->redirect($c->uri_for('/'));
}
-As with the login controller, be sure to delete the
-C<$c->response->body('Matched MyApp::Controller::Logout in Logout.');>
+As with the login controller, be sure to delete the
+C<$c-E<gt>response-E<gt>body('Matched MyApp::Controller::Logout in Logout.');>
line of the C<sub index>.
[% META title = 'Login' %]
<!-- Login form -->
- <form method="post" action=" [% Catalyst.uri_for('/login') %] ">
+ <form method="post" action="[% c.uri_for('/login') %]">
<table>
<tr>
<td>Username:</td>
=cut
# Note that 'auto' runs after 'begin' but before your actions and that
- # 'auto' "chain" (all from application path to most specific class are run)
+ # 'auto's "chain" (all from application path to most specific class are run)
# See the 'Actions' section of 'Catalyst::Manual::Intro' for more info.
sub auto : Private {
my ($self, $c) = @_;
# Allow unauthenticated users to reach the login page. This
- # allows anauthenticated users to reach any action in the Login
+ # allows unauthenticated users to reach any action in the Login
# controller. To lock it down to a single action, we could use:
# if ($c->action eq $c->controller('Login')->action_for('index'))
- # to only allow unauthenticated access to the C<index> action we
+ # to only allow unauthenticated access to the 'index' action we
# added above.
if ($c->controller eq $c->controller('Login')) {
return 1;
return 1;
}
-B<Note:> Catalyst provides a number of different types of actions, such
-as C<Local>, C<Regex>, and C<Private>. You should refer to
-L<Catalyst::Manual::Intro> for a more detailed explanation, but the
-following bullet points provide a quick introduction:
-
-=over 4
-
-=item *
-
-The majority of application use C<Local> actions for items that respond
-to user requests and C<Private> actions for those that do not directly
-respond to user input.
-
-=item *
-
-There are five types of C<Private> actions: C<begin>, C<end>,
-C<default>, C<index>, and C<auto>.
-
-=item *
-
-With C<begin>, C<end>, C<default>, C<index> private actions, only the
-most specific action of each type will be called. For example, if you
-define a C<begin> action in your controller it will I<override> a
-C<begin> action in your application/root controller -- I<only> the
-action in your controller will be called.
-
-=item *
-
-Unlike the other actions where only a single method is called for each
-request, I<every> auto action along the chain of namespaces will be
-called. Each C<auto> action will be called I<from the application/root
-controller down through the most specific class>.
-
-=back
-
-By placing the authentication enforcement code inside the C<auto> method
-of C<lib/MyApp/Controller/Root.pm> (or C<lib/MyApp.pm>), it will be
-called for I<every> request that is received by the entire application.
+As discussed in
+L<Catalyst::Manual::Tutorial::MoreCatalystBasics/CREATE A CATALYST CONTROLLER>,
+every C<auto> method from the application/root controller down to the
+most specific controller will be called. By placing the
+authentication enforcement code inside the C<auto> method of
+C<lib/MyApp/Controller/Root.pm> (or C<lib/MyApp.pm>), it will be
+called for I<every> request that is received by the entire
+application.
=head2 Displaying Content Only to Authenticated Users
this, open C<root/src/login.tt2> in your editor and add the following
lines to the bottom of the file:
+ ...
<p>
[%
- # This code illustrates how certain parts of the TT
+ # This code illustrates how certain parts of the TT
# template will only be shown to users who have logged in
%]
- [% IF Catalyst.user_exists %]
- Please Note: You are already logged in as '[% Catalyst.user.username %]'.
- You can <a href="[% Catalyst.uri_for('/logout') %]">logout</a> here.
+ [% IF c.user_exists %]
+ Please Note: You are already logged in as '[% c.user.username %]'.
+ You can <a href="[% c.uri_for('/logout') %]">logout</a> here.
[% ELSE %]
You need to log in to use this application.
[% END %]
[%#
Note that this whole block is a comment because the "#" appears
- immediate after the "[%" (with no spaces in between). Although it
- can be a handy way to temporarily "comment out" a whole block of
- TT code, it's probably a little too subtle for use in "normal"
+ immediate after the "[%" (with no spaces in between). Although it
+ can be a handy way to temporarily "comment out" a whole block of
+ TT code, it's probably a little too subtle for use in "normal"
comments.
%]
- </p>
+ </p>
Although most of the code is comments, the middle few lines provide a
"you are already logged in" reminder if the user returns to the login
$ script/myapp_server.pl
-B<IMPORTANT NOTE>: If you happen to be using Internet Explorer, you may
-need to use the command C<script/myapp_server.pl -k> to enable the
-keepalive feature in the development server. Otherwise, the HTTP
-redirect on successful login may not work correctly with IE (it seems to
-work without -k if you are running the web browser and development
-server on the same machine). If you are using browser a browser other
-than IE, it should work either way. If you want to make keepalive the
-default, you can edit C<script/myapp_server.pl> and change the
-initialization value for C<$keepalive> to C<1>. (You will need to do
-this every time you create a new Catalyst application or rebuild the
-C<myapp_server.pl> script.)
-
-Now trying going to L<http://localhost:3000/books/list> and you should
-be redirected to the login page, hitting Shift+Reload if necessary (the
-"You are already logged in" message should I<not> appear -- if it does,
-click the C<logout> button and try again). Note the C<***Root::auto User
-not found...> debug message in the development server output. Enter
-username C<test01> and password C<mypass>, and you should be taken to
-the Book List page.
+B<IMPORTANT NOTE:> If you are having issues with authentication on
+Internet Explorer, be sure to check the system clocks on both your
+server and client machines. Internet Explorer is very picky about
+timestamps for cookies. You can quickly sync a Debian system by
+installing the "ntpdate" package:
+
+ sudo aptitude -y install ntpdate
+
+And then run the following command:
+
+ sudo ntpdate-debian
+
+Or, depending on your firewall configuration:
+
+ sudo ntpdate-debian -u
+
+Note: NTP can be a little more finicky about firewalls because it uses
+UDP vs. the more common TCP that you see with most Internet protocols.
+Worse case, you might have to manually set the time on your development
+box instead of using NTP.
+
+Now trying going to L<http://localhost:3000/books/list> and you should
+be redirected to the login page, hitting Shift+Reload or Ctrl+Reload
+if necessary (the "You are already logged in" message should I<not>
+appear -- if it does, click the C<logout> button and try again). Note
+the C<***Root::auto User not found...> debug message in the
+development server output. Enter username C<test01> and password
+C<mypass>, and you should be taken to the Book List page.
Open C<root/src/books/list.tt2> and add the following lines to the
-bottom:
+bottom (below the closing </table> tag):
<p>
- <a href="[% Catalyst.uri_for('/login') %]">Login</a>
- <a href="[% Catalyst.uri_for('form_create') %]">Create</a>
+ <a href="[% c.uri_for('/login') %]">Login</a>
+ <a href="[% c.uri_for(c.controller.action_for('form_create')) %]">Create</a>
</p>
-Reload your browser and you should now see a "Login" and "Create" links
-at the bottom of the page (as mentioned earlier, you can update template
-files without reloading the development server). Click the first link
-to return to the login page. This time you I<should> see the "You are
+Reload your browser and you should now see a "Login" and "Create" links
+at the bottom of the page (as mentioned earlier, you can update template
+files without reloading the development server). Click the first link
+to return to the login page. This time you I<should> see the "You are
already logged in" message.
Finally, click the C<You can logout here> link on the C</login> page.
B<Note:> This section is optional. You can skip it and the rest of the
tutorial will function normally.
-Note that even with the techniques shown in this section, the browser
+Be aware that even with the techniques shown in this section, the browser
still transmits the passwords in cleartext to your application. We are
just avoiding the I<storage> of cleartext passwords in the database by
using a SHA-1 hash. If you are concerned about cleartext passwords
between the browser and your application, consider using SSL/TLS, made
-easy with the Catalyst plugin Catalyst::Plugin:RequireSSL.
+easy with the Catalyst plugin Catalyst::Plugin:RequireSSL. You should
+also consider adding a "salt" mechanism to your hashed passwords to
+mitigate the risk of a "rainbow table" crack against your passwords (see
+L<Catalyst::Authentication::Credential::Password|Catalyst::Authentication::Credential::Password>
+for more information on using a salt value).
=head2 Get a SHA-1 Hash for the Password
$ perl -MDigest::SHA -e 'print Digest::SHA::sha1_hex("mypass"), "\n"'
e727d1464ae12436e899a726da5b2f11d8381b26
- $
B<Note:> You should probably modify this code for production use to
not read the password from the command line. By having the script
$ sqlite3 myapp.db < myapp03.sql
-B<Note:> We are using SHA-1 hashes here, but many other hashing
+B<Note:> We are using SHA-1 hashes here, but many other hashing
algorithms are supported. See C<Digest> for more information.
=head2 Enable SHA-1 Hash Passwords in
C<Catalyst::Plugin::Authentication::Store::DBIC>
-Edit C<myapp.yml> and update it to match (the C<password_type> and
+Edit C<myapp.conf> and update it to match (the C<password_type> and
C<password_hash_type> are new, everything else is the same):
- ---
- name: MyApp
- authentication:
- dbic:
- # Note this first definition would be the same as setting
- # __PACKAGE__->config->{authentication}->{dbic}->{user_class} = 'MyAppDB::User'
- # in lib/MyApp.pm (IOW, each hash key becomes a "name:" in the YAML file).
- #
- # This is the model object created by Catalyst::Model::DBIC from your
- # schema (you created 'MyAppDB::User' but as the Catalyst startup
- # debug messages show, it was loaded as 'MyApp::Model::MyAppDB::User').
- # NOTE: Omit 'MyApp::Model' here just as you would when using
- # '$c->model("MyAppDB::User)'
- user_class: MyAppDB::User
- # This is the name of the field in your 'users' table that contains the user's name
- user_field: username
- # This is the name of the field in your 'users' table that contains the password
- password_field: password
- # Other options can go here for hashed passwords
- # Enabled hashed passwords
- password_type: hashed
- # Use the SHA-1 hashing algorithm
- password_hash_type: SHA-1
+ # rename this file to MyApp.yml and put a : in front of "name" if
+ # you want to use yaml like in old versions of Catalyst
+ name MyApp
+ <authentication>
+ default_realm dbic
+ <realms>
+ <dbic>
+ <credential>
+ # Note this first definition would be the same as setting
+ # __PACKAGE__->config->{authentication}->{realms}->{dbic}
+ # ->{credential} = 'Password' in lib/MyApp.pm
+ #
+ # Specify that we are going to do password-based auth
+ class Password
+ # This is the name of the field in the users table with the
+ # password stored in it
+ password_field password
+ # Switch to more secure hashed passwords
+ password_type hashed
+ # Use the SHA-1 hashing algorithm
+ password_hash_type SHA-1
+ </credential>
+ <store>
+ # Use DBIC to retrieve username, password & role information
+ class DBIx::Class
+ # This is the model object created by Catalyst::Model::DBIC
+ # from your schema (you created 'MyApp::Schema::Result::User'
+ # but as the Catalyst startup debug messages show, it was
+ # loaded as 'MyApp::Model::DB::Users').
+ # NOTE: Omit 'MyApp::Model' here just as you would when using
+ # '$c->model("DB::Users)'
+ user_class DB::Users
+ </store>
+ </dbic>
+ </realms>
+ </authentication>
=head2 Try Out the Hashed Passwords
$ script/myapp_server.pl
You should now be able to go to L<http://localhost:3000/books/list> and
-login as before. When done, click the "Logout" link on the login page
+login as before. When done, click the "logout" link on the login page
(or point your browser at L<http://localhost:3000/logout>).
-B<Note:> If you receive the debug screen in your browser with a
-C<Can't call method "stash" on an undefined value...> error message,
-make sure that you are using v0.07 of
-L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL>.
-The following command can be a useful way to quickly dump the version number
-of this module on your system:
-
- perl -MCatalyst::Plugin::Authorization::ACL -e 'print $Catalyst::Plugin::Authorization::ACL::VERSION, "\n";'
-
=head1 USING THE SESSION FOR FLASH
As discussed in Part 3 of the tutorial, C<flash> allows you to set
-variables in a way that is very similar to C<stash>, but it will
+variables in a way that is very similar to C<stash>, but it will
remain set across multiple requests. Once the value is read, it
is cleared (unless reset). Although C<flash> has nothing to do with
authentication, it does leverage the same session plugins. Now that
-those plugins are enabled, let's go back and improve the "delete
+those plugins are enabled, let's go back and update the "delete
and redirect with query parameters" code seen at the end of the
-L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD> part of the
-tutorial.
+L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD> part of the
+tutorial to take advantage of C<flash>.
First, open C<lib/MyApp/Controller/Books.pm> and modify C<sub delete>
-to match the following:
+to match the following (everything after the model search line of code
+has changed):
- =head2 delete
+ =head2 delete
Delete a book
-
+
=cut
- sub delete : Local {
- # $id = primary key of book to delete
- my ($self, $c, $id) = @_;
+ sub delete :Chained('object') :PathPart('delete') :Args(0) {
+ my ($self, $c) = @_;
- # Search for the book and then delete it
- $c->model('MyAppDB::Book')->search({id => $id})->delete_all;
+ # Use the book object saved by 'object' and delete it along
+ # with related 'book_authors' entries
+ $c->stash->{object}->delete;
# Use 'flash' to save information across requests until it's read
$c->flash->{status_msg} = "Book deleted";
-
- # Redirect the user back to the list page with status msg as an arg
- $c->response->redirect($c->uri_for('/books/list'));
+
+ # Redirect the user back to the list page
+ $c->response->redirect($c->uri_for($self->action_for('list')));
}
-Next, open C<root/lib/site/layout> and update the TT code to pull from
+Next, open C<root/src/wrapper.tt2> and update the TT code to pull from
flash vs. the C<status_msg> query parameter:
- <div id="header">[% PROCESS site/header %]</div>
-
+ ...
<div id="content">
- <span class="message">[% status_msg || Catalyst.flash.status_msg %]</span>
- <span class="error">[% error_msg %]</span>
- [% content %]
- </div>
-
- <div id="footer">[% PROCESS site/footer %]</div>
+ [%# Status and error messages %]
+ <span class="message">[% status_msg || c.flash.status_msg %]</span>
+ <span class="error">[% error_msg %]</span>
+ [%# This is where TT will stick all of your template's contents. -%]
+ [% content %]
+ </div><!-- end content -->
+ ...
+
+Although the sample above only shows the C<content> div, leave the
+rest of the file intact -- the only change we made to the C<wrapper.tt2>
+was to add "C<|| c.request.params.status_msg>" to the
+C<E<lt>span class="message"E<gt>> line.
=head2 Try Out Flash
-Restart the development server and point your browser to
-L<http://localhost:3000/books/url_create/Test/1/4> to create an extra
-book. Click the "Return to list" link and delete the "Test" book you
-just added. The C<flash> mechanism should retain our "Book deleted"
-status message across the redirect.
+Restart the development server, log in, and then point your browser to
+L<http://localhost:3000/books/url_create/Test/1/4> to create an extra
+several books. Click the "Return to list" link and delete one of the
+"Test" books you just added. The C<flash> mechanism should retain our
+"Book deleted" status message across the redirect.
B<NOTE:> While C<flash> will save information across multiple requests,
I<it does get cleared the first time it is read>. In general, this is
information.
+=head2 Switch To Flash-To-Stash
+
+Although the a use of flash above works well, the
+C<status_msg || c.flash.status_msg> statement is a little ugly. A nice
+alternative is to use the C<flash_to_stash> feature that automatically
+copies the content of flash to stash. This makes your controller
+and template code work regardless of where it was directly access, a
+forward, or a redirect. To enable C<flash_to_stash>, you can either
+set the value in C<lib/MyApp.pm> by changing the default
+C<__PACKAGE__-E<gt>config> setting to something like:
+
+ __PACKAGE__->config(
+ name => 'MyApp',
+ session => {flash_to_stash => 1}
+ );
+
+B<or> add the following to C<myapp.conf>:
+
+ <session>
+ flash_to_stash 1
+ </session>
+
+The C<__PACKAGE__-E<gt>config> option is probably preferable here
+since it's not something you will want to change at runtime without it
+possibly breaking some of your code.
+
+Then edit C<root/src/wrapper.tt2> and change the C<status_msg> line
+to match the following:
+
+ <span class="message">[% status_msg %]</span>
+
+Restart the development server and go to
+L<http://localhost:3000/books/list> in your browser. Delete another
+of the "Test" books you added in the previous step. Flash should still
+maintain the status message across the redirect even though you are no
+longer explicitly accessing C<c.flash>.
+
+
=head1 AUTHOR
Kennedy Clark, C<hkclark@gmail.com>
Please report any errors, issues or suggestions to the author. The
most recent version of the Catalyst Tutorial can be found at
-L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>.
+L<http://dev.catalyst.perl.org/repos/Catalyst/Catalyst-Manual/5.70/trunk/lib/Catalyst/Manual/Tutorial/>.
-Copyright 2006, Kennedy Clark, under Creative Commons License
-(L<http://creativecommons.org/licenses/by-nc-sa/2.5/>).
+Copyright 2006-2008, Kennedy Clark, under Creative Commons License
+(L<http://creativecommons.org/licenses/by-sa/3.0/us/>).