File Coverage

blib/lib/Apache2/Authen/OdinAuth.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Apache2::Authen::OdinAuth;
2              
3 1     1   4642 use 5.006;
  1         2  
  1         33  
4 1     1   4 use strict;
  1         1  
  1         28  
5 1     1   5 use warnings;
  1         2  
  1         36  
6              
7             =head1 NAME
8              
9             Apache2::Authen::OdinAuth - A cookie-based single sign-on module for Apache.
10              
11             =head1 VERSION
12              
13             Version 0.8
14              
15             =cut
16              
17             our $VERSION = 0.8;
18              
19 1     1   5 use Crypt::OdinAuth;
  1         1  
  1         18  
20              
21 1     1   208 use Apache2::Log;
  0            
  0            
22             use Apache2::RequestRec ();
23             use Apache2::RequestUtil;
24             use Apache2::ServerUtil ();
25             use Apache2::URI ();
26             use Apache2::Connection;
27             use Apache2::Const -compile => qw(OK REDIRECT REMOTE_NOLOOKUP FORBIDDEN);
28             use APR::Table;
29             use YAML::XS;
30              
31             use Sys::Hostname;
32              
33             =head1 SYNOPSIS
34              
35             This module defines an Apache handler for the Odin Authenticator
36             single sign-on system. The system is based on the GodAuth script,
37             available at L.
38              
39             =head1 USAGE
40              
41             To make Apache use the handler for authentication, enable mod_perl and
42             add following directives in apache2.conf:
43              
44             PerlSetVar odinauth_config /path/to/odin_auth.yml
45             PerlFixupHandler Apache2::Authen::OdinAuth
46              
47             The C statement needs to be global; the
48             C statement can be global or occur in a
49             C, C, or C section.
50              
51             =head2 YAML CONFIG
52              
53             The handler reads (and automatically reloads if it's older than
54             C seconds) an additional YAML config file. It sets
55             configures the shared secret, cookie name, authorizer app URL, and
56             permissions (which are unfortunately regexp-based).
57              
58             A sample configuration file looks like this:
59              
60             # Sample config for Apache2::Authen::OdinAuth
61            
62             permissions:
63             # URLs no auth
64             - url: !!perl/regexp ^localhost
65             who: all
66             # Require a role
67             - url: !!perl/regexp ^dev\.myapp\.com
68             who: role:admin
69             # Require username
70             - url: !!perl/regexp ^debug\.myapp\.com/
71             who: cal
72             # A list is fine too
73             - url: !!perl/regexp ^debug2\.myapp\.com/
74             who:
75             - role:devel
76             - cal
77             - myles
78             # Allow any authenticated user
79             - url: !!perl/regexp ^debug3\.myapp\.com/
80             who: authed
81            
82            
83             # log_file: /tmp/odin.log
84             secret: ****************
85             reload_timeout: 600
86             need_auth_url: http://example.com/?NA
87             invalid_cookie_url: http://example.com/?CIU
88             not_on_list_url: http://example.com/?NOL
89             cookie: oa
90              
91             NOTE: The config is better than original GodAuth configuration, but
92             will probably need to be refactored; it would be best to make it live
93             inside Apache's configuration. I'm still not sure how to make it
94             happen in mod_perl.
95              
96             =cut
97              
98             use constant RELOAD_TIMEOUT => 10*60; # reload config every 10 minutes
99              
100             =head1 SUBROUTINES
101              
102             =head2 Configuration closure
103              
104             =cut
105             {
106             my $last_reload_time = -1;
107             my $config_file = undef;
108             my $config = undef;
109              
110             =head3 config
111              
112             Reloads configuration if older than RELOAD_TIMEOUT
113              
114             =cut
115             sub config {
116             if ( time() - $last_reload_time > RELOAD_TIMEOUT ) {
117             $config = YAML::XS::LoadFile($config_file);
118             }
119             $config;
120             }
121              
122             =head3 init_config(request)
123              
124             Finds config file and loads it for the first time
125              
126             =cut
127             sub init_config {
128             my $r = shift;
129             $config_file ||=
130             $r->server->dir_config('odinauth_config');
131             config;
132             }
133             }
134              
135             $| = 1;
136              
137             =head2 handler(request)
138              
139             Main Apache mod_perl handler
140              
141             =cut
142             sub handler {
143             #
144             # get URL
145             #
146              
147             my $r = shift;
148              
149             my $domain = $r->headers_in->{'Host'} || 'UNKNOWN-HOST';
150             my $path = $r->unparsed_uri;
151              
152             my $host = hostname;
153              
154             $ENV{OdinAuth_User} = '';
155              
156             my $url = $domain . $path;
157             my $log = "OdinAuth :: $url";
158              
159             init_config($r);
160              
161              
162             #########################################################
163             #
164             # 1) check we have a cookie secret
165             #
166             config->{'secret'} ||= 'nottherightsecret';
167              
168              
169             #########################################################
170             #
171             # 1) determine if we need to perform access control for this url
172             #
173              
174             my $allow = 'none';
175              
176             for my $obj (@{config->{'permissions'}}) {
177             if ($url =~ $obj->{url}) {
178             $allow = $obj->{who};
179             last;
180             }
181             }
182              
183             $log .= " allow:$allow";
184              
185              
186             #########################################################
187             #
188             # 2) we might need auth - see if we have a valid cookie
189             #
190              
191             my $cookie_is_invalid = 'by default';
192             my $cookie_user = '?';
193             my $cookie_roles = '_';
194              
195             my $cookies = &parse_cookie_jar($r->headers_in->{'Cookie'});
196             my $cookie = $cookies->{config->{cookie}};
197              
198             if ($cookie) {
199             my ( $user, $roles );
200             eval {
201             ( $user, $roles ) =
202             Crypt::OdinAuth::check_cookie(
203             config->{secret},
204             $cookie,
205             $r->headers_in->{'User-Agent'});
206             };
207              
208             if ( $@ ) {
209             $cookie_user = $user;
210             $cookie_roles = $roles;
211             chomp ( $cookie_is_invalid = $@ );
212             $log .= "(invalid cookie: $cookie_is_invalid)";
213             } else {
214             $cookie_is_invalid = undef;
215             $cookie_user = $user;
216             $cookie_roles = $roles;
217              
218             $r->headers_in->set('OdinAuth-User', $cookie_user);
219             $r->headers_in->set('OdinAuth-Roles', $cookie_roles);
220              
221             $ENV{OdinAuth_User} = $cookie_user;
222             $ENV{OdinAuth_Roles} = $cookie_roles;
223              
224             $r->notes->add("OdinAuth_User" => $cookie_user);
225             $r->notes->add("OdinAuth_Roles" => $cookie_roles);
226              
227             $log .= " (valid cookie: $cookie_user $cookie_roles)";
228             }
229             } else {
230             $log .= " (no cookie)";
231             }
232              
233             $r->log->debug($log);
234              
235             if ( $cookie_is_invalid ) {
236             $r->log->warn("Invalid cookie for $cookie_user($cookie_roles): $cookie_is_invalid");
237             }
238              
239             #########################################################
240             #
241             # 3) exit now if we got an 'all'
242             #
243              
244             if (ref $allow ne 'ARRAY') {
245             if ($allow eq 'all') {
246             return Apache2::Const::OK;
247             }
248             }
249              
250              
251             #########################################################
252             #
253             # 4) if we don't have a valid cookie, redirect to the auther
254             #
255              
256             if (!$cookie) {
257             return &redir($r, config->{need_auth_url});
258             }
259              
260             if ($cookie_is_invalid) {
261             return &redir($r, config->{invalid_cookie_url}, $cookie_is_invalid);
262             }
263              
264              
265             #########################################################
266             #
267             # 5) set user; exit now for authed
268             #
269              
270             $r->user($cookie_user);
271             $r->subprocess_env('REMOTE_USER' => $cookie_user);
272             $r->set_basic_credentials($cookie_user, '*****');
273              
274             if (ref $allow ne 'ARRAY') {
275             if ($allow eq 'authed') {
276             return Apache2::Const::OK;
277             }
278             }
279              
280              
281             #########################################################
282             #
283             # 5) now we need to match usernames and/or roles
284             #
285              
286             # get arrayref of allowed roles
287             unless (ref $allow eq 'ARRAY'){
288             $allow = [$allow];
289             }
290              
291             # get arrayref of our roles
292             my $matches = [$cookie_user];
293             for my $role (split /,/, $cookie_roles) {
294             if ($role ne '_') {
295             push @{$matches}, 'role:'.$role;
296             }
297             }
298              
299              
300             for my $a (@{$allow}) {
301             for my $b (@{$matches}) {
302              
303             if ($a eq $b) {
304             return Apache2::Const::OK;
305             }
306             }
307             }
308              
309              
310             #
311             # send the user to the not-on-list page
312             #
313              
314             return &redir($r, config->{not_on_list_url});
315             }
316              
317             =head2 redir(request, target, reason)
318              
319             Redirect to Authorizer App
320              
321             =cut
322             sub redir {
323             my ($r, $target, $reason) = @_;
324             my $ref = &urlencode($r->construct_url($r->unparsed_uri));
325             $target .= ($target =~ /\?/) ? "&ref=$ref" : "?ref=$ref";
326             $target .= '&reason='.urlencode($reason) if $reason;
327              
328             $r->headers_out->set('Location', $target);
329             return Apache2::Const::REDIRECT;
330             }
331              
332             =head2 parse_cookie_jar(jar)
333              
334             Parse cookies into a hashref
335              
336             =cut
337             sub parse_cookie_jar {
338             my ($jar) = @_;
339              
340             return {} unless defined $jar;
341              
342             my @bits = split /;\s*/, $jar;
343             my $out = {};
344             for my $bit (@bits) {
345             my ($k, $v) = split '=', $bit, 2;
346             $k = &urldecode($k);
347             $v = &urldecode($v);
348             $out->{$k} = $v;
349             }
350             return $out;
351             }
352              
353             =head2 urldecode(str)
354              
355             =cut
356             sub urldecode {
357             $_[0] =~ s!\+! !g;
358             $_[0] =~ s/%([a-fA-F0-9]{2,2})/chr(hex($1))/eg;
359             return $_[0];
360             }
361              
362             =head2 urlencode(str)
363              
364             =cut
365             sub urlencode {
366             $_[0] =~ s!([^a-zA-Z0-9-_ ])! sprintf('%%%02x', ord $1) !gex;
367             $_[0] =~ s! !+!g;
368             return $_[0];
369             }
370              
371              
372             =head1 AUTHOR
373              
374             Maciej Pasternacki, C<< >>
375              
376             =head1 BUGS
377              
378             Please report any bugs or feature requests to C, or through
379             the web interface at L. I will be notified, and then you'll
380             automatically be notified of progress on your bug as I make changes.
381              
382              
383              
384              
385             =head1 SUPPORT
386              
387             You can find documentation for this module with the perldoc command.
388              
389             perldoc Apache2::Authen::OdinAuth
390              
391              
392             You can also look for information at:
393              
394             =over 4
395              
396             =item * RT: CPAN's request tracker (report bugs here)
397              
398             L
399              
400             =item * AnnoCPAN: Annotated CPAN documentation
401              
402             L
403              
404             =item * CPAN Ratings
405              
406             L
407              
408             =item * Search CPAN
409              
410             L
411              
412             =back
413              
414              
415             =head1 ACKNOWLEDGEMENTS
416              
417              
418             =head1 LICENSE AND COPYRIGHT
419              
420             Copyright 2012 Maciej Pasternacki.
421              
422             This program is free software; you can redistribute it and/or modify it
423             under the terms of either: the GNU General Public License as published
424             by the Free Software Foundation; or the Artistic License.
425              
426             See http://dev.perl.org/licenses/ for more information.
427              
428              
429             =cut
430              
431             1; # End of Apache2::Authen::OdinAuth