Project

General

Profile

Download (16.4 KB) Statistics
| Branch: | Revision:
1 95b003ff Origo
#
2
# Module to generate authentication tickets for mod_auth_tkt apache module.
3
#
4
5
package Apache::AuthTkt;
6
7
use 5.005;
8
use Carp;
9
use MIME::Base64;
10
#use strict;
11
use vars qw($VERSION $AUTOLOAD);
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
    my $self = shift;
65
    local $_ = shift;
66
    return $1 if m/^\s*(\d+)\s*$/;
67
    my $sec = 0;
68
    while (m/\G(\d+)([shdwmMy])\b\s*/gc) {
69
        my $amt = $1;
70
        my $unit = $2 || 's';
71
        $sec += $amt * $units{$unit};
72
#       print STDERR "$amt : $unit : $sec\n";
73
    }
74
    return $sec;
75
}
76
77
# Parse (simplistically) the given apache config file for TKTAuth directives
78
sub parse_conf
79
{
80
    my $self = shift;
81
    my ($conf) = @_;
82
83
    my %seen = ();
84
    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
    local $/ = "\n";
89
    while (<CF>) {
90
        if (m/^\s*(${PREFIX}\w+)\s+(.*)/) {
91
            $seen{$1} = $2 unless exists $seen{$1};
92
        }
93
    }
94
95
    close CF;
96
    die "[$me] TKTAuthSecret directive not found in config file '$conf'"
97
        unless $seen{TKTAuthSecret};
98
99
    # Set directives as $self attributes
100
    my %merge = ( %seen );
101
    for my $directive (keys %merge) {
102
        local $_ = $directive;
103
        s/^TKTAuth(\w)/\L$1/;
104
        s/([a-z])([A-Z]+)/\L$1_$2/g;
105
        $merge{$directive} =~ s/^"([^"]+)"$/$1/ if $merge{$directive};
106
        if ($BOOLEAN{$directive}) {
107
            $merge{$directive} = 0 
108
                if $merge{$directive} =~ m/^(off|no|false)$/i;
109
            $merge{$directive} = 1 
110
                if $merge{$directive} =~ m/^(on|yes|true)$/i;
111
        }
112
        elsif (defined $merge{$directive}) {
113
            $merge{$directive} =~ s/^\s+//;
114
            $merge{$directive} =~ s/\s+$//;
115
        }
116
        if ($directive eq 'TKTAuthCookieExpires' || $directive eq 'TKTAuthTimeout') {
117
          $self->{$_} = $self->convert_time_seconds($merge{$directive});
118
        }
119
        # Don't allow TKTAuthDebug to turn on debugging here
120
        elsif ($directive ne 'TKTAuthDebug') {
121
          $self->{$_} = $merge{$directive};
122
        }
123
    }
