File Coverage

blib/lib/Authen/Simple/WebForm.pm
Criterion Covered Total %
statement 21 131 16.0
branch 0 102 0.0
condition 0 25 0.0
subroutine 7 12 58.3
pod 1 1 100.0
total 29 271 10.7


line stmt bran cond sub pod time code
1             package Authen::Simple::WebForm;
2              
3 2     2   105150 use warnings;
  2         5  
  2         82  
4 2     2   11 use strict;
  2         4  
  2         73  
5 2     2   13 use base 'Authen::Simple::Adapter';
  2         16  
  2         4615  
6              
7 2     2   225834 use URI;
  2         20324  
  2         79  
8 2     2   2331 use LWP;
  2         187062  
  2         5481  
9 2     2   12116 use LWP::ConnCache;
  2         5321  
  2         94  
10 2     2   22 use Params::Validate qw[];
  2         4  
  2         22436  
11              
12             =head1 NAME
13              
14             Authen::Simple::WebForm - Simple authentication against existing web based forms.
15              
16             =head1 VERSION
17              
18             Version 0.02
19              
20             =cut
21              
22             our $VERSION = '0.02';
23              
24             __PACKAGE__->options({
25             initial_url => {
26             type => Params::Validate::SCALAR,
27             default => '',
28             optional => 1,
29             },
30             # compiled regex or string
31             initial_expect => {
32             type => Params::Validate::SCALAR,
33             default => '',
34             optional => 1,
35             },
36             # compiled regex or string
37             initial_expect_cookie => {
38             type => Params::Validate::SCALAR | Params::Validate::SCALARREF,
39             default => '',
40             optional => 1,
41             },
42             check_initial_status_code => {
43             type => Params::Validate::BOOLEAN,
44             default => 1,
45             optional => 1,
46             },
47             initial_request_method => {
48             type => Params::Validate::SCALAR,
49             default => 'GET',
50             optional => 1,
51             },
52             login_url => {
53             type => Params::Validate::SCALAR,
54             default => '',
55             optional => 0,
56             },
57             # compiled regex or string
58             login_expect => {
59             type => Params::Validate::SCALAR | Params::Validate::SCALARREF,
60             default => '',
61             optional => 1,
62             },
63             # compiled regex or string
64             login_expect_cookie => {
65             type => Params::Validate::SCALAR | Params::Validate::SCALARREF,
66             default => '',
67             optional => 1,
68             },
69             check_login_status_code => {
70             type => Params::Validate::BOOLEAN,
71             default => 1,
72             optional => 1,
73             },
74             login_request_method => {
75             type => Params::Validate::SCALAR,
76             default => 'POST',
77             optional => 1,
78             },
79             # for "domain\" if needed
80             username_prefix => {
81             type => Params::Validate::SCALAR,
82             default => '',
83             optional => 1,
84             },
85             username_field => {
86             type => Params::Validate::SCALAR,
87             default => 'username',
88             optional => 1,
89             },
90             password_field => {
91             type => Params::Validate::SCALAR,
92             default => 'password',
93             optional => 1,
94             },
95             lwp_user_agent => {
96             type => Params::Validate::SCALAR,
97             default => 'Authen::Simple::WebForm/'.$VERSION,
98             optional => 1,
99             },
100             lwp_timeout => {
101             type => Params::Validate::SCALAR,
102             default => '15',
103             optional => 1,
104             },
105             lwp_protocols_allowed => {
106             type => Params::Validate::ARRAYREF,
107             default => ['http', 'https'],
108             optional => 1,
109             },
110             lwp_use_conn_cache => {
111             type => Params::Validate::BOOLEAN,
112             default => 1,
113             optional => 1,
114             },
115             lwp_requests_redirectable => {
116             type => Params::Validate::ARRAYREF,
117             default => ['GET', 'POST'],
118             optional => 1,
119             },
120             # yes, this looks like a hash, but it's not (allows keys to show up twice)
121             # [ field => value, field => value ]
122             extra_fields => {
123             type => Params::Validate::ARRAYREF,
124             default => [],
125             optional => 1,
126             },
127             # yes, this looks like a hash, but it's not (allows keys to show up twice)
128             # [ field => value, field => value ]
129             extra_headers => {
130             type => Params::Validate::ARRAYREF,
131             default => [],
132             optional => 1,
133             },
134             trace => {
135             type => Params::Validate::BOOLEAN,
136             default => 0,
137             optional => 1,
138             },
139             });
140              
141              
142             =head1 SYNOPSIS
143              
144             use Authen::Simple::WebForm;
145              
146             my $webform = Authen::Simple::WebForm->new(
147             login_url => 'http://host.company.com/login.pl',
148             login_expect => 'Successful Login',
149             );
150              
151             if ($webform->authenticate( $username, $password ) ) {
152             # successful authentication
153             }
154              
155             # or as a mod_perl Authen handler
156            
157             PerlModule Authen::Simple::Apache
158             PerlModule Authen::Simple::WebForm
159              
160             PerlSetVar AuthenSimpleWebForm_login_url "http://host.company.com/login.pl"
161             PerlSetVar AuthenSimpleWebForm_login_expect "Successful Login"
162              
163            
164             PerlAuthenHandler Authen::Simple::WebForm
165             AuthType Basic
166             AuthName "Protected Area"
167             Require valid-user
168            
169              
170             =head1 DESCRIPTION
171              
172             Authentication against a variety of login forms. This wraps up the LWP (libwww-perl)
173             calls needed to attempt a login to a site that uses an HTML form for logins. It supports
174             logins that require cookies, various form variables, special headers, and more.
175              
176             You can also subclass this to make it easier to setup, such as the
177             L module.
178              
179             There are a log of options, but they all have sane defaults. In most cases, you'll only need to use the following:
180              
181             =over
182              
183             =item login_url
184              
185             =item login_expect
186              
187             =item uesrname_field
188              
189             =item password_field
190              
191             =item extra_fields
192              
193             =back
194              
195              
196             Also helpful may be the "trace" option, which may help you to configure
197             your settings. It will print out the response code, cookies, and the resulting
198             page to STDERR.
199              
200              
201             =head1 INSTALLATION
202              
203             To install this module, run the following commands:
204              
205             perl Makefile.PL
206             make
207             make test
208             make install
209              
210              
211             =head1 METHODS
212              
213             =head2 new
214              
215             This method takes a hash of parameters. The following options are accepted:
216              
217             =over
218              
219             =item initial_url
220              
221             A URL to go to prior to logging in.
222              
223             If the login page requires you to go to some page prior to posting, use this.
224             It will accept and store any cookies returned, and use this page as the
225             referrer when submitting to the login form.
226              
227             Off by default.
228              
229              
230             =item initial_expect
231              
232             String or a compiled regex (eg. C).
233              
234             If you want to make sure the page you got is the login form, you can set
235             a string here to check for. The page content will be tested against this,
236             and authentication will fail (with a logged error) if this doesn't match.
237              
238             With this, you can make sure the server isn't returning a sorry server page, or similar.
239              
240             Off by default.
241              
242              
243             =item initial_expect_cookie
244              
245             String or a compiled regex (eg. C).
246              
247             Similar to initial_expect, but checks the cookies returned by the page.
248              
249             NOTE: this matches the cookie key, and the value must simple have some length.
250              
251             Off by default.
252              
253              
254             =item check_initial_status_code
255              
256             Boolean, set to 0 to disable.
257              
258             Set to undef to skip checking the response status code from the initial page. Otherwise, it must match HTTP::Status->is_success.
259              
260             Defaults to enabled (1).
261              
262              
263             =item initial_request_method
264              
265             This can be either "GET" or "POST".
266              
267             How the initial url will be sent to the server, either via HTTP GET request, or HTTP POST.
268              
269             Defaults to "GET".
270              
271              
272             =item login_url
273              
274             REQUIRED
275              
276             The URL to which the login credentials will be submitted.
277              
278             For example: https://host.company.com/login.pl
279              
280              
281             =item login_expect
282              
283             String or a compiled regex (eg. C).
284              
285             Set to a unique string to expect in the resulting page when the login was successful.
286              
287             Be default, this is not turned on. If you do not set this, then as long as the
288             server returns a successful status code (see HTTP::Status::is_success), then
289             the user will be authenticated. Most form based login systems return a successful
290             status code even when the login fails, so you'll probably want to set this.
291              
292             A notable exception is the use of something like L, which
293             will return a 403 Forbidden error code when authentication fails.
294              
295             Off by default.
296              
297              
298             =item login_expect_cookie
299              
300             String or a compiled regex (eg. C).
301              
302             Similar to login_expect, but checks the cookies returned by the page. If you are also using "initial_url", please be aware that an cookies set by that page will also test true here (ie. this checks our cookie jar, not the content of the page). The cookie jar is reset on every authentication request, so you don't have to worry about stale cookies from previous authentication attempts.
303              
304             NOTE: this matches the cookie key, and the value must simple have some length.
305              
306             Off by default.
307              
308              
309             =item check_login_status_code
310              
311             Boolean, set to 0 to disable.
312              
313             Set to undef to skip checking the response status code from the login page. Otherwise, it must match HTTP::Status->is_success.
314              
315             Defaults to enabled (1).
316              
317              
318             =item login_request_method
319              
320             This can be either "GET" or "POST".
321              
322             How the initial url will be sent to the server, either via HTTP GET request, or HTTP POST.
323              
324             Defaults to "POST".
325              
326              
327             =item username_prefix
328              
329             Username prefix string.
330              
331             With this, you can automatically prefix your the submitted username with
332             some string. This can can be useful if loging into a windows domain, for
333             example. In that case, you would set it to something like "MyDomain\".
334              
335             Off be default.
336              
337              
338             =item username_field
339              
340             Form field name for the username.
341              
342             Defaults to "username".
343              
344              
345             =item password_field
346              
347             Form field name for the password.
348              
349             Defaults to "password".
350              
351              
352             =item extra_fields
353              
354             Array reference of key => value pairs, representing additional form fields to submit.
355              
356             Often when submitting to a login form, other form fields are expected by the
357             login script. You may specify any number of them, and their repsective values,
358             using this option.
359              
360             Example:
361              
362             extra_fields => [
363             'language' => 'en_US',
364             'trusted' => 1
365             ],
366              
367             None submitted by default.
368              
369              
370             =item extra_headers
371              
372             Array reference of key => value pairs, representing additional HTTP headers.
373              
374             You can use this if you need to further mask your client to appear as
375             a popular web browser. Some misbehaved servers may reject your script
376             if these are not set.
377              
378             Example: (pose as netscape)
379              
380             extra_headers => [
381             'User-Agent' => 'Mozilla/4.76 [en] (Win98; U)',
382             'Accept' => 'image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*',
383             'Accept-Charset' => 'iso-8859-1,*,utf-8',
384             'Accept-Language' => 'en-US'
385             ],
386              
387             None submitted by default.
388              
389              
390             =item lwp_user_agent
391              
392             The HTTP User Agent string to submit to the server in the HTTP headers.
393              
394             Some servers may restrict access to certain user agents (ie. limit only
395             to MS Internet Explorer and Mozilla clients). You can forge a user agent
396             string with this.
397              
398             Example:
399              
400             lwp_user_agent => 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.14) Gecko/2009090216 Ubuntu/9.04 (jaunty) Firefox/3.0.14',
401              
402             Defaults to "Authen::Simple::WebForm/$VERSION".
403              
404              
405             =item lwp_timeout
406              
407             Timeout in seconds. Set to zero to disable.
408              
409             This is how long the script will wait for a response for each page fetch.
410              
411             Defaults to "15" seconds.
412              
413              
414             =item lwp_protocols_allowed
415              
416             Array reference of protocols to allow.
417              
418             This will limit what protocols will be fetched. You're already setting the
419             URLS that will be loaded, but if you allow redirects (via lwp_requests_redirectable)
420             then those may go to a different protocol. For example, you may submit to an
421             SSL protected site (https) but be redirected to an unprotected page (http).
422              
423             Defaults to ["http", "https"]
424              
425              
426             =item lwp_use_conn_cache
427              
428             Boolean, set to 0 to disable.
429              
430             Whether to use connection caching. See L for details, as well as the "conn_cache" option to L.
431              
432             Defaults to enabled (1).
433              
434              
435             =item lwp_requests_redirectable
436              
437             Array reference of request names for which we will automatically redirect.
438              
439             See L option requests_redirectable for details. This affects the responses
440             we get from the server. For example, if you are posting form data
441             (login_request_method == POST), and the successful login page returns a redirect
442             to some other page, "POST" would be needed here. We allow GET and POST by
443             default, so you only need to set this is if do not want this behavior.
444              
445             Defaults to ["GET", "POST"]
446              
447              
448             =item trace
449              
450             Boolean, set to 1 to enable.
451              
452             If set to true, the data we recieve will be dumped out to STDERR.
453             This can be useful while you're trying to determine what fields need
454             passed, and what might be going wrong. When running your test scripts,
455             assuming your are starting from a test script, simply dump STDERR
456             to a file:
457              
458             perl test.pl 2>somefile.txt
459              
460             Defaults to disabled (0).
461              
462              
463             =back
464              
465              
466             =head2 log
467              
468             Any object that supports C, C, C and C.
469              
470             log => Log::Log4perl->get_logger('Authen::Simple::WebForm')
471              
472             See L for a simple logging class you may use,
473             or L for more advanced logging.
474              
475              
476             =head2 authenticate( $username, $password )
477              
478             Returns true on success and false on failure.
479              
480             =head2 check($user, $pass)
481              
482             Internal method used to do the actual authentication check.
483              
484             =cut
485              
486              
487             sub check
488             {
489 0     0 1   my ($self, $username, $password) = @_;
490              
491             # prepend prefix. If none set, or blank, this will just be $username
492 0           my $full_username = join('', ($self->username_prefix, $username));
493              
494             # prep any additional headers we might need
495 0           my @headers;
496 0           my $extra_headers = $self->extra_headers;
497 0 0 0       if (ref($extra_headers) eq 'ARRAY' && @$extra_headers)
498             {
499 0 0         if ((@$extra_headers % 2) == 0)
500             {
501 0           push(@headers, @$extra_headers);
502             } else {
503 0 0         $self->log->error("Invalid extra_headers option.") if $self->log;
504 0           return 0;
505             }
506             }
507              
508             # determine request method
509 0   0       my $initial_req_method = uc($self->initial_request_method || 'GET');
510 0 0         unless ($initial_req_method =~ /^(GET|POST)$/i) {
511 0 0         $self->log->error("Invalid initial_request_method.") if $self->log;
512 0           return 0;
513             }
514 0   0       my $login_req_method = uc($self->login_request_method || 'GET');
515 0 0         unless ($login_req_method =~ /^(GET|POST)$/i) {
516 0 0         $self->log->error("Invalid login_request_method.") if $self->log;
517 0           return 0;
518             }
519              
520             # initialize the user agent
521 0 0         my $ua = LWP::UserAgent->new() or die "Unable to init LWP::UserAgent : $@";
522             # keep in memory cookie jar
523 0           $ua->cookie_jar({});
524 0 0         $ua->agent($self->lwp_user_agent) if $self->lwp_user_agent;
525 0 0         $ua->timeout( $self->lwp_timeout ) if $self->lwp_timeout;
526 0 0         $ua->conn_cache(LWP::ConnCache->new()) if $self->lwp_use_conn_cache;
527              
528 0           my $req_redirectable = $self->lwp_requests_redirectable;
529 0 0 0       if (ref($req_redirectable) eq 'ARRAY' && @$req_redirectable) {
530 0           push @{$ua->requests_redirectable}, @$req_redirectable;
  0            
531             }
532              
533             # get an inital page?
534 0 0         if ($self->initial_url)
535             {
536 0 0         my $res = ($initial_req_method eq 'GET') ? $ua->get($self->initial_url, @headers):
537             $ua->post($self->initial_url, @headers);
538 0 0         if ($self->trace)
539             {
540 0           print STDERR ("-"x80)."\n";
541 0           print STDERR "TRACE: initial response, response code [".$res->code."]\n";
542 0           print STDERR "TRACE: initial response, cookies [".$ua->cookie_jar->as_string()."]\n";
543 0           print STDERR $res->decoded_content;
544 0           print STDERR "\n\n\n";
545 0           print STDERR ("-"x80)."\n";
546             }
547             # make sure status code is ok?
548 0 0         if ($self->check_initial_status_code)
549             {
550 0 0         unless ($res->is_success)
551             {
552 0 0         $self->log->error("Can't get ".$self->initial_url." -- ".$res->status_line)
553             if $self->log;
554 0           return 0;
555             }
556             }
557              
558             # do we care to check the content?
559 0 0         if ($self->initial_expect)
560             {
561 0           my $expect = $self->initial_expect;
562 0 0         unless (ref($expect) eq 'Regexp') {
563 0           $expect = qr/\Q$expect\E/;
564             }
565 0 0         unless ($res->decoded_content =~ /$expect/)
566             {
567 0 0         $self->log->error("Initial url didn't return expected results.") if $self->log;
568 0           return 0;
569             }
570             }
571              
572             # do we care to check for a cookie
573 0 0         if ($self->initial_expect_cookie)
574             {
575 0           my $expect = $self->initial_expect_cookie;
576              
577 0           my $found = 0;
578 0           my $search; # cookie_jar search callback
579              
580 0 0         if (ref($expect) eq 'Regexp')
581             {
582 0 0 0 0     $search = sub { $found++ if $_[1] =~ /$expect/ && length($_[2]); };
  0            
583             } else {
584 0 0 0 0     $search = sub { $found++ if $_[1] eq $expect && length($_[2]); };
  0            
585             }
586              
587             # search the cookie jar
588 0           $ua->cookie_jar->scan($search);
589 0 0         unless ($found)
590             {
591 0 0         $self->log->debug("Failed to authenticate user '$full_username'. Reason: Initial Cookie $expect was not found.")
592             if $self->log;
593 0           return 0;
594             }
595             }
596             }
597              
598              
599             # build data to post
600 0           my @data = (
601             $self->username_field => $full_username,
602             $self->password_field => $password
603             );
604             # add an extra fields to submit
605 0           my $extra_fields = $self->extra_fields;
606 0 0 0       if (ref($extra_fields) eq 'ARRAY' && @$extra_fields)
607             {
608 0 0         if ((@$extra_fields % 2) == 0)
609             {
610 0           push(@data, @$extra_fields);
611             } else {
612 0 0         $self->log->error("Invalid extra_fields option.") if $self->log;
613 0           return 0;
614             }
615             }
616              
617             # attempt to login
618 0           my $res;
619 0 0         if ($login_req_method eq 'GET')
620             {
621 0           my $url = URI->new($self->login_url);
622 0 0         unless ($url) {
623 0 0         $self->log->error("Unable to parse login_url. $@") if $self->log;
624 0           return 0;
625             }
626 0           $url->query_form( \@data );
627 0           $res = $ua->get($url, @headers);
628             } else { # POST
629 0           $res = $ua->post($self->login_url, \@data, @headers);
630             }
631 0 0         if ($self->trace)
632             {
633 0           print STDERR ("-"x80)."\n";
634 0           print STDERR "TRACE: initial response, response code [".$res->code."]\n";
635 0           print STDERR "TRACE: initial response, cookies [".$ua->cookie_jar->as_string()."]\n";
636 0           print STDERR $res->decoded_content;
637 0           print STDERR "\n\n\n";
638 0           print STDERR ("-"x80)."\n";
639             }
640              
641             # make sure status code is ok?
642 0 0         if ($self->check_login_status_code)
643             {
644 0 0         unless ($res->is_success)
645             {
646 0 0         if ($res->is_redirect)
647             {
648 0 0         $self->log->debug("Failed to authenticate user '$full_username'. Reason: Login page returned redirect status code '".$res->code."'. You may wish to enable lwp_requests_redirectable -- ".$res->status_line)
649             if $self->log;
650             } else {
651 0 0         $self->log->debug("Failed to authenticate user '$full_username'. Reason: Login page returned invalid status code '".$res->code."' -- ".$res->status_line)
652             if $self->log;
653             }
654 0           return 0;
655             }
656             }
657              
658             # do we care to check the content?
659 0 0         if ($self->login_expect)
660             {
661 0           my $expect = $self->login_expect;
662 0 0         unless (ref($expect) eq 'Regexp') {
663 0           $expect = qr/\Q$expect\E/;
664             }
665 0 0         unless ($res->decoded_content =~ /$expect/)
666             {
667 0 0         $self->log->debug("Failed to authenticate user '$full_username'. Reason: Login page response did not match expected value.")
668             if $self->log;
669 0           return 0;
670             }
671             }
672              
673             # do we care to check for a cookie
674 0 0         if ($self->login_expect_cookie)
675             {
676 0           my $expect = $self->login_expect_cookie;
677              
678 0           my $found = 0;
679 0           my $search; # cookie_jar search callback
680              
681 0 0         if (ref($expect) eq 'Regexp')
682             {
683 0 0 0 0     $search = sub { $found++ if $_[1] =~ /$expect/ && length($_[2]); };
  0            
684             } else {
685 0 0 0 0     $search = sub { $found++ if $_[1] eq $expect && length($_[2]); };
  0            
686             }
687              
688             # search the cookie jar
689 0           $ua->cookie_jar->scan($search);
690 0 0         unless ($found)
691             {
692 0 0         $self->log->debug("Failed to authenticate user '$full_username'. Reason: Login Cookie $expect was not found.")
693             if $self->log;
694 0           return 0;
695             }
696             }
697              
698 0 0         $self->log->debug("Successfully authenticated user '$full_username'.") if $self->log;
699 0           return 1;
700             }
701              
702             =head1 TODO
703              
704             Add lwp_cookie_jar option(s) so that it may use a file.
705              
706             Add a debug mode. It's often difficult to determine what content is being returned, and what to look for. The debug mode should print each step out to STDERR, and include the relevant response information from the page.
707              
708             Write tests using HTTP::Daemon as a local webserver. See LWP test t/local/http.t and t/local/chunked.t for example.
709              
710             =head1 AUTHOR
711              
712             Joshua I. Miller, C<< >>
713              
714             =head1 BUGS
715              
716             Please report any bugs or feature requests to C, or through
717             the web interface at L. I will be notified, and then you'll
718             automatically be notified of progress on your bug as I make changes.
719              
720              
721              
722              
723             =head1 SUPPORT
724              
725             You can find documentation for this module with the perldoc command.
726              
727             perldoc Authen::Simple::WebForm
728              
729              
730             You can also look for information at:
731              
732             =over 4
733              
734             =item * RT: CPAN's request tracker
735              
736             L
737              
738             =item * AnnoCPAN: Annotated CPAN documentation
739              
740             L
741              
742             =item * CPAN Ratings
743              
744             L
745              
746             =item * Search CPAN
747              
748             L
749              
750             =back
751              
752              
753             =head1 SEE ALSO
754              
755             L
756              
757             L
758              
759             examples/ex1.pl (an example that can be used to auth against freshmeat.net).
760              
761             L
762              
763             =head1 COPYRIGHT & LICENSE
764              
765             Copyright 2009 Joshua I. Miller, all rights reserved.
766              
767             This program is free software; you can redistribute it and/or modify it
768             under the same terms as Perl itself.
769              
770              
771             =cut
772              
773             1; # End of Authen::Simple::WebForm