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