Switch to use of DBIx::Class::EncodedColumn and C::Auth::Realm::SimpleDB
Kennedy Clark [Sun, 3 May 2009 02:40:36 +0000 (02:40 +0000)]
lib/Catalyst/Manual/Tutorial/Authentication.pod
lib/Catalyst/Manual/Tutorial/Intro.pod

index 8267025..c8b21a8 100644 (file)
@@ -154,14 +154,14 @@ C<lib/MyApp/Schema/Result/Users.pm>:
     #
     # Set relationships:
     #
-
+    
     # has_many():
     #   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 (aka, foreign key in peer table)
     __PACKAGE__->has_many(map_user_role => 'MyApp::Schema::Result::UserRoles', 'user_id');
-
+    
     # many_to_many():
     #   args:
     #     1) Name of relationship, DBIC will create accessor with this name
@@ -176,7 +176,7 @@ C<lib/MyApp/Schema/Result/Roles.pm>:
     #
     # Set relationships:
     #
-
+    
     # has_many():
     #   args:
     #     1) Name of relationship, DBIC will create accessor with this name
@@ -190,14 +190,14 @@ C<lib/MyApp/Schema/Result/UserRoles.pm>:
     #
     # Set relationships:
     #
-
+    
     # 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(user => 'MyApp::Schema::Result::Users', 'user_id');
-
+    
     # belongs_to():
     #   args:
     #     1) Name of relationship, DBIC will create accessor with this name
@@ -258,11 +258,11 @@ C<StackTrace> is new):
     use Catalyst qw/-Debug
                 ConfigLoader
                 Static::Simple
-
+    
                 StackTrace
-
+    
                 Authentication
-
+    
                 Session
                 Session::Store::FastMmap
                 Session::State::Cookie
@@ -296,67 +296,27 @@ 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.conf> and automatically load this information
-into C<MyApp-E<gt>config> using the
-L<ConfigLoader|Catalyst::Plugin::ConfigLoader> plugin.
-
-As discussed in Chapter 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.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.
+There are a variety of way to provide configuration information to
+L<Catalyst::Plugin::Authentication|Catalyst::Plugin::Authentication>.
+Here we will use 
+L<Catalyst::Authentication::Realm::SimpleDB|Catalyst::Authentication::Realm::SimpleDB>
+because it automatically sets a reasonable set of defaults for us. Open 
+C<lib/MyApp.pm> and place the following text above the call to
+C<__PACKAGE__-E<gt>setup();>:
+
+    # Configure SimpleDB Authentication
+    __PACKAGE__->config->{'Plugin::Authentication'} = {
+            default => {
+                class           => 'SimpleDB',
+                user_model      => 'DB::Users',
+                password_type   => 'clear',
+            },
+        };
+
+We could have placed this configuration in C<myapp.conf>, but placing 
+it in C<lib/MyApp.pm> is probably a better place since it's not likely 
+something that users of your application will want to change during 
+deployment.
 
 
 =head2 Add Login and Logout Controllers
@@ -378,18 +338,18 @@ inserted by the helpers when we created the Login controller above,
 and update the definition of C<sub index> to match:
 
     =head2 index
-
+    
     Login logic
-
+    
     =cut
-
+    
     sub index :Path :Args(0) {
         my ($self, $c) = @_;
-
+    
         # Get the username and password from form
         my $username = $c->request->params->{username} || "";
         my $password = $c->request->params->{password} || "";
-
+    
         # If the username and password values were found in form
         if ($username && $password) {
             # Attempt to log the user in
@@ -404,7 +364,7 @@ and update the definition of C<sub index> to match:
                 $c->stash->{error_msg} = "Bad username or password.";
             }
         }
-
+    
         # If either of above don't work out, send to the login page
         $c->stash->{template} = 'login.tt2';
     }
@@ -437,17 +397,17 @@ Next, update the corresponding method in
 C<lib/MyApp/Controller/Logout.pm> to match:
 
     =head2 index
-
+    
     Logout logic
-
+    
     =cut
-
+    
     sub index :Path :Args(0) {
         my ($self, $c) = @_;
-
+    
         # Clear the user's state
         $c->logout;
-
+    
         # Send the user to the starting point
         $c->response->redirect($c->uri_for('/'));
     }
@@ -462,7 +422,7 @@ line of the C<sub index>.
 Create a login form by opening C<root/src/login.tt2> and inserting:
 
     [% META title = 'Login' %]
-
+    
     <!-- Login form -->
     <form method="post" action="[% c.uri_for('/login') %]">
       <table>
@@ -494,17 +454,17 @@ Edit the existing C<lib/MyApp/Controller/Root.pm> class file and insert
 the following method:
 
     =head2 auto
-
+    
     Check if there is a user and, if not, forward to login page
-
+    
     =cut
