Add howto
[catagits/Catalyst-Plugin-Authentication.git] / lib / Catalyst / Authentication / Credential / Remote.pm
CommitLineData
b94c4996 1package Catalyst::Authentication::Credential::Remote;
202af0af 2use Moose;
3use namespace::autoclean;
b94c4996 4
202af0af 5with 'MooseX::Emulate::Class::Accessor::Fast';
b94c4996 6
202af0af 7use Try::Tiny qw/ try catch /;
b94c4996 8
202af0af 9__PACKAGE__->mk_accessors(
10 qw/allow_re deny_re cutname_re source realm username_field/);
b94c4996 11
12sub new {
13 my ( $class, $config, $app, $realm ) = @_;
14
15 my $self = { };
16 bless $self, $class;
7289cea0 17
b94c4996 18 # we are gonna compile regular expresions defined in config parameters
8f57bf96 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 };
b94c4996 26 }
8f57bf96 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 };
b94c4996 33 }
8f57bf96 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 };
b94c4996 40 }
41 $self->source($config->{source} || 'REMOTE_USER');
42 $self->realm($realm);
0de2d093 43 $self->username_field($config->{username_field} || 'username');
b94c4996 44 return $self;
45}
46
47sub authenticate {
48 my ( $self, $c, $realm, $authinfo ) = @_;
49
50 my $remuser;
51 if ($self->source eq "REMOTE_USER") {
52 # compatibility hack:
0e28e47d 53 if ($c->engine->can('env') && defined($c->engine->env)) {
b94c4996 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
675fe850 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 }
b94c4996 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 }
69be7d10 112
113 $authinfo->{ $self->username_field } = $usr;
b94c4996 114 my $user_obj = $realm->find_user( $authinfo, $c );
115 return ref($user_obj) ? $user_obj : undef;
116}
117
1181;
119
120__END__
121
122=pod
123
124=head1 NAME
125
126Catalyst::Authentication::Credential::Remote - Let the webserver (e.g. Apache)
127authenticate 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
176This module allows you to authenticate the users of your Catalyst application
177on underlaying webserver. The complete list of authentication method available
178via this module depends just on what your webserver (e.g. Apache, IIS, Lighttpd)
179is able to handle.
180
181Besides the common methods like HTTP Basic and Digest authentication you can
182also use sophisticated ones like so called "integrated authentication" via
183NTLM or Kerberos (popular in corporate intranet applications running in Windows
e17a338b 184Active Directory environment) or even the SSL authentication when users
b94c4996 185authenticate themself using their client SSL certificates.
186
187The main idea of this module is based on a fact that webserver passes the name
188of authenticated user into Catalyst application as REMOTE_USER variable (or in
189case of SSL client authentication in other variables like SSL_CLIENT_S_DN on
190Apache + mod_ssl) - from this point referenced as WEBUSER.
191This module simply takes this value - perfoms some optional checks (see
192below) - and if everything is OK the WEBUSER is declared as authenticated on
193Catalyst level. In fact this module does not perform any check for password or
194other credential; it simply believes the webserver that user was properly
195authenticated.
196
197=head1 CONFIG
198
199=head2 class
200
201This config item is B<REQUIRED>.
202
203B<class> is part of the core L<Catalyst::Plugin::Authentication> module, it
204contains the class name of the store to be used.
205
206The classname used for Credential. This is part of L<Catalyst::Plugin::Authentication>
207and is the method by which Catalyst::Authentication::Credential::Remote is
208loaded as the credential validator. For this module to be used, this must be set
209to 'Remote'.
210
211=head2 source
212
213This config item is B<OPTIONAL> - default is REMOTE_USER.
214
215B<source> contains a name of a variable passed from webserver that contains the
216user identification.
217
218Supported values: REMOTE_USER, SSL_CLIENT_*, CERT_*, AUTH_USER
219
220B<BEWARE:> Support for using different variables than REMOTE_USER does not work
221properly with Catalyst 5.8004 and before (if you want details see source code).
222
223Note1: Apache + mod_ssl uses SSL_CLIENT_S_DN, SSL_CLIENT_S_DN_* etc. (has to be
224enabled by 'SSLOption +StdEnvVars') or you can also let Apache make a copy of
225this value into REMOTE_USER (Apache option 'SSLUserName SSL_CLIENT_S_DN').
226
227Note2: Microsoft IIS uses CERT_SUBJECT, CERT_SERIALNUMBER etc. for storing info
228about client authenticated via SSL certificate. AUTH_USER on IIS seems to have
229the same value as REMOTE_USER (but there might be some differences I am not
230aware of).
231
232=head2 deny_regexp
233
234This config item is B<OPTIONAL> - no default value.
235
236B<deny_regexp> contains a regular expression used for check against WEBUSER
237(see details below)
238
239=head2 allow_regexp
240
241This config item is B<OPTIONAL> - no default value.
242
243B<deny_regexp> contains a regular expression used for check against WEBUSER.
244
245Allow/deny checking of WEBUSER values goes in this way:
246
2471) If B<deny_regexp> is defined and WEBUSER matches deny_regexp then
248authentication FAILS otherwise continues with next step. If deny_regexp is not
249defined or is an empty string we skip this step.
250
2512) If B<allow_regexp> is defined and WEBUSER matches allow_regexp then
252authentication PASSES otherwise FAILS. If allow_regexp is not
253defined or is an empty string we skip this step.
254
255The order deny-allow is fixed.
256
257=head2 cutname_regexp
258
259This config item is B<OPTIONAL> - no default value.
260
261If param B<cutname_regexp> is specified we try to cut the final usename passed to
e17a338b 262Catalyst application as a substring from WEBUSER. This is useful for
b94c4996 263example 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
265pure usename by cutname_regexp set to 'CN=(.*), OU=Unit Name, O=Company, C=CZ'.
266
267Substring is always taken as '$1' regexp substring. If WEBUSER does not
268match cutname_regexp at all or if '$1' regexp substring is empty we pass the
269original WEBUSER value (without cutting) to Catalyst application.
270
7289cea0 271=head2 username_field
272
273This config item is B<OPTIONAL> - default is I<username>
274
275The key name in the authinfo hash that the user's username is mapped into.
276This is useful for using a store which requires a specific unusual field name
277for the username. The username is additionally mapped onto the I<id> key.
278
b94c4996 279=head1 METHODS
280
281=head2 new ( $config, $app, $realm )
282
283Instantiate a new Catalyst::Authentication::Credential::Remote object using the
284configuration hash provided in $config. In case of invalid value of any
285configuration parameter (e.g. invalid regular expression) throws an exception.
286
287=cut
288
289=head2 authenticate ( $realm, $authinfo )
290
291Takes the username form WEBUSER set by webserver, performs additional
292checks using optional allow_regexp/deny_regexp configuration params, optionaly
293takes substring from WEBUSER and the sets the resulting value as
294a Catalyst username.
295
296=cut
297
298=head1 COMPATIBILITY
299
300It is B<strongly recommended> to use this module with Catalyst 5.80005 and above
301as previous versions have some bugs related to $c->engine->env and do not
302support $c->req->remote_user.
303
304This module tries some workarounds when it detects an older version and should
305work as well.
306
3fc43032 307=head1 USING WITH A REVERSE PROXY
308
309If you are using a reverse proxy, then the WEBUSER will not be
310directly accessible by the Catalyst server. To use remote
311authentication, you will have to modify the web server to set a header
312containing the WEBUSER. You would then need to modify the PSGI
313configuration to map the header back to the WEBUSER variable.
314
315For 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
323You then need to create a Plack::Middleware module to map the
324header 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
347Finally, 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
b94c4996 364=cut