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