-
+    
     # Note that 'auto' runs after 'begin' but before your actions and that
     # '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 unauthenticated users to reach any action in the Login
         # controller.  To lock it down to a single action, we could use:
@@ -514,7 +474,7 @@ the following method:
         if ($c->controller eq $c->controller('Login')) {
             return 1;
         }
-
+    
         # If a user doesn't exist, force login
         if (!$c->user_exists) {
             # Dump a log message to the development server debug output
@@ -524,7 +484,7 @@ the following method:
             # Return 0 to cancel 'post-auto' processing and prevent use of application
             return 0;
         }
-
+    
         # User found, so return 1 to continue with processing after this 'auto'
         return 1;
     }
@@ -631,8 +591,10 @@ need to log in to use this application."
 
 =head1 USING PASSWORD HASHES
 
-In this section we increase the security of our system by converting
-from cleartext passwords to SHA-1 password hashes.
+In this section we increase the security of our system by converting 
+from cleartext passwords to SHA-1 password hashes that include a 
+random "salt" value to make them extremely difficult to crack with
+dictionary and "rainbow table" attacks.
 
 B<Note:> This section is optional.  You can skip it and the rest of the
 tutorial will function normally.
@@ -640,96 +602,142 @@ tutorial will function normally.
 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
+using a salted 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.  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).
+easy with the Catalyst plugin Catalyst::Plugin:RequireSSL.
 
 
-=head2 Get a SHA-1 Hash for the Password
+=head2 Install DBIx::Class::EncodedColumn
 
-Catalyst uses the C<Digest> module to support a variety of hashing
-algorithms.  Here we will use SHA-1 (SHA = Secure Hash Algorithm).
-First, we should compute the SHA-1 hash for the "mypass" password we are
-using.  The following command-line Perl script provides a "quick and
-dirty" way to do this:
+L<DBIx::Class::EncodedColumn|DBIx::Class::EncodedColumn> provides features
+that can greatly simplify the maintenance of passwords.  It's currently 
+not available as a .deb package in the normal Debian repositories, so let's
+install it directly from CPAN:
 
-    $ perl -MDigest::SHA -e 'print Digest::SHA::sha1_hex("mypass"), "\n"'
-    e727d1464ae12436e899a726da5b2f11d8381b26
+    $ sudo cpan DBIx::Class::EncodedColumn
 
-B<Note:> You should probably modify this code for production use to
-not read the password from the command line.  By having the script
-prompt for the cleartext password, it avoids having the password linger
-in forms such as your C<.bash_history> files (assuming you are using
-BASH as your shell).  An example of such a script can be found in
-Appendix 3.
 
+=head2 Re-Run the DBIC::Schema Model Helper to Include DBIx::Class::EncodedColumn
 
-=head2 Switch to SHA-1 Password Hashes in the Database
+Next, we can re-run the model helper to have it include 
+L<DBIx::Class::EncodedColumn|DBIx::Class::EncodedColumn> in all of the 
+Result Classes it generates for us.  Simply use the same command we 
+saw in Chapters 3 and 4, but add C<,EncodedColumn> to the C<components>
+argument:
 
-Next, we need to change the C<password> column of our C<users> table to
-store this hash value vs. the existing cleartext password.  Open
-C<myapp03.sql> in your editor and enter:
+    $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
+        create=static components=TimeStamp,EncodedColumn dbi:SQLite:myapp.db
 
