Project

General

Profile

Download (16.4 KB) Statistics
| Branch: | Revision:
1
#
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
    (1-1/1)