f39602fa8f968b492d7645950fd16a2c376b1b21
[catagits/Catalyst-Plugin-Authentication.git] / lib / Catalyst / Authentication / Credential / Remote.pm
1 package Catalyst::Authentication::Credential::Remote;
2
3 use strict;
4 use warnings;
5
6 use base 'Class::Accessor::Fast';
7
8 BEGIN {
9     __PACKAGE__->mk_accessors(qw/allow_re deny_re cutname_re source realm/);
10 }
11
12 sub new {
13     my ( $class, $config, $app, $realm ) = @_;
14
15     my $self = { };
16     bless $self, $class;
17     
18     # we are gonna compile regular expresions defined in config parameters
19     # and explicitly throw an exception saying what parameter was invalid 
20     if (defined($config->{allow_regexp}) && ($config->{allow_regexp} ne "")) { 
21         eval { $self->allow_re( qr/$config->{allow_regexp}/ ) };
22         Catalyst::Exception->throw( "Invalid regular expression in ".
23         "'allow_regexp' configuration parameter") if $@;
24     }
25     if (defined($config->{deny_regexp}) && ($config->{deny_regexp} ne "")) { 
26         eval { $self->deny_re( qr/$config->{deny_regexp}/ ) };     
27         Catalyst::Exception->throw( "Invalid regular expression in ".
28              "'deny_regexp' configuration parameter") if $@;
29     }
30     if (defined($config->{cutname_regexp}) && ($config->{cutname_regexp} ne "")) { 
31         eval { $self->cutname_re( qr/$config->{cutname_regexp}/ ) };
32         Catalyst::Exception->throw( "Invalid regular expression in ".
33              "'cutname_regexp' configuration parameter") if $@;
34     }
35     $self->source($config->{source} || 'REMOTE_USER');
36     $self->realm($realm);
37     return $self;
38 }
39
40 sub authenticate {
41     my ( $self, $c, $realm, $authinfo ) = @_;
42
43     my $remuser;
44     if ($self->source eq "REMOTE_USER") {    
45         # compatibility hack:
46         if (defined($c->engine->env)) {
47             # BEWARE: $c->engine->env was broken prior 5.80005
48             $remuser = $c->engine->env->{REMOTE_USER};
49         }
50         elsif ($c->req->can('remote_user')) {
51             # $c->req->remote_users was introduced in 5.80005; if not evailable we are
52             # gonna use $c->req->user that is deprecated but more or less works as well 
53             $remuser = $c->req->remote_user;
54         }
55         elsif ($c->req->can('user')) {
56             # maybe show warning that we are gonna use DEPRECATED $req->user            
57             if (ref($c->req->user)) {
58                 # I do not know exactly when this happens but it happens
59                 Catalyst::Exception->throw( "Cannot get remote user from ".
60                 "\$c->req->user as it seems to be a reference not a string" );
61             }
62             else {
63                 $remuser = $c->req->user;
64             }
65         }
66     }    
67     elsif ($self->source =~ /^(SSL_CLIENT_.*|CERT_*|AUTH_USER)$/) {
68         # if you are using 'exotic' webserver or if the user is 
69         # authenticated e.g via SSL certificate his name could be avaliable
70         # in different variables
71         # BEWARE: $c->engine->env was broken prior 5.80005
72         my $nam=$self->source;
73         if ($c->engine->can('env')) {
74             $remuser = $c->engine->env->{$nam};
75         }
76         else {
77             # this happens on Catalyst 5.80004 and before (when using FastCGI)
78             Catalyst::Exception->throw( "Cannot handle parameter 'source=$nam'".
79                 " as runnig Catalyst engine has broken \$c->engine->env" );
80         }
81     }
82     else {
83         Catalyst::Exception->throw( "Invalid value of 'source' parameter");
84     }
85     return unless defined($remuser);
86     return if ($remuser eq "");
87
88     # $authinfo hash can contain item username (it is optional) - if it is so
89     # this username has to be equal to remote_user 
90     my $authuser = $authinfo->{username};             
91     return if (defined($authuser) && ($authuser ne $remuser));
92
93     # handle deny / allow checks 
94     return if (defined($self->deny_re)  && ($remuser =~ $self->deny_re));
95     return if (defined($self->allow_re) && ($remuser !~ $self->allow_re));
96
97     # if param cutname_regexp is specified we try to cut the final usename as a
98     # substring from remote_user 
99     my $usr = $remuser;
100     if (defined($self->cutname_re)) {
101         if (($remuser =~ $self->cutname_re) && ($1 ne "")) {
102             $usr = $1;
103         }
104     }
105     
106     $authinfo->{id} = $authinfo->{username} = $usr; 
107     $authinfo->{remote_user} = $remuser; # just to keep the original value
108     my $user_obj = $realm->find_user( $authinfo, $c );
109     return ref($user_obj) ? $user_obj : undef;
110 }
111
112 1;
113
114 __END__
115
116 =pod
117
118 =head1 NAME
119
120 Catalyst::Authentication::Credential::Remote - Let the webserver (e.g. Apache)
121 authenticate Catalyst application users
122
123 =head1 SYNOPSIS
124
125     # in your MyApp.pm
126     __PACKAGE__->config(
127
128         'Plugin::Authentication' => {
129             default_realm => 'remoterealm',
130             realms => {
131                 remoterealm => {
132                     credential => {
133                         class        => 'Remote',
134                         allow_regexp => '^(user.*|admin|guest)$',
135                         deny_regexp  => 'test',
136                     },
137                     store => {
138                         class => 'Null',
139                         # if you want to have some additional user attributes
140                         # like user roles, user full name etc. you can specify
141                         # here the store where you keep this data
142                     }
143                 },
144             },
145         },
146         
147     );
148     
149     # in your Controller/Root.pm you can implement "auto-login" in this way
150     sub begin : Private {
151         my ( $self, $c ) = @_;        
152         unless ($c->user_exists) {
153             # authenticate() for this module does not need any user info
154             # as the username is taken from $c->req->remote_user and
155             # password is not needed     
156             unless ($c->authenticate( {} )) {
157               # return 403 forbidden or kick out the user in other way
158             };
159         }   
160     }
161
162     # or you can implement in any controller an ordinary login action like this
163     sub login : Global {
164         my ( $self, $c ) = @_;
165         $c->authenticate( {} );
166     }
167
168 =head1 DESCRIPTION
169
170 This module allows you to authenticate the users of your Catalyst application
171 on underlaying webserver. The complete list of authentication method available 
172 via this module depends just on what your webserver (e.g. Apache, IIS, Lighttpd)
173 is able to handle.
174
175 Besides the common methods like HTTP Basic and Digest authentication you can
176 also use sophisticated ones like so called "integrated authentication" via
177 NTLM or Kerberos (popular in corporate intranet applications running in Windows
178 Active Directory enviroment) or even the SSL authentication when users 
179 authenticate themself using their client SSL certificates.   
180
181 The main idea of this module is based on a fact that webserver passes the name
182 of authenticated user into Catalyst application as REMOTE_USER variable (or in 
183 case of SSL client authentication in other variables like SSL_CLIENT_S_DN on
184 Apache + mod_ssl) - from this point referenced as WEBUSER. 
185 This module simply takes this value - perfoms some optional checks (see
186 below) - and if everything is OK the WEBUSER is declared as authenticated on 
187 Catalyst level. In fact this module does not perform any check for password or 
188 other credential; it simply believes the webserver that user was properly 
189 authenticated.
190
191 =head1 CONFIG
192
193 =head2 class
194
195 This config item is B<REQUIRED>. 
196
197 B<class> is part of the core L<Catalyst::Plugin::Authentication> module, it 
198 contains the class name of the store to be used.
199
200 The classname used for Credential. This is part of L<Catalyst::Plugin::Authentication>
201 and is the method by which Catalyst::Authentication::Credential::Remote is
202 loaded as the credential validator. For this module to be used, this must be set
203 to 'Remote'.
204
205 =head2 source
206
207 This config item is B<OPTIONAL> - default is REMOTE_USER.
208
209 B<source> contains a name of a variable passed from webserver that contains the 
210 user identification.
211
212 Supported values: REMOTE_USER, SSL_CLIENT_*, CERT_*, AUTH_USER
213
214 B<BEWARE:> Support for using different variables than REMOTE_USER does not work 
215 properly with Catalyst 5.8004 and before (if you want details see source code). 
216
217 Note1: Apache + mod_ssl uses SSL_CLIENT_S_DN, SSL_CLIENT_S_DN_* etc. (has to be 
218 enabled by 'SSLOption +StdEnvVars') or you can also let Apache make a copy of 
219 this value into REMOTE_USER (Apache option 'SSLUserName SSL_CLIENT_S_DN'). 
220
221 Note2: Microsoft IIS uses CERT_SUBJECT, CERT_SERIALNUMBER etc. for storing info
222 about client authenticated via SSL certificate. AUTH_USER on IIS seems to have
223 the same value as REMOTE_USER (but there might be some differences I am not
224 aware of).
225
226 =head2 deny_regexp
227
228 This config item is B<OPTIONAL> - no default value.
229
230 B<deny_regexp> contains a regular expression used for check against WEBUSER 
231 (see details below)
232
233 =head2 allow_regexp
234
235 This config item is B<OPTIONAL> - no default value.
236
237 B<deny_regexp> contains a regular expression used for check against WEBUSER.
238
239 Allow/deny checking of WEBUSER values goes in this way:
240
241 1) If B<deny_regexp> is defined and WEBUSER matches deny_regexp then 
242 authentication FAILS otherwise continues with next step. If deny_regexp is not 
243 defined or is an empty string we skip this step.  
244
245 2) If B<allow_regexp> is defined and WEBUSER matches allow_regexp then 
246 authentication PASSES otherwise FAILS. If allow_regexp is not 
247 defined or is an empty string we skip this step.
248
249 The order deny-allow is fixed.
250
251 =head2 cutname_regexp
252
253 This config item is B<OPTIONAL> - no default value.
254
255 If param B<cutname_regexp> is specified we try to cut the final usename passed to
256 Catalyst application as a substring from WEBUSER. This is usefull for 
257 example in case of SSL authentication when WEBUSER looks like this 
258 'CN=john, OU=Unit Name, O=Company, C=CZ' - from this format we can simply cut
259 pure usename by cutname_regexp set to 'CN=(.*), OU=Unit Name, O=Company, C=CZ'.
260
261 Substring is always taken as '$1' regexp substring. If WEBUSER does not
262 match cutname_regexp at all or if '$1' regexp substring is empty we pass the
263 original WEBUSER value (without cutting) to Catalyst application.
264
265 =head1 METHODS
266
267 =head2 new ( $config, $app, $realm )
268
269 Instantiate a new Catalyst::Authentication::Credential::Remote object using the
270 configuration hash provided in $config. In case of invalid value of any 
271 configuration parameter (e.g. invalid regular expression) throws an exception.
272
273 =cut
274
275 =head2 authenticate ( $realm, $authinfo )
276
277 Takes the username form WEBUSER set by webserver, performs additional 
278 checks using optional allow_regexp/deny_regexp configuration params, optionaly 
279 takes substring from WEBUSER and the sets the resulting value as
280 a Catalyst username.
281
282 =cut
283
284 =head1 COMPATIBILITY
285
286 It is B<strongly recommended> to use this module with Catalyst 5.80005 and above
287 as previous versions have some bugs related to $c->engine->env and do not 
288 support $c->req->remote_user.
289
290 This module tries some workarounds when it detects an older version and should
291 work as well.
292
293 =cut