File Coverage

blib/lib/CAM/UserApp.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package CAM::UserApp;
2              
3             =head1 NAME
4              
5             CAM::UserApp - Extension of CAM::App to support web login
6              
7             =head1 LICENSE
8              
9             Copyright 2005 Clotho Advanced Media, Inc.,
10              
11             This library is free software; you can redistribute it and/or modify it
12             under the same terms as Perl itself.
13              
14             =head1 DESCRIPTION
15              
16             CAM::UserApp provides generic session-based login capabilities. It
17             supports login, state maintenance and password changing in a framework
18             that supports either SOAP or cookie-based HTML, among other
19             possibilities.
20              
21             CAM::UserApp is not complete by itself. Some of its methods must be
22             implemented by a subclass. In particular, retrieveUser() must be
23             supplied. In an HTML or other human-interaction environment, the
24             offerLogin() and offerChangePassword() methods should be implemented.
25             Others are optional, and are described below.
26              
27             =head1 SYNOPSIS
28              
29             A nearly-complete example subclass:
30              
31             package MyApp;
32             use CAM::UserApp;
33             our @ISA=qw(CAM::UserApp);
34            
35             sub retrieveUser {
36             my ($self, $user, $pass) = @_;
37             # (do some SQL lookup perhaps)
38             my $user = Some::Pkg->new($user, $pass);
39             return $user;
40             }
41            
42             sub offerLogin {
43             my ($self, %args) = @_;
44             print $self->header();
45             $self->getTemplate("login.tmpl",
46             error=>$args{error},
47             passthru=>$args{passthru})
48             ->print();
49             }
50            
51             sub offerChangePassword {
52             my ($self, %args) = @_;
53             print $self->header();
54             $self->getTemplate("changePass.tmpl", error=>$args{error})
55             ->print();
56             }
57             1;
58              
59             A CGI script that uses CAM::UserApp through that subclass:
60              
61             #!perl
62             use Config;
63             use MyApp;
64             my $app = MyApp->new(config => Config->new());
65             $app->authenticate() or exit(0);
66             my $user = $app->getUser();
67             if ($app->getCGI()->param('logout')) {
68             $app->deauthenticate();
69             exit(0);
70             } elsif ($app->getCGI()->param('changepass')) {
71             $app->changePassword($user->getUsername()) or exit(0);
72             }
73            
74             print $app->header();
75             print "Welcome " . $user->getName() . "!\n";
76             ...
77              
78             Note that the class for $user is not defined here. You must build
79             that yourself. The new() and getName() and getUsername() methods
80             shown above are for example only.
81              
82             Note that authentication is performed separately from initialization
83             for the sake of applications where login is optional. If your
84             application requires login, we recommend that your CAM::UserApp
85             subclass include methods like the following in addition to those shown
86             in the subclass above.
87              
88             use Config;
89             sub new {
90             my $pkg = shift;
91             return $pkg->SUPER::new(config => Config->new(),
92             needPassword => 1, @_);
93             }
94             sub init {
95             my $self = shift;
96             $self->SUPER::init() or return undef;
97             $self->authenticate() or exit(0);
98             if ($app->getCGI()->param('logout')) {
99             $app->deauthenticate();
100             exit(0);
101             } elsif ($app->getCGI()->param('changepass')) {
102             $app->changePassword($app->getUser()->getUsername()) or exit(0);
103             }
104             return $self;
105             }
106              
107             Thus your CGI could look as simple as:
108              
109             #!perl
110             use MyApp;
111             my $app = MyApp->new();
112             print $app->header();
113             print "Welcome " . $app->getUser()->getName() . "!\n";
114             ...
115              
116             while still including full login support.
117              
118             =cut
119              
120             #--------------------------------#
121              
122             require 5.005_62;
123 1     1   28521 use strict;
  1         3  
  1         47  
124 1     1   5 use warnings;
  1         2  
  1         35  
125 1     1   453 use CAM::App;
  0            
  0            
