dos newlines to unix ones, so native can kick in
[catagits/Catalyst-Authentication-Credential-HTTP.git] / lib / Catalyst / Plugin / Authentication / Credential / HTTP.pm
1 #!/usr/bin/perl
2
3
4
5 package Catalyst::Plugin::Authentication::Credential::HTTP;
6
7 use base qw/Catalyst::Plugin::Authentication::Credential::Password/;
8
9
10
11 use strict;
12
13 use warnings;
14
15
16
17 use String::Escape ();
18
19 use URI::Escape    ();
20
21 use Catalyst       ();
22
23 use Digest::MD5    ();
24
25
26
27 our $VERSION = "0.05";
28
29
30
31 sub authenticate_http {
32
33     my $c = shift;
34
35
36
37     return $c->authenticate_digest || $c->authenticate_basic;
38
39 }
40
41
42
43 sub authenticate_basic {
44
45     my $c = shift;
46
47
48
49     $c->log->debug('Checking http basic authentication.') if $c->debug;
50
51
52
53     my $headers = $c->req->headers;
54
55
56
57     if ( my ( $user, $password ) = $headers->authorization_basic ) {
58
59
60
61         if ( my $store = $c->config->{authentication}{http}{store} ) {
62
63             $user = $store->get_user($user);
64
65         }
66
67
68
69         return $c->login( $user, $password );
70
71     }
72
73
74
75     return 0;
76
77 }
78
79
80
81 sub authenticate_digest {
82
83     my $c = shift;
84
85
86
87     $c->log->debug('Checking http digest authentication.') if $c->debug;
88
89
90
91     my $headers       = $c->req->headers;
92
93     my @authorization = $headers->header('Authorization');
94
95     foreach my $authorization (@authorization) {
96
97         next unless $authorization =~ m{^Digest};
98
99
100
101         $c->_check_cache;
102
103
104
105         my %res = map {
106
107             my @key_val = split /=/, $_, 2;
108
109             $key_val[0] = lc $key_val[0];
110
111             $key_val[1] =~ s{"}{}g;    # remove the quotes
112
113             @key_val;
114
115         } split /,\s?/, substr( $authorization, 7 );    #7 == length "Digest "
116
117
118
119         my $opaque = $res{opaque};
120
121         my $nonce  = $c->cache->get( __PACKAGE__ . '::opaque:' . $opaque );
122
123         next unless $nonce;
124
125
126
127         $c->log->debug('Checking authentication parameters.')
128
129           if $c->debug;
130
131
132
133         my $uri         = '/' . $c->request->path;
134
135         my $algorithm   = $res{algorithm} || 'MD5';
136
137         my $nonce_count = '0x' . $res{nc};
138
139
140
141         my $check = $uri eq $res{uri}
142
143           && ( exists $res{username} )
144
145           && ( exists $res{qop} )
146
147           && ( exists $res{cnonce} )
148
149           && ( exists $res{nc} )
150
151           && $algorithm eq $nonce->algorithm
152
153           && hex($nonce_count) > hex( $nonce->nonce_count )
154
155           && $res{nonce} eq $nonce->nonce;    # TODO: set Stale instead
156
157
158
159         unless ($check) {
160
161             $c->log->debug('Digest authentication failed. Bad request.')
162
163               if $c->debug;
164
165             $c->res->status(400);             # bad request
166
167             die $Catalyst::DETACH;
168
169         }
170
171
172
173         $c->log->debug('Checking authentication response.')
174
175           if $c->debug;
176
177
178
179         my $username = $res{username};
180
181         my $realm    = $res{realm};
182
183
184
185         my $user;
186
187         my $store = $c->config->{authentication}{http}{store}
188
189           || $c->default_auth_store;
190
191         $user = $store->get_user($username) if $store;
192
193         unless ($user) {    # no user, no authentication
194
195             $c->log->debug('Unknown user: $user.') if $c->debug;
196
197             return 0;
198
199         }
200
201
202
203         # everything looks good, let's check the response
204
205
206
207         # calculate H(A2) as per spec
208
209         my $ctx = Digest::MD5->new;
210
211         $ctx->add( join( ':', $c->request->method, $res{uri} ) );
212
213         if ( $res{qop} eq 'auth-int' ) {
214
215             my $digest =
216
217               Digest::MD5::md5_hex( $c->request->body );    # not sure here
218
219             $ctx->add( ':', $digest );
220
221         }
222
223         my $A2_digest = $ctx->hexdigest;
224
225
226
227         # the idea of the for loop:
228
229         # if we do not want to store the plain password in our user store,
230
231         # we can store md5_hex("$username:$realm:$password") instead
232
233         for my $r ( 0 .. 1 ) {
234
235
236
237             # calculate H(A1) as per spec
238
239             my $A1_digest = $r ? $user->password : do {
240
241                 $ctx = Digest::MD5->new;
242
243                 $ctx->add( join( ':', $username, $realm, $user->password ) );
244
245                 $ctx->hexdigest;
246
247             };
248
249             if ( $nonce->algorithm eq 'MD5-sess' ) {
250
251                 $ctx = Digest::MD5->new;
252
253                 $ctx->add( join( ':', $A1_digest, $res{nonce}, $res{cnonce} ) );
254
255                 $A1_digest = $ctx->hexdigest;
256
257             }
258
259
260
261             my $rq_digest = Digest::MD5::md5_hex(
262
263                 join( ':',
264
265                     $A1_digest, $res{nonce},
266
267                     $res{qop} ? ( $res{nc}, $res{cnonce}, $res{qop} ) : (),
268
269                     $A2_digest )
270
271             );
272
273
274
275             $nonce->nonce_count($nonce_count);
276
277             $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque,
278
279                 $nonce );
280
281
282
283             return $c->login( $user, $user->password )
284
285               if $rq_digest eq $res{response};
286
287         }
288
289     }
290
291
292
293     return 0;
294
295 }
296
297
298
299 sub _check_cache {
300
301     my $c = shift;
302
303
304
305     die "A cache is needed for http digest authentication."
306
307       unless $c->can('cache');
308
309 }
310
311
312
313 sub _is_auth_type {
314
315     my ( $c, $type ) = @_;
316
317
318
319     my $cfgtype = lc( $c->config->{authentication}{http}{type} || 'any' );
320
321     return 1 if $cfgtype eq 'any' || $cfgtype eq lc $type;
322
323     return 0;
324
325 }
326
327
328
329 sub authorization_required {
330
331     my ( $c, %opts ) = @_;
332
333
334
335     return 1 if $c->_is_auth_type('digest') && $c->authenticate_digest;
336
337     return 1 if $c->_is_auth_type('basic')  && $c->authenticate_basic;
338
339
340
341     $c->authorization_required_response(%opts);
342
343
344
345     die $Catalyst::DETACH;
346
347 }
348
349
350
351 sub authorization_required_response {
352
353     my ( $c, %opts ) = @_;
354
355
356
357     $c->res->status(401);
358
359
360
361     my ( $digest, $basic );
362
363     $digest = $c->build_authorization_required_response( \%opts, 'Digest' )
364
365       if $c->_is_auth_type('digest');
366
367     $basic = $c->build_authorization_required_response( \%opts, 'Basic' )
368
369       if $c->_is_auth_type('basic');
370
371
372
373     die 'Could not build authorization required response. '
374
375       . 'Did you configure a valid authentication http type: '
376
377       . 'basic, digest, any'
378
379       unless $digest || $basic;
380
381
382
383     $c->res->headers->push_header( 'WWW-Authenticate' => $digest )
384
385       if $digest;
386
387     $c->res->headers->push_header( 'WWW-Authenticate' => $basic ) if $basic;
388
389 }
390
391
392
393 sub build_authorization_required_response {
394
395     my ( $c, $opts, $type ) = @_;
396
397     my @opts;
398
399
400
401     if ( my $realm = $opts->{realm} ) {
402
403         push @opts, 'realm=' . String::Escape::qprintable($realm);
404
405     }
406
407
408
409     if ( my $domain = $opts->{domain} ) {
410
411         Catalyst::Excpetion->throw("domain must be an array reference")
412
413           unless ref($domain) && ref($domain) eq "ARRAY";
414
415
416
417         my @uris =
418
419           $c->config->{authentication}{http}{use_uri_for}
420
421           ? ( map { $c->uri_for($_) } @$domain )
422
423           : ( map { URI::Escape::uri_escape($_) } @$domain );
424
425
426
427         push @opts, qq{domain="@uris"};
428
429     }
430
431
432
433     if ( $type eq 'Digest' ) {
434
435         my $package = __PACKAGE__ . '::Nonce';
436
437         my $nonce   = $package->new;
438
439         $nonce->algorithm( $c->config->{authentication}{http}{algorithm}
440
441               || $nonce->algorithm );
442
443
444
445         push @opts, 'qop="' . $nonce->qop . '"';
446
447         push @opts, 'nonce="' . $nonce->nonce . '"';
448
449         push @opts, 'opaque="' . $nonce->opaque . '"';
450
451         push @opts, 'algorithm="' . $nonce->algorithm . '"';
452
453
454
455         $c->_check_cache;
456
457         $c->cache->set( __PACKAGE__ . '::opaque:' . $nonce->opaque, $nonce );
458
459     }
460
461
462
463     return "$type " . join( ', ', @opts );
464
465 }
466
467
468
469 package Catalyst::Plugin::Authentication::Credential::HTTP::Nonce;
470
471
472
473 use strict;
474
475 use base qw[ Class::Accessor::Fast ];
476
477 use Data::UUID ();
478
479
480
481 our $VERSION = "0.01";
482
483
484
485 __PACKAGE__->mk_accessors(qw[ nonce nonce_count qop opaque algorithm ]);
486
487
488
489 sub new {
490
491     my $class = shift;
492
493     my $self  = $class->SUPER::new(@_);
494
495
496
497     $self->nonce( Data::UUID->new->create_b64 );
498
499     $self->opaque( Data::UUID->new->create_b64 );
500
501     $self->qop('auth,auth-int');
502
503     $self->nonce_count('0x0');
504
505     $self->algorithm('MD5');
506
507
508
509     return $self;
510
511 }
512
513
514
515 1;
516
517
518
519 __END__
520
521
522
523 =pod
524
525
526
527 =head1 NAME
528
529
530
531 Catalyst::Plugin::Authentication::Credential::HTTP - HTTP Basic and Digest authentication
532
533 for Catlayst.
534
535
536
537 =head1 SYNOPSIS
538
539
540
541     use Catalyst qw/
542
543         Authentication
544
545         Authentication::Store::Moose
546
547         Authentication::Credential::HTTP
548
549     /;
550
551
552
553     __PACKAGE__->config->{authentication}{http}{type} = 'any'; # or 'digest' or 'basic'
554     __PACKAGE__->config->{authentication}{users} = {
555         Mufasa => { password => "Circle Of Life", },
556     };
557
558
559
560     sub foo : Local {
561
562         my ( $self, $c ) = @_;
563
564
565
566         $c->authorization_required( realm => "foo" ); # named after the status code ;-)
567
568
569
570         # either user gets authenticated or 401 is sent
571
572
573
574         do_stuff();
575
576     }
577
578
579
580     # with ACL plugin
581
582     __PACKAGE__->deny_access_unless("/path", sub { $_[0]->authenticate_http });
583
584
585
586     sub end : Private {
587
588         my ( $self, $c ) = @_;
589
590
591
592         $c->authorization_required_response( realm => "foo" );
593
594         $c->error(0);
595
596     }
597
598
599
600 =head1 DESCRIPTION
601
602
603
604 This moduule lets you use HTTP authentication with
605
606 L<Catalyst::Plugin::Authentication>. Both basic and digest authentication
607
608 are currently supported.
609
610
611
612 =head1 METHODS
613
614
615
616 =over 4
617
618
619
620 =item authorization_required
621
622
623
624 Tries to C<authenticate_http>, and if that fails calls
625
626 C<authorization_required_response> and detaches the current action call stack.
627
628
629
630 =item authenticate_http
631
632
633
634 Looks inside C<< $c->request->headers >> and processes the digest and basic
635
636 (badly named) authorization header.
637
638
639
640 =item authorization_required_response
641
642
643
644 Sets C<< $c->response >> to the correct status code, and adds the correct
645
646 header to demand authentication data from the user agent.
647
648
649
650 =back
651
652
653
654 =head1 AUTHORS
655
656
657
658 Yuval Kogman, C<nothingmuch@woobling.org>
659
660
661
662 Jess Robinson
663
664
665
666 Sascha Kiefer C<esskar@cpan.org>
667
668
669
670 =head1 COPYRIGHT & LICENSE
671
672
673
674         Copyright (c) 2005-2006 the aforementioned authors. All rights
675
676         reserved. This program is free software; you can redistribute
677
678         it and/or modify it under the same terms as Perl itself.
679
680
681
682 =cut
683