-    --
-    -- Convert passwords to SHA-1 hashes
-    --
-    UPDATE users SET password = 'e727d1464ae12436e899a726da5b2f11d8381b26' WHERE id = 1;
-    UPDATE users SET password = 'e727d1464ae12436e899a726da5b2f11d8381b26' WHERE id = 2;
-    UPDATE users SET password = 'e727d1464ae12436e899a726da5b2f11d8381b26' WHERE id = 3;
-
-Then use the following command to update the SQLite database:
-
-    $ sqlite3 myapp.db < myapp03.sql
-
-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 Catalyst::Plugin::Authentication::Store::DBIC
-
-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):
-
-    # 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>
+If you then open one of the Result Classes, you will see that it 
+includes EncodedColumn in the C<load_components> line.  Take a look at 
+C<lib/MyApp/Schema/Result/Users.pm> since that's the main class where we
+want to use hashed and salted passwords:
+
+    __PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp", "EncodedColumn", "Core");
+
+
+=head2 Modify the "password" Column to Use EncodedColumn
+
+Open the file C<lib/MyApp/Schema/Result/Users.pm> and enter the following
+text below the "# DO NOT MODIFY THIS OR ANYTHING ABOVE!" line but above
+the closing "1;":
+
+    # Have the 'password' column use a SHA-1 hash and 10-character salt
+    # with hex encoding; Generate the 'check_password" method
+    __PACKAGE__->add_columns(
+        'password' => {
+            data_type           => "TEXT",
+            size                => undef,
+            encode_column       => 1,
+            encode_class        => 'Digest',
+            encode_args         => {salt_length => 10},
+            encode_check_method => 'check_password',
+        },
+    );
+
+This redefines the automatically generated definition for the password 
+fields at the top of the Result Class file to now use EncodedColumn 
+logic (C<encoded_column> is set to 1).  C<encode_class> can be set to 
+either C<Digest> to use 
+L<DBIx::Class::EncodedColumn::Digest|DBIx::Class::EncodedColumn::Digest>, 
+or C<Crypt::Eksblowfish::Bcrypt> for 
+L<DBIx::Class::EncodedColumn::Crypt::Eksblowfish::Bcrypt|DBIx::Class::EncodedColumn::Crypt::Eksblowfish::Bcrypt>.
+C<encode_args> is then used to customize the type of Digest you 
+selected. Here we only specified the size of the salt to use, but
+we could have also modified the hashing algorithm ('SHA-256' is 
+the default) and the format to use ('base64' is the default, but
+'hex' and 'binary' are other options).  To use these, you could 
+change the C<encode_args> to something like:
+
+            encode_args         => {algorithm => 'SHA-1', 
+                                    format => 'hex', 
+                                    salt_length => 10},
+
+
+=head2 Load Hashed Passwords in the Database
+
+Next, let's create a quick script to load some hashed and salted passwords
+into the C<password> column of our C<users> table.  Open the file
+C<set_hashed_passwords.pl> in your editor and enter the following text:
+
+    #!/usr/bin/perl
+    
+    use strict;
+    use warnings;
+    
+    use MyApp::Schema;
+    
+    my $schema = MyApp::Schema->connect('dbi:SQLite:myapp.db');
+    
+    my @users = $schema->resultset('Users')->all;
+    
+    foreach my $user (@users) {
+        $user->password('mypass');
+        $user->update;
+    }
+
+EncodedColumn lets us simple call C<$user->check_password($password)> 
+to see if the user has supplied the correct password, or, as we show 
+above, call C<$user->update($new_password)> to update the hashed 
+password stored for this user.
+
+Then run the following command:
+
+    $ perl -Ilib set_hashed_passwords.pl
+
+We had to use the C<-Ilib> arguement to tell perl to look under the 
+C<lib> directory for our C<MyApp::Schema> model.
+
+Then dump the users table to verify that it worked:
+
+    $ sqlite3 myapp.db "select * from users"
+    1|test01|38d3974fa9e9263099f7bc2574284b2f55473a9bM=fwpX2NR8|t01@na.com|Joe|Blow|1
+    2|test02|6ed8586587e53e0d7509b1cfed5df08feadc68cbMJlnPyPt0I|t02@na.com|Jane|Doe|1
+    3|test03|af929a151340c6aed4d54d7e2651795d1ad2e2f7UW8dHoGv9z|t03@na.com|No|Go|0
+
+As you can see, the passwords are much harder to steal from the 
+database.  Also note that this demonstrates how to use a DBIx::Class 
+model outside of your web application -- a very useful feature in many 
+situations.
+
+
+=head2 Enable Hashed and Salted Passwords
+
+Edit C<lib/MyApp.pm> and update it to match the following text (the only change
+is to the C<password_type> field):
+
+    # Configure SimpleDB Authentication
+    __PACKAGE__->config->{'Plugin::Authentication'} = {
+            default => {
+                class           => 'SimpleDB',
+                user_model      => 'DB::Users',
+                password_type   => 'self_check',
+            },
+        };
+
+The use of C<self_check> will cause 
+Catalyst::Plugin::Authentication::Store::DBIC to call the 
+C<check_password> method we enabled on our C<password> columns.
 
 
 =head2 Try Out the Hashed Passwords
@@ -761,21 +769,21 @@ to match the following (everything after the model search line of code
 has changed):
 
     =head2 delete
-
+    
     Delete a book
-
+    
     =cut
-
+    
     sub delete :Chained('object') :PathPart('delete') :Args(0) {
         my ($self, $c) = @_;
-
+    
         # 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
         $c->response->redirect($c->uri_for($self->action_for('list')));
     }
@@ -828,7 +836,7 @@ 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',
+            name    => 'MyApp',
             session => {flash_to_stash => 1}
         );
 
index 0c87127..b51f252 100644 (file)
@@ -598,7 +598,7 @@ B<NOTE:> You can run the test cases for the final code through Chapter 8
 with the following commands:
 
     sudo cpan Catalyst::Model::DBIC::Schema Time::Warp DBICx::TestDatabase \
-        DBIx::Class::DynamicDefault DBIx::Class::TimeStamp
+        DBIx::Class::DynamicDefault DBIx::Class::TimeStamp DBIx::Class::EncodedColumn
     wget http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/Tutorial/MyApp_Chapter8.tgz
     tar zxvf MyApp_Chapter8.tgz
     cd MyApp