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