File Coverage

blib/lib/Catalyst/Authentication/Credential/Remote.pm
Criterion Covered Total %
statement 46 56 82.1
branch 21 38 55.2
condition 15 28 53.5
subroutine 8 11 72.7
pod 2 2 100.0
total 92 135 68.1


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