Commit | Line | Data |
d442cc9f |
1 | =head1 NAME |
2 | |
3 | Catalyst::Manual::Tutorial::Authentication - Catalyst Tutorial - Part 4: Authentication |
4 | |
5 | |
6 | =head1 OVERVIEW |
7 | |
8 | This is B<Part 4 of 9> for the Catalyst tutorial. |
9 | |
10 | L<Tutorial Overview|Catalyst::Manual::Tutorial> |
11 | |
12 | =over 4 |
13 | |
14 | =item 1 |
15 | |
16 | L<Introduction|Catalyst::Manual::Tutorial::Intro> |
17 | |
18 | =item 2 |
19 | |
20 | L<Catalyst Basics|Catalyst::Manual::Tutorial::CatalystBasics> |
21 | |
22 | =item 3 |
23 | |
24 | L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD> |
25 | |
26 | =item 4 |
27 | |
28 | B<Authentication> |
29 | |
30 | =item 5 |
31 | |
32 | L<Authorization|Catalyst::Manual::Tutorial::Authorization> |
33 | |
34 | =item 6 |
35 | |
36 | L<Debugging|Catalyst::Manual::Tutorial::Debugging> |
37 | |
38 | =item 7 |
39 | |
40 | L<Testing|Catalyst::Manual::Tutorial::Testing> |
41 | |
42 | =item 8 |
43 | |
44 | L<AdvancedCRUD|Catalyst::Manual::Tutorial::AdvancedCRUD> |
45 | |
46 | =item 9 |
47 | |
48 | L<Appendices|Catalyst::Manual::Tutorial::Appendices> |
49 | |
50 | =back |
51 | |
52 | |
2d0526d1 |
53 | =head1 IMPORTANT NOTE |
54 | |
55 | Since this tutorial was written, there has been a new Authentication |
56 | API released (Catalyst::Plugin::Authentication version 0.1 and later). |
57 | Some of this tutorial does not work with this API, and requires |
58 | minimal changes. For an example application that uses the new API see |
59 | L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/examples/NewAuthApp/NewAuthApp-0.01.tar.gz>. It |
60 | is recommended that you read this tutorial first, and then download |
61 | the source code linked above to understand the differences. |
62 | |
d442cc9f |
63 | =head1 DESCRIPTION |
64 | |
65 | Now that we finally have a simple yet functional application, we can |
66 | focus on providing authentication (with authorization coming next in |
67 | Part 5). |
68 | |
69 | This part of the tutorial is divided into two main sections: 1) basic, |
70 | cleartext authentication and 2) hash-based authentication. |
71 | |
72 | You can checkout the source code for this example from the catalyst |
73 | subversion repository as per the instructions in |
74 | L<Catalyst::Manual::Tutorial::Intro> |
75 | |
76 | =head1 BASIC AUTHENTICATION |
77 | |
78 | This section explores how to add authentication logic to a Catalyst |
79 | application. |
80 | |
81 | |
82 | =head2 Add Users and Roles to the Database |
83 | |
84 | First, we add both user and role information to the database (we will |
85 | add the role information here although it will not be used until the |
86 | authorization section, Part 5). Create a new SQL script file by opening |
87 | C<myapp02.sql> in your editor and insert: |
88 | |
89 | -- |
90 | -- Add users and roles tables, along with a many-to-many join table |
91 | -- |
92 | CREATE TABLE users ( |
93 | id INTEGER PRIMARY KEY, |
94 | username TEXT, |
95 | password TEXT, |
96 | email_address TEXT, |
97 | first_name TEXT, |
98 | last_name TEXT, |
99 | active INTEGER |
100 | ); |
101 | CREATE TABLE roles ( |
102 | id INTEGER PRIMARY KEY, |
103 | role TEXT |
104 | ); |
105 | CREATE TABLE user_roles ( |
106 | user_id INTEGER, |
107 | role_id INTEGER, |
108 | PRIMARY KEY (user_id, role_id) |
109 | ); |
110 | -- |
111 | -- Load up some initial test data |
112 | -- |
113 | INSERT INTO users VALUES (1, 'test01', 'mypass', 't01@na.com', 'Joe', 'Blow', 1); |
114 | INSERT INTO users VALUES (2, 'test02', 'mypass', 't02@na.com', 'Jane', 'Doe', 1); |
115 | INSERT INTO users VALUES (3, 'test03', 'mypass', 't03@na.com', 'No', 'Go', 0); |
116 | INSERT INTO roles VALUES (1, 'user'); |
117 | INSERT INTO roles VALUES (2, 'admin'); |
118 | INSERT INTO user_roles VALUES (1, 1); |
119 | INSERT INTO user_roles VALUES (1, 2); |
120 | INSERT INTO user_roles VALUES (2, 1); |
121 | INSERT INTO user_roles VALUES (3, 1); |
122 | |
123 | Then load this into the C<myapp.db> database with the following command: |
124 | |
125 | $ sqlite3 myapp.db < myapp02.sql |
126 | |
127 | |
128 | =head2 Add User and Role Information to DBIC Schema |
129 | |
130 | This step adds DBIC-based classes for the user-related database tables |
131 | (the role information will not be used until Part 5): |
132 | |
133 | Edit C<lib/MyAppDB.pm> and update the contents to match (only the |
134 | C<MyAppDB =E<gt> [qw/Book BookAuthor Author User UserRole Role/]> line |
135 | has changed): |
136 | |
137 | package MyAppDB; |
138 | |
139 | =head1 NAME |
140 | |
141 | MyAppDB -- DBIC Schema Class |
142 | |
143 | =cut |
144 | |
145 | # Our schema needs to inherit from 'DBIx::Class::Schema' |
146 | use base qw/DBIx::Class::Schema/; |
147 | |
148 | # Need to load the DB Model classes here. |
149 | # You can use this syntax if you want: |
150 | # __PACKAGE__->load_classes(qw/Book BookAuthor Author User UserRole Role/); |
151 | # Also, if you simply want to load all of the classes in a directory |
152 | # of the same name as your schema class (as we do here) you can use: |
153 | # __PACKAGE__->load_classes(qw//); |
154 | # But the variation below is more flexible in that it can be used to |
155 | # load from multiple namespaces. |
156 | __PACKAGE__->load_classes({ |
157 | MyAppDB => [qw/Book BookAuthor Author User UserRole Role/] |
158 | }); |
159 | |
160 | 1; |
161 | |
162 | |
163 | =head2 Create New "Result Source Objects" |
164 | |
165 | Create the following three files with the content shown below. |
166 | |
167 | C<lib/MyAppDB/User.pm>: |
168 | |
169 | package MyAppDB::User; |
170 | |
171 | use base qw/DBIx::Class/; |
172 | |
173 | # Load required DBIC stuff |
174 | __PACKAGE__->load_components(qw/PK::Auto Core/); |
175 | # Set the table name |
176 | __PACKAGE__->table('users'); |
177 | # Set columns in table |
178 | __PACKAGE__->add_columns(qw/id username password email_address first_name last_name/); |
179 | # Set the primary key for the table |
180 | __PACKAGE__->set_primary_key('id'); |
181 | |
182 | # |
183 | # Set relationships: |
184 | # |
185 | |
186 | # has_many(): |
187 | # args: |
188 | # 1) Name of relationship, DBIC will create accessor with this name |
189 | # 2) Name of the model class referenced by this relationship |
190 | # 3) Column name in *foreign* table |
191 | __PACKAGE__->has_many(map_user_role => 'MyAppDB::UserRole', 'user_id'); |
192 | |
193 | |
194 | =head1 NAME |
195 | |
196 | MyAppDB::User - A model object representing a person with access to the system. |
197 | |
198 | =head1 DESCRIPTION |
199 | |
200 | This is an object that represents a row in the 'users' table of your application |
201 | database. It uses DBIx::Class (aka, DBIC) to do ORM. |
202 | |
203 | For Catalyst, this is designed to be used through MyApp::Model::MyAppDB. |
204 | Offline utilities may wish to use this class directly. |
205 | |
206 | =cut |
207 | |
208 | 1; |
209 | |
210 | |
211 | C<lib/MyAppDB/Role.pm>: |
212 | |
213 | package MyAppDB::Role; |
214 | |
215 | use base qw/DBIx::Class/; |
216 | |
217 | # Load required DBIC stuff |
218 | __PACKAGE__->load_components(qw/PK::Auto Core/); |
219 | # Set the table name |
220 | __PACKAGE__->table('roles'); |
221 | # Set columns in table |
222 | __PACKAGE__->add_columns(qw/id role/); |
223 | # Set the primary key for the table |
224 | __PACKAGE__->set_primary_key('id'); |
225 | |
226 | # |
227 | # Set relationships: |
228 | # |
229 | |
230 | # has_many(): |
231 | # args: |
232 | # 1) Name of relationship, DBIC will create accessor with this name |
233 | # 2) Name of the model class referenced by this relationship |
234 | # 3) Column name in *foreign* table |
235 | __PACKAGE__->has_many(map_user_role => 'MyAppDB::UserRole', 'role_id'); |
236 | |
237 | |
238 | =head1 NAME |
239 | |
240 | MyAppDB::Role - A model object representing a class of access permissions to |
241 | the system. |
242 | |
243 | =head1 DESCRIPTION |
244 | |
245 | This is an object that represents a row in the 'roles' table of your |
246 | application database. It uses DBIx::Class (aka, DBIC) to do ORM. |
247 | |
248 | For Catalyst, this is designed to be used through MyApp::Model::MyAppDB. |
249 | "Offline" utilities may wish to use this class directly. |
250 | |
251 | =cut |
252 | |
253 | 1; |
254 | |
255 | |
256 | C<lib/MyAppDB/UserRole.pm>: |
257 | |
258 | package MyAppDB::UserRole; |
259 | |
260 | use base qw/DBIx::Class/; |
261 | |
262 | # Load required DBIC stuff |
263 | __PACKAGE__->load_components(qw/PK::Auto Core/); |
264 | # Set the table name |
265 | __PACKAGE__->table('user_roles'); |
266 | # Set columns in table |
267 | __PACKAGE__->add_columns(qw/user_id role_id/); |
268 | # Set the primary key for the table |
269 | __PACKAGE__->set_primary_key(qw/user_id role_id/); |
270 | |
271 | # |
272 | # Set relationships: |
273 | # |
274 | |
275 | # belongs_to(): |
276 | # args: |
277 | # 1) Name of relationship, DBIC will create accessor with this name |
278 | # 2) Name of the model class referenced by this relationship |
279 | # 3) Column name in *this* table |
280 | __PACKAGE__->belongs_to(user => 'MyAppDB::User', 'user_id'); |
281 | |
282 | # belongs_to(): |
283 | # args: |
284 | # 1) Name of relationship, DBIC will create accessor with this name |
285 | # 2) Name of the model class referenced by this relationship |
286 | # 3) Column name in *this* table |
287 | __PACKAGE__->belongs_to(role => 'MyAppDB::Role', 'role_id'); |
288 | |
289 | |
290 | =head1 NAME |
291 | |
292 | MyAppDB::UserRole - A model object representing the JOIN between Users and Roles. |
293 | |
294 | =head1 DESCRIPTION |
295 | |
296 | This is an object that represents a row in the 'user_roles' table of your application |
297 | database. It uses DBIx::Class (aka, DBIC) to do ORM. |
298 | |
299 | You probably won't need to use this class directly -- it will be automatically |
300 | used by DBIC where joins are needed. |
301 | |
302 | For Catalyst, this is designed to be used through MyApp::Model::MyAppDB. |
303 | Offline utilities may wish to use this class directly. |
304 | |
305 | =cut |
306 | |
307 | 1; |
308 | |
309 | The code for these three result source classes is obviously very familiar to the C<Book>, C<Author>, and C<BookAuthor> classes created in Part 2. |
310 | |
311 | |
312 | =head2 Sanity-Check Reload of Development Server |
313 | |
314 | We aren't ready to try out the authentication just yet; we only want to do a quick check to be sure our model loads correctly. Press C<Ctrl-C> to kill the previous server instance (if it's still running) and restart it: |
315 | |
316 | $ script/myapp_server.pl |
317 | |
318 | Look for the three new model objects in the startup debug output: |
319 | |
320 | ... |
321 | .-------------------------------------------------------------------+----------. |
322 | | Class | Type | |
323 | +-------------------------------------------------------------------+----------+ |
324 | | MyApp::Controller::Books | instance | |
325 | | MyApp::Controller::Root | instance | |
326 | | MyApp::Model::MyAppDB | instance | |
327 | | MyApp::Model::MyAppDB::Author | class | |
328 | | MyApp::Model::MyAppDB::Book | class | |
329 | | MyApp::Model::MyAppDB::BookAuthor | class | |
330 | | MyApp::Model::MyAppDB::Role | class | |
331 | | MyApp::Model::MyAppDB::User | class | |
332 | | MyApp::Model::MyAppDB::UserRole | class | |
333 | | MyApp::View::TT | instance | |
334 | '-------------------------------------------------------------------+----------' |
335 | ... |
336 | |
337 | Again, notice that your "result source" classes have been "re-loaded" by Catalyst under C<MyApp::Model>. |
338 | |
339 | |
340 | =head2 Include Authentication and Session Plugins |
341 | |
342 | Edit C<lib/MyApp.pm> and update it as follows (everything below C<StackTrace> is new): |
343 | |
344 | use Catalyst qw/ |
345 | -Debug |
346 | ConfigLoader |
347 | Static::Simple |
348 | |
349 | StackTrace |
350 | |
351 | Authentication |
d442cc9f |
352 | |
353 | Session |
354 | Session::Store::FastMmap |
355 | Session::State::Cookie |
356 | /; |
357 | |
6d0971ad |
358 | The C<Authentication> plugin supports |
d442cc9f |
359 | Authentication while the C<Session> plugins are required to maintain |
6d0971ad |
360 | state across multiple HTTP requests. |
361 | |
362 | Note that the only required Authentication class is the main |
363 | one. This is a change that occured in version 0.09999_01 |
364 | of the C<Authentication> plugin. You B<do not need> to specify a |
365 | particular Authentication::Store or Authentication::Credential plugin. |
366 | Instead, indicate the Store and Credential you want to use in your application |
367 | configuration (see below). |
368 | |
369 | Note that there are several |
e74b1cd1 |
370 | options for L<Session::Store|Catalyst::Plugin::Session::Store> |
d442cc9f |
371 | (L<Session::Store::FastMmap|Catalyst::Plugin::Session::Store::FastMmap> |
372 | is generally a good choice if you are on Unix; try |
e74b1cd1 |
373 | L<Session::Store::File|Catalyst::Plugin::Session::Store::File> if you |
374 | are on Win32) -- consult |
375 | L<Session::Store|Catalyst::Plugin::Session::Store> and its subclasses |
376 | for additional information and options (for example to use a |
377 | database-backed session store). |
d442cc9f |
378 | |
379 | |
380 | =head2 Configure Authentication |
381 | |
382 | Although C<__PACKAGE__-E<gt>config(name =E<gt> 'value');> is still |
383 | supported, newer Catalyst applications tend to place all configuration |
384 | information in C<myapp.yml> and automatically load this information into |
385 | C<MyApp-E<gt>config> using the |
386 | L<ConfigLoader|Catalyst::Plugin::ConfigLoader> plugin. Here, we need |
387 | to load several parameters that tell |
388 | L<Catalyst::Plugin::Authentication|Catalyst::Plugin::Authentication> |
389 | where to locate information in your database. To do this, edit the |
390 | C<myapp.yml> YAML and update it to match: |
391 | |
392 | --- |
393 | name: MyApp |
394 | authentication: |
6d0971ad |
395 | default_realm: dbic |
396 | realms: |
397 | dbic: |
398 | credential: |
399 | class: Password |
400 | password_field: password |
401 | password_type: self_check |
402 | store: |
403 | class: DBIx::Class |
d442cc9f |
404 | # This is the model object created by Catalyst::Model::DBIC from your |
405 | # schema (you created 'MyAppDB::User' but as the Catalyst startup |
406 | # debug messages show, it was loaded as 'MyApp::Model::MyAppDB::User'). |
407 | # NOTE: Omit 'MyApp::Model' to avoid a component lookup issue in Catalyst 5.66 |
6d0971ad |
408 | user_class: MyApp::Users |
d442cc9f |
409 | # This is the name of the field in your 'users' table that contains the user's name |
6d0971ad |
410 | id_field: username |
411 | role_relation: roles |
412 | role_field: rolename |
413 | ignore_fields_in_find: [ 'remote_name' ] |
d442cc9f |
414 | |
415 | Inline comments in the code above explain how each field is being used. |
416 | |
417 | B<TIP>: Although YAML uses a very simple and easy-to-ready format, it |
418 | does require the use of a consistent level of indenting. Be sure you |
419 | line up everything on a given 'level' with the same number of indents. |
420 | Also, be sure not to use C<tab> characters (YAML does not support them |
421 | because they are handled inconsistently across editors). |
422 | |
423 | |
424 | =head2 Add Login and Logout Controllers |
425 | |
426 | Use the Catalyst create script to create two stub controller files: |
427 | |
428 | $ script/myapp_create.pl controller Login |
429 | $ script/myapp_create.pl controller Logout |
430 | |
431 | B<NOTE>: You could easily use a single controller here. For example, |
432 | you could have a C<User> controller with both C<login> and C<logout> |
433 | actions. Remember, Catalyst is designed to be very flexible, and leaves |
434 | such matters up to you, the designer and programmer. |
435 | |
436 | Then open C<lib/MyApp/Controller/Login.pm>, locate the C<sub index : |
437 | Private> method (this was automatically inserted by the helpers when we |
438 | created the Login controller above), and delete this line: |
439 | |
440 | $c->response->body('Matched MyApp::Controller::Login in Login.'); |
441 | |
442 | Then update it to match: |
443 | |
444 | =head2 index |
445 | |
446 | Login logic |
447 | |
448 | =cut |
449 | |
450 | sub index : Private { |
451 | my ($self, $c) = @_; |
452 | |
453 | # Get the username and password from form |
454 | my $username = $c->request->params->{username} || ""; |
455 | my $password = $c->request->params->{password} || ""; |
456 | |
457 | # If the username and password values were found in form |
458 | if ($username && $password) { |
459 | # Attempt to log the user in |
f632e28b |
460 | if ($c->authenticate({ username => $username, |
461 | password => $password} )) { |
d442cc9f |
462 | # If successful, then let them use the application |
463 | $c->response->redirect($c->uri_for('/books/list')); |
464 | return; |
465 | } else { |
466 | # Set an error message |
467 | $c->stash->{error_msg} = "Bad username or password."; |
468 | } |
469 | } |
470 | |
471 | # If either of above don't work out, send to the login page |
472 | $c->stash->{template} = 'login.tt2'; |
473 | } |
474 | |
475 | This controller fetches the C<username> and C<password> values from the |
f632e28b |
476 | login form and attempts to authenticate the user. If successful, it |
477 | redirects the user to the book list page. If the login fails, the user |
478 | will stay at the login page but receive an error message. If the |
479 | C<username> and C<password> values are not present in the form, the |
480 | user will be taken to the empty login form. |
d442cc9f |
481 | |
482 | Note that we could have used something like C<sub default :Private>; |
483 | however, the use of C<default> actions is discouraged because it does |
484 | not receive path args as with other actions. The recommended practice |
485 | is to only use C<default> in C<MyApp::Controller::Root>. |
486 | |
487 | Another option would be to use something like |
488 | C<sub base :Path :Args(0) {...}> (where the C<...> refers to the login |
489 | code shown in C<sub index : Private> above). We are using C<sub base |
490 | :Path :Args(0) {...}> here to specifically match the URL C</login>. |
491 | C<Path> actions (aka, "literal actions") create URI matches relative to |
492 | the namespace of the controller where they are defined. Although |
493 | C<Path> supports arguments that allow relative and absolute paths to be |
494 | defined, here we use an empty C<Path> definition to match on just the |
495 | name of the controller itself. The method name, C<base>, is arbitrary. |
496 | We make the match even more specific with the C<:Args(0)> action |
497 | modifier -- this forces the match on I<only> C</login>, not |
498 | C</login/somethingelse>. |
499 | |
500 | Next, update the corresponding method in C<lib/MyApp/Controller/Logout.pm> |
501 | to match: |
502 | |
503 | =head2 index |
504 | |
505 | Logout logic |
506 | |
507 | =cut |
508 | |
509 | sub index : Private { |
510 | my ($self, $c) = @_; |
511 | |
512 | # Clear the user's state |
513 | $c->logout; |
514 | |
515 | # Send the user to the starting point |
516 | $c->response->redirect($c->uri_for('/')); |
517 | } |
518 | |
519 | As with the login controller, be sure to delete the |
520 | C<$c->response->body('Matched MyApp::Controller::Logout in Logout.');> |
521 | line of the C<sub index>. |
522 | |
523 | |
524 | =head2 Add a Login Form TT Template Page |
525 | |
526 | Create a login form by opening C<root/src/login.tt2> and inserting: |
527 | |
528 | [% META title = 'Login' %] |
529 | |
530 | <!-- Login form --> |
531 | <form method="post" action=" [% Catalyst.uri_for('/login') %] "> |
532 | <table> |
533 | <tr> |
534 | <td>Username:</td> |
535 | <td><input type="text" name="username" size="40" /></td> |
536 | </tr> |
537 | <tr> |
538 | <td>Password:</td> |
539 | <td><input type="password" name="password" size="40" /></td> |
540 | </tr> |
541 | <tr> |
542 | <td colspan="2"><input type="submit" name="submit" value="Submit" /></td> |
543 | </tr> |
544 | </table> |
545 | </form> |
546 | |
547 | |
548 | =head2 Add Valid User Check |
549 | |
550 | We need something that provides enforcement for the authentication |
551 | mechanism -- a I<global> mechanism that prevents users who have not |
552 | passed authentication from reaching any pages except the login page. |
553 | This is generally done via an C<auto> action/method (prior to Catalyst |
554 | v5.66, this sort of thing would go in C<MyApp.pm>, but starting in |
555 | v5.66, the preferred location is C<lib/MyApp/Controller/Root.pm>). |
556 | |
557 | Edit the existing C<lib/MyApp/Controller/Root.pm> class file and insert |
558 | the following method: |
559 | |
560 | =head2 auto |
561 | |
562 | Check if there is a user and, if not, forward to login page |
563 | |
564 | =cut |
565 | |
566 | # Note that 'auto' runs after 'begin' but before your actions and that |
567 | # 'auto' "chain" (all from application path to most specific class are run) |
568 | # See the 'Actions' section of 'Catalyst::Manual::Intro' for more info. |
569 | sub auto : Private { |
570 | my ($self, $c) = @_; |
571 | |
572 | # Allow unauthenticated users to reach the login page. This |
573 | # allows anauthenticated users to reach any action in the Login |
574 | # controller. To lock it down to a single action, we could use: |
575 | # if ($c->action eq $c->controller('Login')->action_for('index')) |
576 | # to only allow unauthenticated access to the C<index> action we |
577 | # added above. |
578 | if ($c->controller eq $c->controller('Login')) { |
579 | return 1; |
580 | } |
581 | |
582 | # If a user doesn't exist, force login |
583 | if (!$c->user_exists) { |
584 | # Dump a log message to the development server debug output |
585 | $c->log->debug('***Root::auto User not found, forwarding to /login'); |
586 | # Redirect the user to the login page |
587 | $c->response->redirect($c->uri_for('/login')); |
588 | # Return 0 to cancel 'post-auto' processing and prevent use of application |
589 | return 0; |
590 | } |
591 | |
592 | # User found, so return 1 to continue with processing after this 'auto' |
593 | return 1; |
594 | } |
595 | |
596 | B<Note:> Catalyst provides a number of different types of actions, such |
597 | as C<Local>, C<Regex>, and C<Private>. You should refer to |
598 | L<Catalyst::Manual::Intro> for a more detailed explanation, but the |
599 | following bullet points provide a quick introduction: |
600 | |
601 | =over 4 |
602 | |
603 | =item * |
604 | |
605 | The majority of application use C<Local> actions for items that respond |
606 | to user requests and C<Private> actions for those that do not directly |
607 | respond to user input. |
608 | |
609 | =item * |
610 | |
611 | There are five types of C<Private> actions: C<begin>, C<end>, |
612 | C<default>, C<index>, and C<auto>. |
613 | |
614 | =item * |
615 | |
616 | With C<begin>, C<end>, C<default>, C<index> private actions, only the |
617 | most specific action of each type will be called. For example, if you |
618 | define a C<begin> action in your controller it will I<override> a |
619 | C<begin> action in your application/root controller -- I<only> the |
620 | action in your controller will be called. |
621 | |
622 | =item * |
623 | |
624 | Unlike the other actions where only a single method is called for each |
625 | request, I<every> auto action along the chain of namespaces will be |
626 | called. Each C<auto> action will be called I<from the application/root |
627 | controller down through the most specific class>. |
628 | |
629 | =back |
630 | |
631 | By placing the authentication enforcement code inside the C<auto> method |
632 | of C<lib/MyApp/Controller/Root.pm> (or C<lib/MyApp.pm>), it will be |
633 | called for I<every> request that is received by the entire application. |
634 | |
635 | |
636 | =head2 Displaying Content Only to Authenticated Users |
637 | |
638 | Let's say you want to provide some information on the login page that |
639 | changes depending on whether the user has authenticated yet. To do |
640 | this, open C<root/src/login.tt2> in your editor and add the following |
641 | lines to the bottom of the file: |
642 | |
643 | <p> |
644 | [% |
645 | # This code illustrates how certain parts of the TT |
646 | # template will only be shown to users who have logged in |
647 | %] |
648 | [% IF Catalyst.user_exists %] |
649 | Please Note: You are already logged in as '[% Catalyst.user.username %]'. |
650 | You can <a href="[% Catalyst.uri_for('/logout') %]">logout</a> here. |
651 | [% ELSE %] |
652 | You need to log in to use this application. |
653 | [% END %] |
654 | [%# |
655 | Note that this whole block is a comment because the "#" appears |
656 | immediate after the "[%" (with no spaces in between). Although it |
657 | can be a handy way to temporarily "comment out" a whole block of |
658 | TT code, it's probably a little too subtle for use in "normal" |
659 | comments. |
660 | %] |
5edc2aae |
661 | </p> |
d442cc9f |
662 | |
663 | Although most of the code is comments, the middle few lines provide a |
664 | "you are already logged in" reminder if the user returns to the login |
665 | page after they have already authenticated. For users who have not yet |
666 | authenticated, a "You need to log in..." message is displayed (note the |
667 | use of an IF-THEN-ELSE construct in TT). |
668 | |
669 | |
670 | =head2 Try Out Authentication |
671 | |
672 | Press C<Ctrl-C> to kill the previous server instance (if it's still |
673 | running) and restart it: |
674 | |
675 | $ script/myapp_server.pl |
676 | |
677 | B<IMPORTANT NOTE>: If you happen to be using Internet Explorer, you may |
678 | need to use the command C<script/myapp_server.pl -k> to enable the |
679 | keepalive feature in the development server. Otherwise, the HTTP |
680 | redirect on successful login may not work correctly with IE (it seems to |
681 | work without -k if you are running the web browser and development |
682 | server on the same machine). If you are using browser a browser other |
683 | than IE, it should work either way. If you want to make keepalive the |
684 | default, you can edit C<script/myapp_server.pl> and change the |
685 | initialization value for C<$keepalive> to C<1>. (You will need to do |
686 | this every time you create a new Catalyst application or rebuild the |
687 | C<myapp_server.pl> script.) |
688 | |
689 | Now trying going to L<http://localhost:3000/books/list> and you should |
690 | be redirected to the login page, hitting Shift+Reload if necessary (the |
691 | "You are already logged in" message should I<not> appear -- if it does, |
692 | click the C<logout> button and try again). Note the C<***Root::auto User |
693 | not found...> debug message in the development server output. Enter |
694 | username C<test01> and password C<mypass>, and you should be taken to |
695 | the Book List page. |
696 | |
697 | Open C<root/src/books/list.tt2> and add the following lines to the |
698 | bottom: |
699 | |
700 | <p> |
701 | <a href="[% Catalyst.uri_for('/login') %]">Login</a> |
702 | <a href="[% Catalyst.uri_for('form_create') %]">Create</a> |
703 | </p> |
704 | |
705 | Reload your browser and you should now see a "Login" and "Create" links |
706 | at the bottom of the page (as mentioned earlier, you can update template |
707 | files without reloading the development server). Click the first link |
708 | to return to the login page. This time you I<should> see the "You are |
709 | already logged in" message. |
710 | |
711 | Finally, click the C<You can logout here> link on the C</login> page. |
712 | You should stay at the login page, but the message should change to "You |
713 | need to log in to use this application." |
714 | |
715 | |
716 | =head1 USING PASSWORD HASHES |
717 | |
718 | In this section we increase the security of our system by converting |
719 | from cleartext passwords to SHA-1 password hashes. |
720 | |
721 | B<Note:> This section is optional. You can skip it and the rest of the |
722 | tutorial will function normally. |
723 | |
724 | Note that even with the techniques shown in this section, the browser |
725 | still transmits the passwords in cleartext to your application. We are |
726 | just avoiding the I<storage> of cleartext passwords in the database by |
727 | using a SHA-1 hash. If you are concerned about cleartext passwords |
728 | between the browser and your application, consider using SSL/TLS, made |
729 | easy with the Catalyst plugin Catalyst::Plugin:RequireSSL. |
730 | |
731 | |
732 | =head2 Get a SHA-1 Hash for the Password |
733 | |
734 | Catalyst uses the C<Digest> module to support a variety of hashing |
735 | algorithms. Here we will use SHA-1 (SHA = Secure Hash Algorithm). |
736 | First, we should compute the SHA-1 hash for the "mypass" password we are |
737 | using. The following command-line Perl script provides a "quick and |
738 | dirty" way to do this: |
739 | |
740 | $ perl -MDigest::SHA -e 'print Digest::SHA::sha1_hex("mypass"), "\n"' |
741 | e727d1464ae12436e899a726da5b2f11d8381b26 |
742 | $ |
743 | |
744 | B<Note:> You should probably modify this code for production use to |
745 | not read the password from the command line. By having the script |
746 | prompt for the cleartext password, it avoids having the password linger |
747 | in forms such as your C<.bash_history> files (assuming you are using |
748 | BASH as your shell). An example of such a script can be found in |
749 | Appendix 3. |
750 | |
751 | |
752 | =head2 Switch to SHA-1 Password Hashes in the Database |
753 | |
754 | Next, we need to change the C<password> column of our C<users> table to |
755 | store this hash value vs. the existing cleartext password. Open |
756 | C<myapp03.sql> in your editor and enter: |
757 | |
758 | -- |
759 | -- Convert passwords to SHA-1 hashes |
760 | -- |
761 | UPDATE users SET password = 'e727d1464ae12436e899a726da5b2f11d8381b26' WHERE id = 1; |
762 | UPDATE users SET password = 'e727d1464ae12436e899a726da5b2f11d8381b26' WHERE id = 2; |
763 | UPDATE users SET password = 'e727d1464ae12436e899a726da5b2f11d8381b26' WHERE id = 3; |
764 | |
765 | Then use the following command to update the SQLite database: |
766 | |
767 | $ sqlite3 myapp.db < myapp03.sql |
768 | |
769 | B<Note:> We are using SHA-1 hashes here, but many other hashing |
770 | algorithms are supported. See C<Digest> for more information. |
771 | |
772 | |
773 | =head2 Enable SHA-1 Hash Passwords in |
774 | C<Catalyst::Plugin::Authentication::Store::DBIC> |
775 | |
776 | Edit C<myapp.yml> and update it to match (the C<password_type> and |
777 | C<password_hash_type> are new, everything else is the same): |
778 | |
779 | --- |
780 | name: MyApp |
781 | authentication: |
782 | dbic: |
783 | # Note this first definition would be the same as setting |
784 | # __PACKAGE__->config->{authentication}->{dbic}->{user_class} = 'MyAppDB::User' |
785 | # in lib/MyApp.pm (IOW, each hash key becomes a "name:" in the YAML file). |
786 | # |
787 | # This is the model object created by Catalyst::Model::DBIC from your |
788 | # schema (you created 'MyAppDB::User' but as the Catalyst startup |
789 | # debug messages show, it was loaded as 'MyApp::Model::MyAppDB::User'). |
790 | # NOTE: Omit 'MyApp::Model' here just as you would when using |
791 | # '$c->model("MyAppDB::User)' |
792 | user_class: MyAppDB::User |
793 | # This is the name of the field in your 'users' table that contains the user's name |
794 | user_field: username |
795 | # This is the name of the field in your 'users' table that contains the password |
796 | password_field: password |
797 | # Other options can go here for hashed passwords |
798 | # Enabled hashed passwords |
799 | password_type: hashed |
800 | # Use the SHA-1 hashing algorithm |
801 | password_hash_type: SHA-1 |
802 | |
803 | |
804 | =head2 Try Out the Hashed Passwords |
805 | |
806 | Press C<Ctrl-C> to kill the previous server instance (if it's still |
807 | running) and restart it: |
808 | |
809 | $ script/myapp_server.pl |
810 | |
811 | You should now be able to go to L<http://localhost:3000/books/list> and |
812 | login as before. When done, click the "Logout" link on the login page |
813 | (or point your browser at L<http://localhost:3000/logout>). |
814 | |
815 | B<Note:> If you receive the debug screen in your browser with a |
816 | C<Can't call method "stash" on an undefined value...> error message, |
817 | make sure that you are using v0.07 of |
818 | L<Catalyst::Plugin::Authorization::ACL|Catalyst::Plugin::Authorization::ACL>. |
819 | The following command can be a useful way to quickly dump the version number |
820 | of this module on your system: |
821 | |
822 | perl -MCatalyst::Plugin::Authorization::ACL -e 'print $Catalyst::Plugin::Authorization::ACL::VERSION, "\n";' |
823 | |
824 | |
825 | =head1 USING THE SESSION FOR FLASH |
826 | |
827 | As discussed in Part 3 of the tutorial, C<flash> allows you to set |
828 | variables in a way that is very similar to C<stash>, but it will |
829 | remain set across multiple requests. Once the value is read, it |
830 | is cleared (unless reset). Although C<flash> has nothing to do with |
831 | authentication, it does leverage the same session plugins. Now that |
832 | those plugins are enabled, let's go back and improve the "delete |
833 | and redirect with query parameters" code seen at the end of the |
834 | L<Basic CRUD|Catalyst::Manual::Tutorial::BasicCRUD> part of the |
835 | tutorial. |
836 | |
837 | First, open C<lib/MyApp/Controller/Books.pm> and modify C<sub delete> |
838 | to match the following: |
839 | |
840 | =head2 delete |
841 | |
842 | Delete a book |
843 | |
844 | =cut |
845 | |
846 | sub delete : Local { |
847 | # $id = primary key of book to delete |
848 | my ($self, $c, $id) = @_; |
849 | |
850 | # Search for the book and then delete it |
851 | $c->model('MyAppDB::Book')->search({id => $id})->delete_all; |
852 | |
853 | # Use 'flash' to save information across requests until it's read |
854 | $c->flash->{status_msg} = "Book deleted"; |
855 | |
856 | # Redirect the user back to the list page with status msg as an arg |
857 | $c->response->redirect($c->uri_for('/books/list')); |
858 | } |
859 | |
860 | Next, open C<root/lib/site/layout> and update the TT code to pull from |
861 | flash vs. the C<status_msg> query parameter: |
862 | |
863 | <div id="header">[% PROCESS site/header %]</div> |
864 | |
865 | <div id="content"> |
866 | <span class="message">[% status_msg || Catalyst.flash.status_msg %]</span> |
867 | <span class="error">[% error_msg %]</span> |
868 | [% content %] |
869 | </div> |
870 | |
871 | <div id="footer">[% PROCESS site/footer %]</div> |
872 | |
873 | |
874 | =head2 Try Out Flash |
875 | |
876 | Restart the development server and point your browser to |
877 | L<http://localhost:3000/books/url_create/Test/1/4> to create an extra |
878 | book. Click the "Return to list" link and delete the "Test" book you |
879 | just added. The C<flash> mechanism should retain our "Book deleted" |
880 | status message across the redirect. |
881 | |
882 | B<NOTE:> While C<flash> will save information across multiple requests, |
883 | I<it does get cleared the first time it is read>. In general, this is |
884 | exactly what you want -- the C<flash> message will get displayed on |
885 | the next screen where it's appropriate, but it won't "keep showing up" |
886 | after that first time (unless you reset it). Please refer to |
887 | L<Catalyst::Plugin::Session|Catalyst::Plugin::Session> for additional |
888 | information. |
889 | |
890 | |
891 | =head1 AUTHOR |
892 | |
893 | Kennedy Clark, C<hkclark@gmail.com> |
894 | |
895 | Please report any errors, issues or suggestions to the author. The |
896 | most recent version of the Catalyst Tutorial can be found at |
d712b826 |
897 | L<http://dev.catalyst.perl.org/repos/Catalyst/trunk/Catalyst-Manual/lib/Catalyst/Manual/Tutorial/>. |
d442cc9f |
898 | |
899 | Copyright 2006, Kennedy Clark, under Creative Commons License |
900 | (L<http://creativecommons.org/licenses/by-nc-sa/2.5/>). |