126              
127             our @ISA = qw(CAM::App);
128             our $VERSION = '1.01';
129              
130             #--------------------------------#
131              
132             =head1 CLASS METHODS
133              
134             =over 4
135              
136             =cut
137              
138             #--------------------------------#
139              
140             =item usernameCGIKey
141              
142             =item passwordCGIKey
143              
144             =item password1CGIKey
145              
146             =item password2CGIKey
147              
148             Simple accessors that return the CGI parameter names used to input
149             login details. These are provided so they can be overrided by
150             subclasses. The defaults are:
151              
152             usernameCGIKey "username"
153             passwordCGIKey "password"
154             password1CGIKey "password1"
155             password2CGIKey "password2"
156              
157             username and password are used for input to authenticate() while
158             password1, password2 and (optionally) password are used for
159             changePassword().
160              
161             =cut
162              
163             sub usernameCGIKey { "username" }
164             sub passwordCGIKey { "password" }
165             sub password1CGIKey { "password1" }
166             sub password2CGIKey { "password2" }
167              
168             #--------------------------------#
169              
170             =item new [argument list...]
171              
172             Overrides the superclass constructor to add boolean settings. These
173             settings are used in the authenticate() and changePassword() methods
174             below. Both of those methods allow callers to override this value
175             directly if desired.
176              
177             All other arguments are passed on the to the superclass constructor.
178              
179             interactive => boolean (default: true)
180              
181             If true, login or change password failures yield calls to offerLogin()
182             or offerChangePassword(), respectively. If false, these calls are
183             skipped. The equivalent effect to interactive = false can be achieved
184             by using a no-op offerLogin() or offerChangePassword(), which are in
185             fact the default behaviors for those functions.
186              
187             useCGI => boolean (default: true)
188              
189             Specifies whether the CGI parameters should be consulted for username
190             and password values, if any. CGI values override session values.
191              
192             useSession => boolean (default: true)
193              
194             Specifies whether the session record should be consulted for username
195             and password values, if any.
196              
197             needPassword => boolean (default: false)
198              
199             Specifies whether the user has to enter their old password before a
200             new one can be set in changePassword(). While it defaults to the lax
201             'false' state, I recommend you set this to true for interactive
202             applications!
203              
204             =cut
205              
206             sub new
207             {
208             my $pkg = shift;
209             my %params = (@_);
210              
211             my $self = $pkg->SUPER::new(%params);
212             $self->{useCGI} = exists $params{useCGI} ? $params{useCGI} : 1;
213             $self->{useSession} = exists $params{useSession} ? $params{useSession} : 1;
214             $self->{needPassword} = exists $params{needPassword} ? $params{needPassword} : 0;
215             $self->{interactive} = exists $params{interactive} ? $params{interactive} : 1;
216             return $self;
217             }
218             #--------------------------------#
219              
220             =back
221              
222             =head1 INSTANCE METHODS
223              
224             =over 4
225              
226             =cut
227              
228             #--------------------------------#
229              
230             =item retrieveUser USERNAME, PASSWORD
231              
232             This method MUST be overridden by a subclass, or authenticate() will
233             never succeed. It should return an object for the specified username
234             and password, or undef if there is no such user. The object can be of
235             any class as long as: 1) it is blessed, 2) it has a
236             recordPassword($password) method that can be called from our
237             changePassword() function. Note that this method MAY be called
238             multiple times during a session, so don't do hit counting in here.
239              
240             =cut
241              
242             sub retrieveUser
243             {
244             my $self = shift;
245             my $username = shift;
246             my $password = shift;
247              
248             my $user;
249            
250             # Do something here:
251             # Get a user object (likely a database record)
252             # Make a record of the login?
253             # Tweak the user object?
254             # Return undef if retrieval fails
255              
256             # The returned object should have a recordPassword() method
257              
258             return $user;
259             }
260             #--------------------------------#
261              
262             =item authenticate
263              
264             Validate a login. Returns a boolean indicating success. Most
265             applications should abort upon receiving a false response. If the
266             login fails, or if username/password parameters are missing, the
267             offerLogin() method is called before false is returned. For this
268             method to succeed, the retrieveUser() method MUST be implemented by a
269             subclass. After success, the getUser() method will return the cached
270             result from retrieveUser().
271              
272             Optional arguments:
273              
274             username => string (default: undef)
275             password => string (default: undef)
276              
277             Values to use for login. Overrides CGI and session values.
278              
279             useCGI => boolean
280             useSession => boolean
281             interactive => boolean
282              
283             These values, if not passed as arguments, are inherited from the
284             CAM::UserApp instance.
285              
286             =cut
287              
288             sub authenticate
289             {
290             my $self = shift;
291             my %args = (@_);
292              
293             my $session;
294             my $cgi;
295             my $passthru = "";
296              
297             foreach my $key ("useCGI", "useSession", "interactive")
298             {
299             $args{$key} = $self->{$key} unless (exists $args{$key});
300             }
301              
302             if ($args{useCGI})
303             {
304             $cgi = $self->getCGI();
305             $args{username} ||= $cgi->param($self->usernameCGIKey());
306             $args{password} ||= $cgi->param($self->passwordCGIKey());
307             if ($args{interactive})
308             {
309             foreach my $key ($cgi->param)
310             {
311             next if ($key eq $self->usernameCGIKey() ||
312             $key eq $self->passwordCGIKey());
313             my $hkey = $cgi->escapeHTML($key);
314             foreach my $value ($cgi->param($key))
315             {
316             $value = "" if (!defined $value);
317             my $hvalue = $cgi->escapeHTML($value);
318             $passthru .= qq[];
319             }
320             }
321             }
322             }
323             if ($args{useSession})
324             {
325             $session = $self->getSession();
326             unless ($session->isNewSession())
327             {
328             $args{username} ||= $session->get("username");
329             $args{password} ||= $session->get("password");
330             }
331             }
332              
333             unless ($args{username} || $args{password})
334             {
335             if ($args{interactive})
336             {
337             $self->offerLogin(passthru => $passthru);
338             }
339             return undef;
340             }
341              
342             unless ($args{username})
343             {
344             if ($args{interactive})
345             {
346             $self->offerLogin(error => "Please enter your username",
347             passthru => $passthru);
348             }
349             return undef;
350             }
351              
352             unless ($args{password})
353             {
354             if ($args{interactive})
355             {
356             $self->offerLogin(error => "Please enter your password",
357             passthru => $passthru);
358             }
359             return undef;
360             }
361              
362             my $user = $self->retrieveUser($args{username}, $args{password});
363             unless ($user)
364             {
365             if ($args{interactive})
366             {
367             $self->offerLogin(error => "Login failed",
368             passthru => $passthru);
369             }
370             return undef;
371             }
372              
373             $self->{User} = $user;
374              
375             if ($session)
376             {
377             $session->set(username => $args{username},
378             password => $args{password});
379             }
380              
381             return $self;
382             }
383              
384             #--------------------------------#
385              
386             =item getUser
387              
388             Returns the User object obtained from authenticate(). If
389             authentication fails, or is never attempted, this method will return
390             undef.
391              
392             =cut
393              
394             sub getUser
395             {
396             my $self = shift;
397             return $self->{User};
398             }
399             #--------------------------------#
400              
401             =item deauthenticate
402              
403             Logs out an authenticated user. If a session is present, it is wiped.
404             After this, the getUser() will return undef. This method returns
405             self.
406              
407             Optional arguments:
408              
409             useSession => boolean (default: true)
410              
411             Specifies whether the session record should be cleared.
412              
413             interactive => boolean (default: true)
414              
415             If true, the offerLogin() method is called at the end of
416             deauthentication.
417              
418             =cut
419              
420             sub deauthenticate
421             {
422             my $self = shift;
423             my %args = (@_);
424              
425             $args{useSession} = 1 unless (exists $args{useSession});
426             $args{interactive} = 1 unless (exists $args{interactive});
427              
428             if ($args{useSession})
429             {
430             my $session = $self->getSession();
431             if ($session)
432             {
433             $session->clear();
434             }
435             }
436             delete $self->{User};
437             if ($args{interactive})
438             {
439             $self->offerLogin();
440             }
441             return $self;
442             }
443              
444             #--------------------------------#
445              
446             =item changePassword
447              
448             Change the users password. The user must already be authenticated.
449             If the new password is missing or invalid or if the retyped value does
450             not match, this calls offerChangePassword and returns undef. If the
451             needPassword flag is set, the old password must be entered. It will
452             be validated via the retrieveUser() method.
453              
454             Optional arguments:
455              
456             username => string (default: undef)
457             password => string (default: undef)
458              
459             Values to use for authentication if needPassword is true. Overrides
460             CGI values.
461              
462             password1 => string (default: undef)
463             password2 => string (default: undef)
464              
465             Values to use for the new password and password verification.
466             Overrides CGI values.
467              
468             interactive => boolean
469             useCGI => boolean
470             useSession => boolean
471             needPassword => boolean
472              
473             These values, if not passed as arguments, are inherited from the
474             CAM::UserApp instance.
475              
476             =cut
477              
478             sub changePassword
479             {
480             my $self = shift;
481             my %args = (@_);
482              
483             foreach my $key ("useCGI", "useSession", "interactive", "needPassword")
484             {
485             $args{$key} = $self->{$key} unless (exists $args{$key});
486             }
487              
488             my $user = $self->getUser();
489             my $cgi;
490              
491             if ($args{useCGI})
492             {
493             $cgi = $self->getCGI();
494             $args{password} ||= $cgi->param($self->passwordCGIKey());
495             $args{password1} ||= $cgi->param($self->password1CGIKey());
496             $args{password2} ||= $cgi->param($self->password2CGIKey());
497             }
498              
499             unless ($args{password1} || $args{password2})
500             {
501             $self->offerChangePassword();
502             return undef;
503             }
504              
505             unless ($args{password1} && $args{password2})
506             {
507             $self->offerChangePassword(error => "Please fill in all password fields");
508             return undef;
509             }
510              
511             if ($args{needPassword})
512             {
513             unless ($args{password})
514             {
515             $self->offerChangePassword(error => "Please fill in all password fields");
516             return undef;
517             }
518             unless ($args{username})
519             {
520             $self->offerChangePassword(error => "Error: no username found");
521             return undef;
522             }
523             unless ($self->retrieveUser($args{username}, $args{password}))
524             {
525             $self->offerChangePassword(error => "Incorrect password");
526             return undef;
527             }
528             }
529              
530             if ($args{password1} ne $args{password2})
531             {
532             $self->offerChangePassword(error => "The passwords you have entered do not match");
533             return undef;
534             }
535              
536             my $password = $args{password1}; # shorthand
537             unless ($self->validateNewPassword($password))
538             {
539             $self->offerChangePassword(error => "Invalid password, please try again");
540             return undef;
541             }
542              
543             unless ($user->can("recordPassword") && $user->recordPassword($password))
544             {
545             $self->offerChangePassword(error => "Unable to record your new password");
546             return undef;
547             }
548              
549             if ($args{useSession})
550             {
551             # Note! We DO NOT want to create a new session here, so we don't
552             # use the getSession() method. If there is no session, well, so
553             # be it.
554              
555             my $session = $self->{session};
556             if ($session)
557             {
558             $session->set(password => $password);
559             }
560             }
561              
562             return $self;
563             }
564             #--------------------------------#
565              
566             =item offerLogin
567              
568             Display an interactive login. By default, this method is a no-op.
569             Interactive subclasses should override this method. The return value
570             of this method is not used. A sample implementation is presented in
571             the Synopsis above.
572              
573             Optional arguments:
574              
575             error => string
576              
577             Indicates a reason why this method has been called, like "Login
578             failure". On a fresh login, this argument is absent.
579              
580             passthru => string
581              
582             An accumulation of CGI parameters passed to this program, in the form
583             of '' for each parameter.
584             Implementations are welcome to ignore this, but they should pass it
585             via an HTML form if they want to make the login be 'transparent',
586             i.e., if the program should go back to whatever it was doing before
587             when login is successful login.
588              
589             Here's an example HTML template file for use with the offerLogin()
590             implementation in the Synopsis above, using these parameters:
591              
592             Login
593            
594             ??error?? ::error::
??error??
595             Username:
596             Password:
597            
598             ::passthru::
599            
600              
601             =cut
602              
603             sub offerLogin
604             {
605             my $self = shift;
606             my %args = (@_);
607              
608             # do nothing unless subclass overrides
609             }
610             #--------------------------------#
611              
612             =item offerChangePassword
613              
614             Display an interactive password change screen. By default, this
615             method is a no-op, so interactive subclasses should override this
616             method. The return value of this method is not used. A sample
617             implementation is presented in the Synopsis above.
618              
619             Optional arguments:
620              
621             error => string
622              
623             Indicates a reason why this method has been called, like "Passwords do
624             not match". On first hit, this argument is absent.
625              
626             Here's an example HTML template file for use with the
627             offerChangePassword() implementation in the Synopsis above, using
628             this parameters:
629              
630             Change Password
631            
632             ??error?? ::error::
??error??
633             Old Password:
634             New Password:
635             Retype Password:
636            
637            
638              
639             =cut
640              
641             sub offerChangePassword
642             {
643             my $self = shift;
644             my %args = (@_);
645              
646             # do nothing unless subclass overrides
647             }
648             #--------------------------------#
649              
650             =item validateNewPassword PASSWORD
651              
652             Performs simple checks on the validity of a new password. This
653             implementation only checks that the password is defined and not the
654             null string. Subclasses may implement more rigorous checks.
655              
656             =cut
657              
658             sub validateNewPassword
659             {
660             my $self = shift;
661             my $password = shift;
662              
663             return undef unless (defined $password && $password ne "");
664              
665             return $self;
666             }
667             #--------------------------------#
668              
669             1;
670             __END__