124
}
125
126
# Process constructor args
127
sub init
128
{
129
    my $self = shift;
130
    my %arg = @_;
131
132
    # Check for invalid args
133
    for (keys %arg) {
134
        croak "[$me] invalid argument to constructor: $_" unless exists $ATTR{$_};
135
    }
136
137
    # Parse config file if set
138
    if ($arg{conf}) {
139
        $self->parse_conf($arg{conf});
140
    }
141
142
    # Store/override from given args
143
    $self->{$_} = $arg{$_} foreach keys %arg;
144
145
    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
        unless $DIGEST_TYPE{ $self->{digest_type } };
149
150
    $self;
151
}
152
153
# Constructor
154
sub new
155
{
156
    my $class = shift;
157
    my $self = { %DEFAULTS };
158
    bless $self, $class;
159
    $self->init(@_);
160
}
161
162
# Setup autoload accessors/mutators
163
sub AUTOLOAD {
164
    my $self = shift;
165
    my $attr = $AUTOLOAD;
166
    $attr =~ s/.*:://;
167
    die qq(Can't locate object method "$attr" via package "$self")
168
        unless $ATTR{$attr};
169
    @_ and $self->{$attr} = $_[0];
170
    return $self->{$attr};
171
}
172
173
sub DESTROY {}
174
175
sub errstr
176
{
177
    my $self = shift;
178
    $@[0] and $self->{errstr} = join ' ', @_;
179
    $self->{errstr};
180
}
181
182
# Return a mod_auth_tkt ticket containing the given user details
183
sub ticket
184
{
185
    my $self = shift;
186
    my %DEFAULTS = (
187
        base64 => 1,
188
        data => '',
189
        tokens => '',
190
    );
191
    my %arg = ( %DEFAULTS, %$self, @_ );
192
    $arg{uid} = $self->guest_user unless exists $arg{uid};
193
    $arg{ip_addr} = $arg{ignore_ip} ? '0.0.0.0' : $ENV{REMOTE_ADDR}
194
        unless exists $arg{ip_addr};
195
    # 0 or undef ip_addr treated as 0.0.0.0
196
    $arg{ip_addr} ||= '0.0.0.0';
197
198
    # Data cleanups
199
    if ($arg{tokens}) {
200
        $arg{tokens} =~ s/\s+,/,/g;
201
        $arg{tokens} =~ s/,\s+/,/g;
202
    }
203
204
    # Data checks
205
    if ($arg{ip_addr} !~ m/^([12]?[0-9]?[0-9]\.){3}[12]?[0-9]?[0-9]$/) {
206
        $self->errstr("invalid ip_addr '$arg{ip_addr}'");
207
        return undef;
208
    }
209
    if ($arg{tokens} =~ m/[!\s]/) {
210
        $self->errstr("invalid chars in tokens '$arg{tokens}'");
211
        return undef;
212
    }
213
214
    # Calculate the hash for the ticket
215
    my $ts = $arg{ts} || time;
216
    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
    my $ticket = sprintf "%s%08x%s!", $digest, $ts, $arg{uid};
221
    $ticket .= $arg{tokens} . '!' if $arg{tokens};
222
    $ticket .= $arg{data};
223
    
224
    return $arg{base64} ? encode_base64($ticket, '') : $ticket;
225
}
226
227
sub _get_digest_function
228
{
229
    my $self = shift;
230
231
    die "Invalid digest_type '" . $self->digest_type . "'\n"
232
        unless $DIGEST_TYPE{ $self->digest_type };
233
234
    my ($module, $func) = @{ $DIGEST_TYPE{ $self->digest_type } };
235
    eval "require $module";
236
    return eval "\\&${module}::$func";
237
}
238
239
sub _get_digest
240
{
241
    my ($self, $ts, $ip_addr, $uid, $tokens, $data, $debug) = @_;
242
    my @ip = split /\./, $ip_addr;
243
    my @ts = ( (($ts & 0xff000000) >> 24),
244
               (($ts & 0xff0000) >> 16),
245
               (($ts & 0xff00) >> 8),
246
               (($ts & 0xff)) );
247
    my $ipts = pack("C8", @ip, @ts);
248
    my $raw = $ipts . $self->secret . $uid . "\0" . $tokens . "\0" . $data;
249
    my $digest_function = $self->_get_digest_function;
250
    my $digest0 = $digest_function->($raw);
251
    my $digest  = $digest_function->($digest0 . $self->secret);
252
253
    if ($debug) {
254
        print STDERR "ts: $ts\nip_addr: $ip_addr\nuid: $uid\ntokens: $tokens\ndata: $data\n";
255
        print STDERR "secret: " . $self->secret . "\n";
256
        print STDERR "raw: '$raw'\n";
257
        my $len = length($raw);
258
        print STDERR "digest0: $digest0 (input length $len)\n";
259
        print STDERR "digest: $digest\n";
260
    }
261
262
    return $digest;
263
}
264
265
# Return a cookie containing a mod_auth_tkt ticket 
266
sub cookie
267
{
268
    my $self = shift;
269
    my %DEFAULTS = (
270
        cookie_name => 'auth_tkt',
271
        cookie_path => '/',
272
    );
273
    my %arg = ( %DEFAULTS, %$self, @_ );
274
    $arg{cookie_domain} ||= $self->domain;
275
276
    # Get ticket, forcing base64 for cookies
277
    my $ticket = $self->ticket(@_, base64 => 1) or return;
278
279
    my $cookie_fmt = "%s=%s%s%s%s";
280
    my $path_elt = "; path=$arg{cookie_path}";
281
    my $domain_elt = $arg{cookie_domain} ? "; domain=$arg{cookie_domain}" : '';
282
    my $secure_elt = $arg{cookie_secure} ? "; secure" : '';
283
    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
    my $self = shift;
292
    my $ticket = shift || croak "No ticket passed to validate_ticket";
293
    my %arg = ( %$self, @_ );
294
295
    $arg{ip_addr} = $arg{ignore_ip} ? '0.0.0.0' : $ENV{REMOTE_ADDR}
296
        unless exists $arg{ip_addr};
297
    # 0 or undef ip_addr treated as 0.0.0.0
298
    $arg{ip_addr} ||= '0.0.0.0';
299
300
    # Parse ticket
301
    my $info = $self->parse_ticket($ticket);
302
303
    # Validate digest
304
    my $expected_digest = $self->_get_digest(
305
        $info->{ts}, $arg{ip_addr}, $info->{uid},
306
        $info->{tokens}, $info->{data});
307
308
    return $info if $expected_digest eq $info->{digest};
309
    return undef;
310
}
311
312
sub parse_ticket
313
{
314
    my $self    = shift;
315
    my $ticket  = shift or croak "No ticket passed to parse_ticket";
316
    my $parts   = {};
317
318
    # Strip possible quotes
319
    $ticket =~ s,^"|"$,,g;
320
321
    return if length($ticket) < 40;
322
323
    # Assume $ticket is not URL-escaped but may be base64-escaped
324
    my $raw = $ticket =~ m/!/ ? $ticket : decode_base64($ticket);
325
326
    # If $raw still doesn't have ! then it is bogus
327
    return if $raw !~ m/!/;
328
    
329
    # Deconstruct
330
    my ($digest,$ts,$uid,$extra) = ($raw =~ m/^(.{32})(.{8})(.+?)!(.*)$/);
331
    $parts->{digest} = $digest;
332
    $parts->{ts}  = hex($ts);
333
    $parts->{uid} = $uid;
334
    $parts->{tokens} = '';
335
    $parts->{data} = '';
336
337
    # Tokens and data if present
338
    if (defined $extra) {
339
        if ($extra =~ m/!/) {
340
            ($parts->{tokens},$parts->{data}) = split m/!/, $extra, 2;
341
        }
342
        else {
343
            $parts->{data} = $extra;
344
        }
345
    }
346
    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__
355
356
=head1 NAME
357
358
Apache::AuthTkt - module to generate authentication tickets for 
359
mod_auth_tkt apache module.
360
361
362
=head1 SYNOPSIS
363
364
    # Constructor - either (preferred):
365
    $at = Apache::AuthTkt->new(
366
        conf => '/etc/httpd/conf.d/auth_tkt.conf',
367
    );
368
    # OR:
369
    $at = Apache::AuthTkt->new(
370
        secret => '818f9c9d-91ed-4b74-9f48-ff99cfe00a0e',
371
        digest_type => 'MD5',
372
    );
373
374
    # Generate ticket
375
    $ticket = $at->ticket(uid => $username, ip_addr => $ip_addr);
376
377
    # Or generate cookie containing ticket
378
    $cookie = $at->cookie(
379
        uid => $username, 
380
        cookie_name => 'auth_tkt',
381
        cookie_domain => 'www.openfusion.com.au',
382
    );
383
384
    # Access the shared secret
385
    $secret = $at->secret();
386
    # If using the 'conf' constructor above, all other TKTAuth attributes 
387
    #   are also available e.g.:
388
    print $at->cookie_name(), $at->ignore_ip(), $at->request_ssl();
389
390
    # Report error string
391
    print $at->errstr;
392
393
394
=head1 INTRODUCTION
395
396
Apache::AuthTkt is a module for generating and validating 
397
authentication tickets used with the 'mod_auth_tkt' apache module. 
398
Tickets are typically generated by a login web page of some kind 
399
when a user has been authenticated. The ticket contains a username/uid 
400
for the authenticated user, and often also the IP address they 
401
authenticated from, a set of authorisation tokens, and any other user 
402
data required. The ticket also includes an MD5 hash of all the included 
403
user data plus a shared secret, so that tickets can be validated by 
404
mod_auth_tkt without requiring access to the user repository.
405
406
See http://www.openfusion.com.au/labs/mod_auth_tkt for mod_auth_tkt
407
itself.
408
409
410
=head1 DESCRIPTION
411
412
=head2 CONSTRUCTOR
413
414
An Apache::AuthTkt object is created via a standard constructor
415
with named arguments. The preferred form is to point the constructor
416
to the apache config file containing the mod_auth_tkt TKTAuthSecret
417
directive, from which Apache::AuthTkt will parse the shared secret
418
it needs, as well as any additional TKTAuth* directives it finds:
419
420
    $at = Apache::Tkt->new(
421
        conf => '/etc/httpd/conf/auth_tkt.conf',
422
    );
423
424
Alternatively, you can pass the mod_auth_tkt shared secret (the 
425
TKTAuthSecret value) and the digest_type to use (default is 'MD5')
426
explicitly to the constructor:
427
428
    $at = Apache::AuthTkt->new(
429
        secret => '818f9c9d-91ed-4b74-9f48-ff99cfe00a0e',
430
        digest_type => 'SHA256',
431
    );
432
433
=head2 ACCESSORS
434
435
If the 'conf' form of the constructor is used, Apache::AuthTkt parses
436
all additional TKTAuth* directives it finds there and stores them in
437
additional internal attributes. Those values are available via 
438
accessors named after the relevant TKTAuth directive (with the 'TKTAuth'
439
prefix dropped and converted to lowercase underscore format) i.e.
440
441
    $at->secret()
442
    $at->secret_old()
443
    $at->digest_type()
444
    $at->cookie_name()
445
    $at->back_cookie_name()
446
    $at->back_arg_name()
447
    $at->domain()
448
    $at->cookie_expires()
449
    $at->login_url()
450
    $at->timeout_url()
451
    $at->unauth_url()
452
    $at->timeout()
453
    $at->timeout_refresh()
454
    $at->token ()
455
    $at->guest_login()
456
    $at->ignore_ip()
457
    $at->require_ssl()
458
459
460
=head2 TICKET GENERATION
461
462
Tickets are generated using the ticket() method with named parameters:
463
464
    # Generate ticket
465
    $ticket = $at->ticket(uid => $username);
466
467
Ticket returns undef on error, with error information available via
468
the errstr() method:
469
 
470
    $ticket = $at->ticket or die $at->errstr;
471
472
ticket() accepts the following arguments, all optional:
473
474
=over 4
475
476
=item uid
477
478
uid, username, or other user identifier for this ticket. There is no
479
requirement that this be unique per-user. Default: 'guest'.
480
481
=item ip_addr
482
483
IP address associated with this ticket. Default: if $at->ignore_ip
484
is true, then '0.0.0.0', otherwise $ENV{REMOTE_ADDR};
485
486
=item tokens
487
488
A comma-separated list of tokens associated with this user. Typically
489
only used if you are using the mod_auth_tkt TKTAuthToken directive.
490
Default: none.
491
492
=item data
493
494
Arbitrary user data to be stored for this ticket. This data is included
495
in the MD5 hash check. Default: none.
496
497
=item base64
498
499
Flag used to indicate whether to base64-encode the ticket. Default: 1.
500
501
=item ts
502
503
Explicitly set the timestamp to use for this ticket. Only for testing!
504
505
=back
506
507
508
As an alternative to ticket(), the cookie() method can be used to 
509
return the generated ticket in cookie format. cookie() returns undef 
510
on error, with error information available via the errstr() method:
511
512
    $cookie = $at->cookie or die $at->errstr;
513
514
cookie() supports all the same arguments as ticket(), plus the 
515
following:
516
517
=over 4
518
519
=item cookie_name
520
521
Cookie name. Should match the TKTAuthCookieName directive, if you're
522
using it. Default: $at->cookie_name, or 'auth_tkt'.
523
524
=item cookie_domain
525
526
Cookie domain. Should match the TKTAuthDomain directive, if you're
527
using it. Default: $at->domain.
528
529
=item cookie_path
530
531
Cookie path. Default: '/'.
532
533
=item cookie_secure
534
535
Flag whether to set the 'secure' cookie flag, so that the cookie is 
536
returned only in HTTPS contexts. Default: $at->require_ssl, or 0.
537
538
=back
539
540
=head2 TICKET PARSING AND VALIDATION
541
542
You may parse and validate existing tickets with the validate_ticket() 
543
method. It takes as its first parameter the ticket to be validated, and
544
then an optional list of named parameter overrides 
545
(e.g. ip_addr => 'x.x.x.x'). If the ticket is valid, validate_ticket 
546
returns a hashref with the following key/value pairs:
547
548
=over 4
549
550
=item digest
551
552
=item ts
553
554
=item uid
555
556
=item tokens
557
558
=item data
559
560
=back
561
562
validate_ticket() will return undef if any errors with the ticket value 
563
are encountered.
564
565
The validate_ticket() method algorithm is analogous to the function with
566
the same name in the mod_auth_tkt C module.
567
568
There is also a parse_ticket() method available that parses the ticket
569
without running it through the validation phase, and returns the same
570
data as validate_ticket(). This is only safe to use where you are certain
571
that the ticket has been validated elsewhere. In general it's considerably
572
safer to just use validate_ticket.
573
574
575
=head2 DIGEST TYPES
576
577
As of version 2.1.0, mod_auth_tkt supports multiple digest types. The
578
following digest_types are currently supported:
579
580
=over 4
581
582
=item MD5
583
584
The current default, for backwards compatibility. Requires the Digest::MD5
585
perl module.
586
587
=item SHA256
588
589
Requires the Digest::SHA perl module.
590
591
=back
592
593
These can be set either via your config (the TKTAuthDigestType directive)
594
or by passing a 'digest_type' parameter to the AuthTkt constructor.
595
596
597
=head1 AUTHOR
598
599
Gavin Carr <gavin@openfusion.com.au>
600
601
Contributors:
602
603
Peter Karman <peter@peknet.com>
604
605
Ton Voon <ton.voon@altinity.com>
606
607
Jose Luis Martinez <jlmartinez@capside.com>
608
609
=head1 COPYRIGHT
610
611
Copyright 2001-2009 Gavin Carr and contributors.
612
613
This program is free software. You may copy or redistribute it under the
614
same terms as perl itself.
615
616
=cut
617
618
619
# vim:sw=4