File Coverage

blib/lib/Apache/AuthTkt.pm
Criterion Covered Total %
statement 138 152 90.7
branch 66 82 80.4
condition 16 21 76.1
subroutine 16 18 88.8
pod 0 9 0.0
total 236 282 83.6


line stmt bran cond sub pod time code
1             #
2             # Module to generate authentication tickets for mod_auth_tkt apache module.
3             #
4              
5             package Apache::AuthTkt;
6              
7 10     10   472443 use 5.005;
  10         40  
  10         1302  
8 10     10   77 use Carp;
  10         16  
  10         2058  
9 10     10   11685 use MIME::Base64;
  10         12490  
  10         789  
10 10     10   101 use strict;
  10         21  
  10         803  
11 10     10   127 use vars qw($VERSION $AUTOLOAD);
  10         17  
  10         38119  
12              
13             $VERSION = 2.1;
14              
15             my $me = 'Apache::AuthTkt';
16             my $PREFIX = 'TKTAuth';
17             my %DEFAULTS = (
18             digest_type => 'MD5',
19             cookie_name => 'auth_tkt',
20             back_arg_name => 'back',
21             timeout => 2 * 60 * 60,
22             timeout_min => 2 * 60,
23             timeout_refresh => 0.5,
24             guest_login => 0,
25             guest_user => 'guest',
26             ignore_ip => 0,
27             require_ssl => 0,
28             cookie_secure => 0,
29             );
30             my %BOOLEAN = map { $_ => 1 } qw(
31             TKTAuthGuestLogin TKTAuthIgnoreIP TKTAuthRequireSSL TKTAuthCookieSecure
32             );
33             # Default TKTAuthDomain to host part of HTTP_HOST, or SERVER_NAME
34             ($DEFAULTS{TKTAuthDomain}) = split /:/, $ENV{HTTP_HOST} || '';
35             $DEFAULTS{TKTAuthDomain} ||= $ENV{SERVER_NAME};
36             my %ATTR = map { $_ => 1 } qw(
37             conf secret secret_old digest_type
38             cookie_name back_cookie_name back_arg_name domain cookie_expires
39             login_url timeout_url post_timeout_url unauth_url
40             timeout timeout_min timeout_refresh token debug
41             guest_login guest_user ignore_ip require_ssl cookie_secure
42             );
43             #my %TICKET_ARGS = map { $_ => 1 }
44              
45             # digest_type => [ module, function ]
46             my %DIGEST_TYPE = (
47             MD5 => [ 'Digest::MD5', 'md5_hex' ],
48             SHA256 => [ 'Digest::SHA', 'sha256_hex' ],
49             SHA512 => [ 'Digest::SHA', 'sha512_hex' ],
50             );
51              
52             # Helper routine to convert time units into seconds
53             my %units = (
54             s => 1,
55             m => 60,
56             h => 3600,
57             d => 86400,
58             w => 7 * 86400,
59             M => 30 * 86400,
60             y => 365 * 86400,
61             );
62             sub convert_time_seconds
63             {
64 12     12 0 18 my $self = shift;
65 12         24 local $_ = shift;
66 12 100       70 return $1 if m/^\s*(\d+)\s*$/;
67 10         16 my $sec = 0;
68 10         51 while (m/\G(\d+)([shdwmMy])\b\s*/gc) {
69 20         34 my $amt = $1;
70 20   50     56 my $unit = $2 || 's';
71 20         86 $sec += $amt * $units{$unit};
72             # print STDERR "$amt : $unit : $sec\n";
73             }
74 10         64 return $sec;
75             }
76              
77             # Parse (simplistically) the given apache config file for TKTAuth directives
78             sub parse_conf
79             {
80 21     21 0 35 my $self = shift;
81 21         31 my ($conf) = @_;
82              
83 21         40 my %seen = ();
84 21 100       977 open CF, "<$conf" or
85             die "[$me] open of config file '$conf' failed: $!";
86              
87             # Take settings from first instance of each TKTAuth directive found
88 20         106 local $/ = "\n";
89 20         1005 while () {
90 257 100       1807 if (m/^\s*(${PREFIX}\w+)\s+(.*)/) {
91 71 50       2844 $seen{$1} = $2 unless exists $seen{$1};
92             }
93             }
94              
95 20         299 close CF;
96 20 100       88 die "[$me] TKTAuthSecret directive not found in config file '$conf'"
97             unless $seen{TKTAuthSecret};
98              
99             # Set directives as $self attributes
100 19         94 my %merge = ( %seen );
101 19         60 for my $directive (keys %merge) {
102 71         107 local $_ = $directive;
103 71         5743 s/^TKTAuth(\w)/\L$1/;
104 71         421 s/([a-z])([A-Z]+)/\L$1_$2/g;
105 71 50       298 $merge{$directive} =~ s/^"([^"]+)"$/$1/ if $merge{$directive};
106 71 100       258 if ($BOOLEAN{$directive}) {
    50          
107 15 100       149 $merge{$directive} = 0
108             if $merge{$directive} =~ m/^(off|no|false)$/i;
109 15 100       114 $merge{$directive} = 1
110             if $merge{$directive} =~ m/^(on|yes|true)$/i;
111             }
112             elsif (defined $merge{$directive}) {
113 56         119 $merge{$directive} =~ s/^\s+//;
114 56         114 $merge{$directive} =~ s/\s+$//;
115             }
116 71 100 100     645 if ($directive eq 'TKTAuthCookieExpires' || $directive eq 'TKTAuthTimeout') {
    50          
117 12         34 $self->{$_} = $self->convert_time_seconds($merge{$directive});
118             }
119             # Don't allow TKTAuthDebug to turn on debugging here
120             elsif ($directive ne 'TKTAuthDebug') {
121 59         347 $self->{$_} = $merge{$directive};
122             }
123             }
124             }
125              
126             # Process constructor args
127             sub init
128             {
129 36     36 0 57 my $self = shift;
130 36         253 my %arg = @_;
131              
132             # Check for invalid args
133 36         120 for (keys %arg) {
134 73 50       261 croak "[$me] invalid argument to constructor: $_" unless exists $ATTR{$_};
135             }
136              
137             # Parse config file if set
138 36 100       117 if ($arg{conf}) {
139 21         72 $self->parse_conf($arg{conf});
140             }
141              
142             # Store/override from given args
143 34         231 $self->{$_} = $arg{$_} foreach keys %arg;
144              
145 34 100 100     397 croak "[$me] bad constructor - 'secret' or 'conf' argument required"
146             unless $self->{conf} || $self->{secret};
147             croak "[$me] invalid digest_type '" . $self->{digest_type} . "'"
148 33 100       345 unless $DIGEST_TYPE{ $self->{digest_type } };
149              
150 32         235 $self;
151             }
152              
153             # Constructor
154             sub new
155             {
156 36     36 0 13389 my $class = shift;
157 36         388 my $self = { %DEFAULTS };
158 36         132 bless $self, $class;
159 36         134 $self->init(@_);
160             }
161              
162             # Setup autoload accessors/mutators
163             sub AUTOLOAD {
164 341     341   40416 my $self = shift;
165 341         499 my $attr = $AUTOLOAD;
166 341         1463 $attr =~ s/.*:://;
167 341 100       958 die qq(Can't locate object method "$attr" via package "$self")
168             unless $ATTR{$attr};
169 339 100       855 @_ and $self->{$attr} = $_[0];
170 339         1621 return $self->{$attr};
171             }
172              
173 0     0   0 sub DESTROY {}
174              
175             sub errstr
176             {
177 0     0 0 0 my $self = shift;
178 0 0       0 $@[0] and $self->{errstr} = join ' ', @_;
179 0         0 $self->{errstr};
180             }
181              
182             # Return a mod_auth_tkt ticket containing the given user details
183             sub ticket
184             {
185 35     35 0 4387 my $self = shift;
186 35         126 my %DEFAULTS = (
187             base64 => 1,
188             data => '',
189             tokens => '',
190             );
191 35         403 my %arg = ( %DEFAULTS, %$self, @_ );
192 35 100       219 $arg{uid} = $self->guest_user unless exists $arg{uid};
193 35 100       179 $arg{ip_addr} = $arg{ignore_ip} ? '0.0.0.0' : $ENV{REMOTE_ADDR}
    100          
194             unless exists $arg{ip_addr};
195             # 0 or undef ip_addr treated as 0.0.0.0
196 35   100     110 $arg{ip_addr} ||= '0.0.0.0';
197              
198             # Data cleanups
199 35 100       85 if ($arg{tokens}) {
200 10         28 $arg{tokens} =~ s/\s+,/,/g;
201 10         39 $arg{tokens} =~ s/,\s+/,/g;
202             }
203              
204             # Data checks
205 35 50       224 if ($arg{ip_addr} !~ m/^([12]?[0-9]?[0-9]\.){3}[12]?[0-9]?[0-9]$/) {
206 0         0 $self->errstr("invalid ip_addr '$arg{ip_addr}'");
207 0         0 return undef;
208             }
209 35 50       96 if ($arg{tokens} =~ m/[!\s]/) {
210 0         0 $self->errstr("invalid chars in tokens '$arg{tokens}'");
211 0         0 return undef;
212             }
213              
214             # Calculate the hash for the ticket
215 35   33     95 my $ts = $arg{ts} || time;
216 35         282 my $digest = $self->_get_digest($ts, $arg{ip_addr}, $arg{uid}, $arg{tokens},
217             $arg{data}, $arg{debug});
218              
219             # Construct the ticket itself
220 35         175 my $ticket = sprintf "%s%08x%s!", $digest, $ts, $arg{uid};
221 35 100       104 $ticket .= $arg{tokens} . '!' if $arg{tokens};
222 35         53 $ticket .= $arg{data};
223            
224 35 100       366 return $arg{base64} ? encode_base64($ticket, '') : $ticket;
225             }
226              
227             sub _get_digest_function
228             {
229 47     47   66 my $self = shift;
230              
231 47 50       214 die "Invalid digest_type '" . $self->digest_type . "'\n"
232             unless $DIGEST_TYPE{ $self->digest_type };
233              
234 47         67 my ($module, $func) = @{ $DIGEST_TYPE{ $self->digest_type } };
  47         177  
235 47         3797 eval "require $module";
236 47         8770 return eval "\\&${module}::$func";
237             }
238              
239             sub _get_digest
240             {
241 47     47   122 my ($self, $ts, $ip_addr, $uid, $tokens, $data, $debug) = @_;
242 47         187 my @ip = split /\./, $ip_addr;
243 47         147 my @ts = ( (($ts & 0xff000000) >> 24),
244             (($ts & 0xff0000) >> 16),
245             (($ts & 0xff00) >> 8),
246             (($ts & 0xff)) );
247 47         205 my $ipts = pack("C8", @ip, @ts);
248 47         379 my $raw = $ipts . $self->secret . $uid . "\0" . $tokens . "\0" . $data;
249 47         112 my $digest_function = $self->_get_digest_function;
250 47         421 my $digest0 = $digest_function->($raw);
251 47         818 my $digest = $digest_function->($digest0 . $self->secret);
252              
253 47 50       124 if ($debug) {
254 0         0 print STDERR "ts: $ts\nip_addr: $ip_addr\nuid: $uid\ntokens: $tokens\ndata: $data\n";
255 0         0 print STDERR "secret: " . $self->secret . "\n";
256 0         0 print STDERR "raw: '$raw'\n";
257 0         0 my $len = length($raw);
258 0         0 print STDERR "digest0: $digest0 (input length $len)\n";
259 0         0 print STDERR "digest: $digest\n";
260             }
261              
262 47         173 return $digest;
263             }
264              
265             # Return a cookie containing a mod_auth_tkt ticket
266             sub cookie
267             {
268 11     11 0 5108 my $self = shift;
269 11         40 my %DEFAULTS = (
270             cookie_name => 'auth_tkt',
271             cookie_path => '/',
272             );
273 11         123 my %arg = ( %DEFAULTS, %$self, @_ );
274 11   100     170 $arg{cookie_domain} ||= $self->domain;
275              
276             # Get ticket, forcing base64 for cookies
277 11 50       31 my $ticket = $self->ticket(@_, base64 => 1) or return;
278              
279 11         14 my $cookie_fmt = "%s=%s%s%s%s";
280 11         22 my $path_elt = "; path=$arg{cookie_path}";
281 11 100       28 my $domain_elt = $arg{cookie_domain} ? "; domain=$arg{cookie_domain}" : '';
282 11 100       23 my $secure_elt = $arg{cookie_secure} ? "; secure" : '';
283 11         79 return sprintf $cookie_fmt,
284             $arg{cookie_name}, $ticket, $domain_elt, $path_elt, $secure_elt;
285             }
286              
287             # Returns a hashref representing the original ticket components
288             # Returns undef if there were any errors
289             sub validate_ticket
290             {
291 12     12 0 29 my $self = shift;
292 12   33     35 my $ticket = shift || croak "No ticket passed to validate_ticket";
293 12         125 my %arg = ( %$self, @_ );
294              
295 12 100       79 $arg{ip_addr} = $arg{ignore_ip} ? '0.0.0.0' : $ENV{REMOTE_ADDR}
    100          
296             unless exists $arg{ip_addr};
297             # 0 or undef ip_addr treated as 0.0.0.0
298 12   100     40 $arg{ip_addr} ||= '0.0.0.0';
299              
300             # Parse ticket
301 12         27 my $info = $self->parse_ticket($ticket);
302              
303             # Validate digest
304 12         50 my $expected_digest = $self->_get_digest(
305             $info->{ts}, $arg{ip_addr}, $info->{uid},
306             $info->{tokens}, $info->{data});
307              
308 12 100       89 return $info if $expected_digest eq $info->{digest};
309 5         47 return undef;
310             }
311              
312             sub parse_ticket
313             {
314 17     17 0 2793 my $self = shift;
315 17 50       40 my $ticket = shift or croak "No ticket passed to parse_ticket";
316 17         27 my $parts = {};
317              
318             # Strip possible quotes
319 17         205 $ticket =~ s,^"|"$,,g;
320              
321 17 50       46 return if length($ticket) < 40;
322              
323             # Assume $ticket is not URL-escaped but may be base64-escaped
324 17 100       80 my $raw = $ticket =~ m/!/ ? $ticket : decode_base64($ticket);
325              
326             # If $raw still doesn't have ! then it is bogus
327 17 50       51 return if $raw !~ m/!/;
328            
329             # Deconstruct
330 17         112 my ($digest,$ts,$uid,$extra) = ($raw =~ m/^(.{32})(.{8})(.+?)!(.*)$/);
331 17         49 $parts->{digest} = $digest;
332 17         42 $parts->{ts} = hex($ts);
333 17         32 $parts->{uid} = $uid;
334 17         30 $parts->{tokens} = '';
335 17         27 $parts->{data} = '';
336              
337             # Tokens and data if present
338 17 50       36 if (defined $extra) {
339 17 100       31 if ($extra =~ m/!/) {
340 3         13 ($parts->{tokens},$parts->{data}) = split m/!/, $extra, 2;
341             }
342             else {
343 14         27 $parts->{data} = $extra;
344             }
345             }
346 17         58 return $parts;
347             }
348              
349             # Alias for compatibility with Jose/Ton's original patch
350             *valid_ticket = \&validate_ticket;
351              
352             1;
353              
354             __END__