1Catalyst::Manual::TutorUisaelr::C0o5n_tCArauittbahuletynestdti:cP:aeMtrailnounDa(ol3c:)u:mTeunttoartiiaoln::05_Authentication(3)
2
3
4
6 Catalyst::Manual::Tutorial::05_Authentication - Catalyst Tutorial -
7 Chapter 5: Authentication
8
10 This is Chapter 5 of 10 for the Catalyst tutorial.
11
12 Tutorial Overview
13
14 1. Introduction
15
16 2. Catalyst Basics
17
18 3. More Catalyst Basics
19
20 4. Basic CRUD
21
22 5. 05_Authentication
23
24 6. Authorization
25
26 7. Debugging
27
28 8. Testing
29
30 9. Advanced CRUD
31
32 10. Appendices
33
35 Now that we finally have a simple yet functional application, we can
36 focus on providing authentication (with authorization coming next in
37 Chapter 6).
38
39 This chapter of the tutorial is divided into two main sections: 1)
40 basic, cleartext authentication and 2) hash-based authentication.
41
42 Source code for the tutorial in included in the /home/catalyst/Final
43 directory of the Tutorial Virtual machine (one subdirectory per
44 chapter). There are also instructions for downloading the code in
45 Catalyst::Manual::Tutorial::01_Intro.
46
48 This section explores how to add authentication logic to a Catalyst
49 application.
50
51 Add Users and Roles to the Database
52 First, we add both user and role information to the database (we will
53 add the role information here although it will not be used until the
54 authorization section, Chapter 6). Create a new SQL script file by
55 opening myapp02.sql in your editor and insert:
56
57 --
58 -- Add users and role tables, along with a many-to-many join table
59 --
60 PRAGMA foreign_keys = ON;
61 CREATE TABLE users (
62 id INTEGER PRIMARY KEY,
63 username TEXT,
64 password TEXT,
65 email_address TEXT,
66 first_name TEXT,
67 last_name TEXT,
68 active INTEGER
69 );
70 CREATE TABLE role (
71 id INTEGER PRIMARY KEY,
72 role TEXT
73 );
74 CREATE TABLE user_role (
75 user_id INTEGER REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE,
76 role_id INTEGER REFERENCES role(id) ON DELETE CASCADE ON UPDATE CASCADE,
77 PRIMARY KEY (user_id, role_id)
78 );
79 --
80 -- Load up some initial test data
81 --
82 INSERT INTO users VALUES (1, 'test01', 'mypass', 't01@na.com', 'Joe', 'Blow', 1);
83 INSERT INTO users VALUES (2, 'test02', 'mypass', 't02@na.com', 'Jane', 'Doe', 1);
84 INSERT INTO users VALUES (3, 'test03', 'mypass', 't03@na.com', 'No', 'Go', 0);
85 INSERT INTO role VALUES (1, 'user');
86 INSERT INTO role VALUES (2, 'admin');
87 INSERT INTO user_role VALUES (1, 1);
88 INSERT INTO user_role VALUES (1, 2);
89 INSERT INTO user_role VALUES (2, 1);
90 INSERT INTO user_role VALUES (3, 1);
91
92 Then load this into the myapp.db database with the following command:
93
94 $ sqlite3 myapp.db < myapp02.sql
95
96 Add User and Role Information to DBIC Schema
97 Although we could manually edit the DBIC schema information to include
98 the new tables added in the previous step, let's use the
99 "create=static" option on the DBIC model helper to do most of the work
100 for us:
101
102 $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
103 create=static components=TimeStamp dbi:SQLite:myapp.db \
104 on_connect_do="PRAGMA foreign_keys = ON"
105 exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model"
106 exists "/home/catalyst/dev/MyApp/script/../t"
107 Dumping manual schema for MyApp::Schema to directory /home/catalyst/dev/MyApp/script/../lib ...
108 Schema dump completed.
109 exists "/home/catalyst/dev/MyApp/script/../lib/MyApp/Model/DB.pm"
110 $
111 $ ls lib/MyApp/Schema/Result
112 Author.pm BookAuthor.pm Book.pm Role.pm User.pm UserRole.pm
113
114 Notice how the helper has added three new table-specific Result Source
115 files to the lib/MyApp/Schema/Result directory. And, more importantly,
116 even if there were changes to the existing result source files, those
117 changes would have only been written above the "# DO NOT MODIFY THIS OR
118 ANYTHING ABOVE!" comment and your hand-edited enhancements would have
119 been preserved.
120
121 Speaking of "hand-edited enhancements," we should now add the
122 "many_to_many" relationship information to the User Result Source file.
123 As with the Book, BookAuthor, and Author files in Chapter 3,
124 DBIx::Class::Schema::Loader has automatically created the "has_many"
125 and "belongs_to" relationships for the new User, UserRole, and Role
126 tables. However, as a convenience for mapping Users to their assigned
127 roles (see Chapter 6), we will also manually add a "many_to_many"
128 relationship. Edit lib/MyApp/Schema/Result/User.pm add the following
129 information between the "# DO NOT MODIFY THIS OR ANYTHING ABOVE!"
130 comment and the closing "1;":
131
132 # many_to_many():
133 # args:
134 # 1) Name of relationship, DBIC will create accessor with this name
135 # 2) Name of has_many() relationship this many_to_many() is shortcut for
136 # 3) Name of belongs_to() relationship in model class of has_many() above
137 # You must already have the has_many() defined to use a many_to_many().
138 __PACKAGE__->many_to_many(roles => 'user_roles', 'role');
139
140 The code for this update is obviously very similar to the edits we made
141 to the "Book" and "Author" classes created in Chapter 3 with one
142 exception: we only defined the "many_to_many" relationship in one
143 direction. Whereas we felt that we would want to map Authors to Books
144 AND Books to Authors, here we are only adding the convenience
145 "many_to_many" in the Users to Roles direction.
146
147 Note that we do not need to make any change to the lib/MyApp/Schema.pm
148 schema file. It simply tells DBIC to load all of the Result Class and
149 ResultSet Class files it finds below the lib/MyApp/Schema directory, so
150 it will automatically pick up our new table information.
151
152 Sanity-Check of the Development Server Reload
153 We aren't ready to try out the authentication just yet; we only want to
154 do a quick check to be sure our model loads correctly. Assuming that
155 you are following along and using the "-r" option on myapp_server.pl,
156 then the development server should automatically reload (if not, press
157 "Ctrl-C" to break out of the server if it's running and then enter
158 script/myapp_server.pl to start it). Look for the three new model
159 objects in the startup debug output:
160
161 ...
162 .-------------------------------------------------------------------+----------.
163 | Class | Type |
164 +-------------------------------------------------------------------+----------+
165 | MyApp::Controller::Books | instance |
166 | MyApp::Controller::Root | instance |
167 | MyApp::Model::DB | instance |
168 | MyApp::Model::DB::Author | class |
169 | MyApp::Model::DB::Book | class |
170 | MyApp::Model::DB::BookAuthor | class |
171 | MyApp::Model::DB::Role | class |
172 | MyApp::Model::DB::User | class |
173 | MyApp::Model::DB::UserRole | class |
174 | MyApp::View::HTML | instance |
175 '-------------------------------------------------------------------+----------'
176 ...
177
178 Again, notice that your "Result Class" classes have been "re-loaded" by
179 Catalyst under "MyApp::Model".
180
181 Include Authentication and Session Plugins
182 Edit lib/MyApp.pm and update it as follows (everything below
183 "StackTrace" is new):
184
185 # Load plugins
186 use Catalyst qw/
187 -Debug
188 ConfigLoader
189 Static::Simple
190
191 StackTrace
192
193 Authentication
194
195 Session
196 Session::Store::File
197 Session::State::Cookie
198 /;
199
200 Note: As discussed in Chapter 3, different versions of
201 "Catalyst::Devel" have used a variety of methods to load the plugins,
202 but we are going to use the current Catalyst 5.9 practice of putting
203 them on the "use Catalyst" line.
204
205 The "Authentication" plugin supports Authentication while the "Session"
206 plugins are required to maintain state across multiple HTTP requests.
207
208 Note that the only required Authentication class is the main one. This
209 is a change that occurred in version 0.09999_01 of the Authentication
210 plugin. You do not need to specify a particular Authentication::Store
211 or "Authentication::Credential" you want to use. Instead, indicate the
212 Store and Credential you want to use in your application configuration
213 (see below).
214
215 Make sure you include the additional plugins as new dependencies in the
216 Makefile.PL file something like this:
217
218 requires 'Catalyst::Plugin::Authentication';
219 requires 'Catalyst::Plugin::Session';
220 requires 'Catalyst::Plugin::Session::Store::File';
221 requires 'Catalyst::Plugin::Session::State::Cookie';
222
223 Note that there are several options for Session::Store.
224 Session::Store::Memcached is generally a good choice if you are on
225 Unix. If you are running on Windows Session::Store::File is fine.
226 Consult Session::Store and its subclasses for additional information
227 and options (for example to use a database-backed session store).
228
229 Configure Authentication
230 There are a variety of ways to provide configuration information to
231 Catalyst::Plugin::Authentication. Here we will use
232 Catalyst::Authentication::Realm::SimpleDB because it automatically sets
233 a reasonable set of defaults for us. (Note: the "SimpleDB" here has
234 nothing to do with the SimpleDB offered in Amazon's web services
235 offerings -- here we are only talking about a "simple" way to use your
236 DB as an authentication backend.) Open lib/MyApp.pm and place the
237 following text above the call to "__PACKAGE__->setup();":
238
239 # Configure SimpleDB Authentication
240 __PACKAGE__->config(
241 'Plugin::Authentication' => {
242 default => {
243 class => 'SimpleDB',
244 user_model => 'DB::User',
245 password_type => 'clear',
246 },
247 },
248 );
249
250 We could have placed this configuration in myapp.conf, but placing it
251 in lib/MyApp.pm is probably a better place since it's not likely
252 something that users of your application will want to change during
253 deployment (or you could use a mixture: leave "class" and "user_model"
254 defined in lib/MyApp.pm as we show above, but place "password_type" in
255 myapp.conf to allow the type of password to be easily modified during
256 deployment). We will stick with putting all of the authentication-
257 related configuration in lib/MyApp.pm for the tutorial, but if you wish
258 to use myapp.conf, just convert to the following code:
259
260 <Plugin::Authentication>
261 <default>
262 password_type clear
263 user_model DB::User
264 class SimpleDB
265 </default>
266 </Plugin::Authentication>
267
268 TIP: Here is a short script that will dump the contents of
269 "MyApp->config" to Config::General format in myapp.conf:
270
271 $ CATALYST_DEBUG=0 perl -Ilib -e 'use MyApp; use Config::General;
272 Config::General->new->save_file("myapp.conf", MyApp->config);'
273
274 HOWEVER, if you try out the command above, be sure to delete the
275 "myapp.conf" command. Otherwise, you will wind up with duplicate
276 configurations.
277
278 NOTE: Because we are using SimpleDB along with a database layout that
279 complies with its default assumptions: we don't need to specify the
280 names of the columns where our username and password information is
281 stored (hence, the "Simple" part of "SimpleDB"). That being said,
282 SimpleDB lets you specify that type of information if you need to.
283 Take a look at "Catalyst::Authentication::Realm::SimpleDB" for details.
284
285 Add Login and Logout Controllers
286 Use the Catalyst create script to create two stub controller files:
287
288 $ script/myapp_create.pl controller Login
289 $ script/myapp_create.pl controller Logout
290
291 You could easily use a single controller here. For example, you could
292 have a "User" controller with both "login" and "logout" actions.
293 Remember, Catalyst is designed to be very flexible, and leaves such
294 matters up to you, the designer and programmer.
295
296 Then open lib/MyApp/Controller/Login.pm, and update the definition of
297 "sub index" to match:
298
299 =head2 index
300
301 Login logic
302
303 =cut
304
305 sub index :Path :Args(0) {
306 my ($self, $c) = @_;
307
308 # Get the username and password from form
309 my $username = $c->request->params->{username};
310 my $password = $c->request->params->{password};
311
312 # If the username and password values were found in form
313 if ($username && $password) {
314 # Attempt to log the user in
315 if ($c->authenticate({ username => $username,
316 password => $password } )) {
317 # If successful, then let them use the application
318 $c->response->redirect($c->uri_for(
319 $c->controller('Books')->action_for('list')));
320 return;
321 } else {
322 # Set an error message
323 $c->stash(error_msg => "Bad username or password.");
324 }
325 } else {
326 # Set an error message
327 $c->stash(error_msg => "Empty username or password.")
328 unless ($c->user_exists);
329 }
330
331 # If either of above don't work out, send to the login page
332 $c->stash(template => 'login.tt2');
333 }
334
335 This controller fetches the "username" and "password" values from the
336 login form and attempts to authenticate the user. If successful, it
337 redirects the user to the book list page. If the login fails, the user
338 will stay at the login page and receive an error message. If the
339 "username" and "password" values are not present in the form, the user
340 will be taken to the empty login form.
341
342 Note that we could have used something like ""sub default :Path"",
343 however, it is generally recommended (partly for historical reasons,
344 and partly for code clarity) only to use "default" in
345 "MyApp::Controller::Root", and then mainly to generate the 404 not
346 found page for the application.
347
348 Instead, we are using ""sub somename :Path :Args(0) {...}"" here to
349 specifically match the URL "/login". "Path" actions (aka, "literal
350 actions") create URI matches relative to the namespace of the
351 controller where they are defined. Although "Path" supports arguments
352 that allow relative and absolute paths to be defined, here we use an
353 empty "Path" definition to match on just the name of the controller
354 itself. The method name, "index", is arbitrary. We make the match even
355 more specific with the :Args(0) action modifier -- this forces the
356 match on only "/login", not "/login/somethingelse".
357
358 Next, update the corresponding method in lib/MyApp/Controller/Logout.pm
359 to match:
360
361 =head2 index
362
363 Logout logic
364
365 =cut
366
367 sub index :Path :Args(0) {
368 my ($self, $c) = @_;
369
370 # Clear the user's state
371 $c->logout;
372
373 # Send the user to the starting point
374 $c->response->redirect($c->uri_for('/'));
375 }
376
377 Add a Login Form TT Template Page
378 Create a login form by opening root/src/login.tt2 and inserting:
379
380 [% META title = 'Login' %]
381
382 <!-- Login form -->
383 <form method="post" action="[% c.uri_for('/login') %]">
384 <table>
385 <tr>
386 <td>Username:</td>
387 <td><input type="text" name="username" size="40" /></td>
388 </tr>
389 <tr>
390 <td>Password:</td>
391 <td><input type="password" name="password" size="40" /></td>
392 </tr>
393 <tr>
394 <td colspan="2"><input type="submit" name="submit" value="Submit" /></td>
395 </tr>
396 </table>
397 </form>
398
399 Add Valid User Check
400 We need something that provides enforcement for the authentication
401 mechanism -- a global mechanism that prevents users who have not passed
402 authentication from reaching any pages except the login page. This is
403 generally done via an "auto" action/method in
404 lib/MyApp/Controller/Root.pm.
405
406 Edit the existing lib/MyApp/Controller/Root.pm class file and insert
407 the following method:
408
409 =head2 auto
410
411 Check if there is a user and, if not, forward to login page
412
413 =cut
414
415 # Note that 'auto' runs after 'begin' but before your actions and that
416 # 'auto's "chain" (all from application path to most specific class are run)
417 # See the 'Actions' section of 'Catalyst::Manual::Intro' for more info.
418 sub auto :Private {
419 my ($self, $c) = @_;
420
421 # Allow unauthenticated users to reach the login page. This
422 # allows unauthenticated users to reach any action in the Login
423 # controller. To lock it down to a single action, we could use:
424 # if ($c->action eq $c->controller('Login')->action_for('index'))
425 # to only allow unauthenticated access to the 'index' action we
426 # added above.
427 if ($c->controller eq $c->controller('Login')) {
428 return 1;
429 }
430
431 # If a user doesn't exist, force login
432 if (!$c->user_exists) {
433 # Dump a log message to the development server debug output
434 $c->log->debug('***Root::auto User not found, forwarding to /login');
435 # Redirect the user to the login page
436 $c->response->redirect($c->uri_for('/login'));
437 # Return 0 to cancel 'post-auto' processing and prevent use of application
438 return 0;
439 }
440
441 # User found, so return 1 to continue with processing after this 'auto'
442 return 1;
443 }
444
445 As discussed in "CREATE A CATALYST CONTROLLER" in
446 Catalyst::Manual::Tutorial::03_MoreCatalystBasics, every "auto" method
447 from the application/root controller down to the most specific
448 controller will be called. By placing the authentication enforcement
449 code inside the "auto" method of lib/MyApp/Controller/Root.pm (or
450 lib/MyApp.pm), it will be called for every request that is received by
451 the entire application.
452
453 Displaying Content Only to Authenticated Users
454 Let's say you want to provide some information on the login page that
455 changes depending on whether the user has authenticated yet. To do
456 this, open root/src/login.tt2 in your editor and add the following
457 lines to the bottom of the file:
458
459 ...
460 <p>
461 [%
462 # This code illustrates how certain parts of the TT
463 # template will only be shown to users who have logged in
464 %]
465 [% IF c.user_exists %]
466 Please Note: You are already logged in as '[% c.user.username %]'.
467 You can <a href="[% c.uri_for('/logout') %]">logout</a> here.
468 [% ELSE %]
469 You need to log in to use this application.
470 [% END %]
471 [%#
472 Note that this whole block is a comment because the "#" appears
473 immediate after the "[%" (with no spaces in between). Although it
474 can be a handy way to temporarily "comment out" a whole block of
475 TT code, it's probably a little too subtle for use in "normal"
476 comments.
477 %]
478 </p>
479
480 Although most of the code is comments, the middle few lines provide a
481 "you are already logged in" reminder if the user returns to the login
482 page after they have already authenticated. For users who have not yet
483 authenticated, a "You need to log in..." message is displayed (note the
484 use of an IF-THEN-ELSE construct in TT).
485
486 Try Out Authentication
487 The development server should have reloaded each time we edited one of
488 the Controllers in the previous section. Now try going to
489 <http://localhost:3000/books/list> and you should be redirected to the
490 login page, hitting Shift+Reload or Ctrl+Reload if necessary (the "You
491 are already logged in" message should not appear -- if it does, click
492 the "logout" button and try again). Note the "***Root::auto User not
493 found..." debug message in the development server output. Enter
494 username "test01" and password "mypass", and you should be taken to the
495 Book List page.
496
497 IMPORTANT NOTE: If you are having issues with authentication on
498 Internet Explorer (or potentially other browsers), be sure to check the
499 system clocks on both your server and client machines. Internet
500 Explorer is very picky about timestamps for cookies. You can use the
501 "ntpq -p" command on the Tutorial Virtual Machine to check time sync
502 and/or use the following command to force a sync:
503
504 sudo ntpdate-debian
505
506 Or, depending on your firewall configuration, try it with "-u":
507
508 sudo ntpdate-debian -u
509
510 Note: NTP can be a little more finicky about firewalls because it uses
511 UDP vs. the more common TCP that you see with most Internet protocols.
512 Worse case, you might have to manually set the time on your development
513 box instead of using NTP.
514
515 Open root/src/books/list.tt2 and add the following lines to the bottom
516 (below the closing </table> tag):
517
518 ...
519 <p>
520 <a href="[% c.uri_for('/login') %]">Login</a>
521 <a href="[% c.uri_for(c.controller.action_for('form_create')) %]">Create</a>
522 </p>
523
524 Reload your browser and you should now see a "Login" and "Create" links
525 at the bottom of the page (as mentioned earlier, you can update
526 template files without a development server reload). Click the first
527 link to return to the login page. This time you should see the "You
528 are already logged in" message.
529
530 Finally, click the "You can logout here" link on the "/login" page.
531 You should stay at the login page, but the message should change to
532 "You need to log in to use this application."
533
535 In this section we increase the security of our system by converting
536 from cleartext passwords to SHA-1 password hashes that include a random
537 "salt" value to make them extremely difficult to crack, even with
538 dictionary and "rainbow table" attacks.
539
540 Note: This section is optional. You can skip it and the rest of the
541 tutorial will function normally.
542
543 Be aware that even with the techniques shown in this section, the
544 browser still transmits the passwords in cleartext to your application.
545 We are just avoiding the storage of cleartext passwords in the database
546 by using a salted SHA-1 hash. If you are concerned about cleartext
547 passwords between the browser and your application, consider using
548 SSL/TLS, made easy with modules such as Catalyst::Plugin::RequireSSL
549 and Catalyst::ActionRole::RequireSSL.
550
551 Re-Run the DBIC::Schema Model Helper to Include
552 DBIx::Class::PassphraseColumn
553 Let's re-run the model helper to have it include
554 DBIx::Class::PassphraseColumn in all of the Result Classes it generates
555 for us. Simply use the same command we saw in Chapters 3 and 4, but
556 add ",PassphraseColumn" to the "components" argument:
557
558 $ script/myapp_create.pl model DB DBIC::Schema MyApp::Schema \
559 create=static components=TimeStamp,PassphraseColumn dbi:SQLite:myapp.db \
560 on_connect_do="PRAGMA foreign_keys = ON"
561
562 If you then open one of the Result Classes, you will see that it
563 includes PassphraseColumn in the "load_components" line. Take a look
564 at lib/MyApp/Schema/Result/User.pm since that's the main class where we
565 want to use hashed and salted passwords:
566
567 __PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp", "PassphraseColumn");
568
569 Modify the "password" Column to Use PassphraseColumn
570 Open the file lib/MyApp/Schema/Result/User.pm and enter the following
571 text below the "# DO NOT MODIFY THIS OR ANYTHING ABOVE!" line but above
572 the closing "1;":
573
574 # Have the 'password' column use a SHA-1 hash and 20-byte salt
575 # with RFC 2307 encoding; Generate the 'check_password" method
576 __PACKAGE__->add_columns(
577 'password' => {
578 passphrase => 'rfc2307',
579 passphrase_class => 'SaltedDigest',
580 passphrase_args => {
581 algorithm => 'SHA-1',
582 salt_random => 20,
583 },
584 passphrase_check_method => 'check_password',
585 },
586 );
587
588 This redefines the automatically generated definition for the password
589 fields at the top of the Result Class file to now use PassphraseColumn
590 logic, storing passwords in RFC 2307 format ("passphrase" is set to
591 "rfc2307"). "passphrase_class" can be set to the name of any
592 "Authen::Passphrase::*" class, such as "SaltedDigest" to use
593 Authen::Passphrase::SaltedDigest, or "BlowfishCrypt" to use
594 Authen::Passphrase::BlowfishCrypt. "passphrase_args" is then used to
595 customize the passphrase class you selected. Here we specified the
596 digest algorithm to use as "SHA-1" and the size of the salt to use, but
597 we could have also specified any other option the selected passphrase
598 class supports.
599
600 Load Hashed Passwords in the Database
601 Next, let's create a quick script to load some hashed and salted
602 passwords into the "password" column of our "users" table. Open the
603 file set_hashed_passwords.pl in your editor and enter the following
604 text:
605
606 #!/usr/bin/perl
607
608 use strict;
609 use warnings;
610
611 use MyApp::Schema;
612
613 my $schema = MyApp::Schema->connect('dbi:SQLite:myapp.db');
614
615 my @users = $schema->resultset('User')->all;
616
617 foreach my $user (@users) {
618 $user->password('mypass');
619 $user->update;
620 }
621
622 PassphraseColumn lets us simply call "$user->check_password($password)"
623 to see if the user has supplied the correct password, or, as we show
624 above, call "$user->update($new_password)" to update the hashed
625 password stored for this user.
626
627 Then run the following command:
628
629 $ DBIC_TRACE=1 perl -Ilib set_hashed_passwords.pl
630
631 We had to use the "-Ilib" argument to tell Perl to look under the lib
632 directory for our "MyApp::Schema" model.
633
634 The DBIC_TRACE output should show that the update worked:
635
636 $ DBIC_TRACE=1 perl -Ilib set_hashed_passwords.pl
637 SELECT me.id, me.username, me.password, me.email_address,
638 me.first_name, me.last_name, me.active FROM users me:
639 UPDATE users SET password = ? WHERE ( id = ? ):
640 '{SSHA}esgz64CpHMo8pMfgIIszP13ft23z/zio04aCwNdm0wc6MDeloMUH4g==', '1'
641 UPDATE users SET password = ? WHERE ( id = ? ):
642 '{SSHA}FpGhpCJus+Ea9ne4ww8404HH+hJKW/fW+bAv1v6FuRUy2G7I2aoTRQ==', '2'
643 UPDATE users SET password = ? WHERE ( id = ? ):
644 '{SSHA}ZyGlpiHls8qFBSbHr3r5t/iqcZE602XLMbkSVRRNl6rF8imv1abQVg==', '3'
645
646 But we can further confirm our actions by dumping the users table:
647
648 $ sqlite3 myapp.db "select * from users"
649 1|test01|{SSHA}esgz64CpHMo8pMfgIIszP13ft23z/zio04aCwNdm0wc6MDeloMUH4g==|t01@na.com|Joe|Blow|1
650 2|test02|{SSHA}FpGhpCJus+Ea9ne4ww8404HH+hJKW/fW+bAv1v6FuRUy2G7I2aoTRQ==|t02@na.com|Jane|Doe|1
651 3|test03|{SSHA}ZyGlpiHls8qFBSbHr3r5t/iqcZE602XLMbkSVRRNl6rF8imv1abQVg==|t03@na.com|No|Go|0
652
653 As you can see, the passwords are much harder to steal from the
654 database (not only are the hashes stored, but every hash is different
655 even though the passwords are the same because of the added "salt"
656 value). Also note that this demonstrates how to use a DBIx::Class
657 model outside of your web application -- a very useful feature in many
658 situations.
659
660 Enable Hashed and Salted Passwords
661 Edit lib/MyApp.pm and update the config() section for
662 "Plugin::Authentication" it to match the following text (the only
663 change is to the "password_type" field):
664
665 # Configure SimpleDB Authentication
666 __PACKAGE__->config(
667 'Plugin::Authentication' => {
668 default => {
669 class => 'SimpleDB',
670 user_model => 'DB::User',
671 password_type => 'self_check',
672 },
673 },
674 );
675
676 The use of "self_check" will cause
677 Catalyst::Plugin::Authentication::Store::DBIx::Class to call the
678 "check_password" method we enabled on our "password" columns.
679
680 Try Out the Hashed Passwords
681 The development server should restart as soon as your save the
682 lib/MyApp.pm file in the previous section. You should now be able to go
683 to <http://localhost:3000/books/list> and login as before. When done,
684 click the "logout" link on the login page (or point your browser at
685 <http://localhost:3000/logout>).
686
688 As discussed in the previous chapter of the tutorial, "flash" allows
689 you to set variables in a way that is very similar to "stash", but it
690 will remain set across multiple requests. Once the value is read, it
691 is cleared (unless reset). Although "flash" has nothing to do with
692 authentication, it does leverage the same session plugins. Now that
693 those plugins are enabled, let's go back and update the "delete and
694 redirect with query parameters" code seen at the end of the Basic CRUD
695 chapter of the tutorial to take advantage of "flash".
696
697 First, open lib/MyApp/Controller/Books.pm and modify "sub delete" to
698 match the following (everything after the model search line of code has
699 changed):
700
701 =head2 delete
702
703 Delete a book
704
705 =cut
706
707 sub delete :Chained('object') :PathPart('delete') :Args(0) {
708 my ($self, $c) = @_;
709
710 # Use the book object saved by 'object' and delete it along
711 # with related 'book_authors' entries
712 $c->stash->{object}->delete;
713
714 # Use 'flash' to save information across requests until it's read
715 $c->flash->{status_msg} = "Book deleted";
716
717 # Redirect the user back to the list page
718 $c->response->redirect($c->uri_for($self->action_for('list')));
719 }
720
721 Next, open root/src/wrapper.tt2 and update the TT code to pull from
722 flash vs. the "status_msg" query parameter:
723
724 ...
725 <div id="content">
726 [%# Status and error messages %]
727 <span class="message">[% status_msg || c.flash.status_msg %]</span>
728 <span class="error">[% error_msg %]</span>
729 [%# This is where TT will stick all of your template's contents. -%]
730 [% content %]
731 </div><!-- end content -->
732 ...
733
734 Although the sample above only shows the "content" div, leave the rest
735 of the file intact -- the only change we made to replace ""||
736 c.request.params.status_msg"" with ""c.flash.status_msg"" in the "<span
737 class="message">" line.
738
739 Try Out Flash
740 Authenticate using the login screen and then point your browser to
741 <http://localhost:3000/books/url_create/Test/1/4> to create an extra
742 several books. Click the "Return to list" link and delete one of the
743 "Test" books you just added. The "flash" mechanism should retain our
744 "Book deleted" status message across the redirect.
745
746 NOTE: While "flash" will save information across multiple requests, it
747 does get cleared the first time it is read. In general, this is
748 exactly what you want -- the "flash" message will get displayed on the
749 next screen where it's appropriate, but it won't "keep showing up"
750 after that first time (unless you reset it). Please refer to
751 Catalyst::Plugin::Session for additional information.
752
753 Note: There is also a "flash-to-stash" feature that will automatically
754 load the contents the contents of flash into stash, allowing us to use
755 the more typical "c.flash.status_msg" in our TT template in lieu of the
756 more verbose "status_msg || c.flash.status_msg" we used above. Consult
757 Catalyst::Plugin::Session for additional information.
758
759 Switch To Catalyst::Plugin::StatusMessages
760 Although the query parameter technique we used in Chapter 4 and the
761 "flash" approach we used above will work in most cases, they both have
762 their drawbacks. The query parameters can leave the status message on
763 the screen longer than it should (for example, if the user hits
764 refresh). And "flash" can display the wrong message on the wrong
765 screen (flash just shows the message on the next page for that user...
766 if the user has multiple windows or tabs open, then the wrong one can
767 get the status message).
768
769 Catalyst::Plugin::StatusMessage is designed to address these
770 shortcomings. It stores the messages in the user's session (so they
771 are available across multiple requests), but ties each status message
772 to a random token. By passing this token across the redirect, we are
773 no longer relying on a potentially ambiguous "next request" like we do
774 with flash. And, because the message is deleted the first time it's
775 displayed, the user can hit refresh and still only see the message a
776 single time (even though the URL may continue to reference the token,
777 it's only displayed the first time). The use of "StatusMessage" or a
778 similar mechanism is recommended for all Catalyst applications.
779
780 To enable "StatusMessage", first edit lib/MyApp.pm and add
781 "StatusMessage" to the list of plugins:
782
783 use Catalyst qw/
784 -Debug
785 ConfigLoader
786 Static::Simple
787
788 StackTrace
789
790 Authentication
791
792 Session
793 Session::Store::File
794 Session::State::Cookie
795
796 StatusMessage
797 /;
798
799 Then edit lib/MyApp/Controller/Books.pm and modify the "delete" action
800 to match the following:
801
802 sub delete :Chained('object') :PathPart('delete') :Args(0) {
803 my ($self, $c) = @_;
804
805 # Saved the PK id for status_msg below
806 my $id = $c->stash->{object}->id;
807
808 # Use the book object saved by 'object' and delete it along
809 # with related 'book_authors' entries
810 $c->stash->{object}->delete;
811
812 # Redirect the user back to the list page
813 $c->response->redirect($c->uri_for($self->action_for('list'),
814 {mid => $c->set_status_msg("Deleted book $id")}));
815 }
816
817 This uses the "set_status_msg" that the plugin added to $c to save the
818 message under a random token. (If we wanted to save an error message,
819 we could have used "set_error_msg".) Because "set_status_msg" and
820 "set_error_msg" both return the random token, we can assign that value
821 to the ""mid"" query parameter via "uri_for" as shown above.
822
823 Next, we need to make sure that the list page will load display the
824 message. The easiest way to do this is to take advantage of the
825 chained dispatch we implemented in Chapter 4. Edit
826 lib/MyApp/Controller/Books.pm again and update the "base" action to
827 match:
828
829 sub base :Chained('/') :PathPart('books') :CaptureArgs(0) {
830 my ($self, $c) = @_;
831
832 # Store the ResultSet in stash so it's available for other methods
833 $c->stash(resultset => $c->model('DB::Book'));
834
835 # Print a message to the debug log
836 $c->log->debug('*** INSIDE BASE METHOD ***');
837
838 # Load status messages
839 $c->load_status_msgs;
840 }
841
842 That way, anything that chains off "base" will automatically get any
843 status or error messages loaded into the stash. Let's convert the
844 "list" action to take advantage of this. Modify the method signature
845 for "list" from:
846
847 sub list :Local {
848
849 to:
850
851 sub list :Chained('base') :PathPart('list') :Args(0) {
852
853 Finally, let's clean up the status/error message code in our wrapper
854 template. Edit root/src/wrapper.tt2 and change the "content" div to
855 match the following:
856
857 <div id="content">
858 [%# Status and error messages %]
859 <span class="message">[% status_msg %]</span>
860 <span class="error">[% error_msg %]</span>
861 [%# This is where TT will stick all of your template's contents. -%]
862 [% content %]
863 </div><!-- end content -->
864
865 Now go to <http://localhost:3000/books/list> in your browser. Delete
866 another of the "Test" books you added in the previous step. You should
867 get redirection from the "delete" action back to the "list" action, but
868 with a "mid=########" message ID query parameter. The screen should
869 say "Deleted book #" (where # is the PK id of the book you removed).
870 However, if you hit refresh in your browser, the status message is no
871 longer displayed (even though the URL does still contain the message
872 ID token, it is ignored -- thereby keeping the state of our
873 status/error messages in sync with the users actions).
874
875 You can jump to the next chapter of the tutorial here: Authorization
876
878 Kennedy Clark, "hkclark@gmail.com"
879
880 Feel free to contact the author for any errors or suggestions, but the
881 best way to report issues is via the CPAN RT Bug system at
882 <https://rt.cpan.org/Public/Dist/Display.html?Name=Catalyst-Manual>.
883
884 Copyright 2006-2011, Kennedy Clark, under the Creative Commons
885 Attribution Share-Alike License Version 3.0
886 (<https://creativecommons.org/licenses/by-sa/3.0/us/>).
887
888
889
890perl v5.32.0 Cata2l0y2s0t-:0:7M-a2n8ual::Tutorial::05_Authentication(3)