File Coverage

blib/lib/Plack/Middleware/Auth/Digest.pm
Criterion Covered Total %
statement 27 79 34.1
branch 2 18 11.1
condition 1 18 5.5
subroutine 9 19 47.3
pod 2 11 18.1
total 41 145 28.2


line stmt bran cond sub pod time code
1             package Plack::Middleware::Auth::Digest;
2 2     2   957 use 5.008001;
  2         9  
  2         92  
3 2     2   13 use strict;
  2         3  
  2         79  
4 2     2   11 use warnings;
  2         4  
  2         101  
5 2     2   1010 use parent qw/Plack::Middleware/;
  2         371  
  2         13  
6 2     2   23158 use Plack::Util::Accessor qw/realm authenticator password_hashed secret nonce_ttl/;
  2         5  
  2         14  
7              
8 2     2   2174 use MIME::Base64 ();
  2         1789  
  2         44  
9 2     2   12 use Digest::MD5 ();
  2         4  
  2         29  
10 2     2   1913 use Digest::HMAC_SHA1 ();
  2         50067  
  2         2682  
11              
12             our $VERSION = '0.04';
13              
14             sub hash {
15 0     0 0 0 Digest::MD5::md5_hex(join ":", @_);
16             }
17              
18             sub prepare_app {
19 2     2 1 8757 my $self = shift;
20              
21 2 50 33     15 if ($self->authenticator && ref $self->authenticator ne 'CODE') {
22 0         0 die 'authenticator should be a code reference';
23             }
24              
25 2 50       146 unless ($self->secret) {
26 0           die "Auth::Digest secret key is not set.";
27             }
28             }
29              
30             sub call {
31 0     0 1   my ($self, $env) = @_;
32              
33 0 0         my $auth = $env->{HTTP_AUTHORIZATION} or return $self->unauthorized;
34              
35 0 0         if ($auth =~ /^Digest (.*)/) {
36 0   0       my $auth = $self->parse_challenge($1) || {};
37 0           $auth->{method} = $env->{REQUEST_METHOD};
38              
39 0 0         if ($auth->{uri} ne $env->{REQUEST_URI}) {
40 0           return [ 400, ['Content-Type', 'text/plain'], [ "Bad Request" ] ];
41             }
42              
43 0           my $password = $self->authenticator->($auth->{username}, $env);
44 0 0 0       if ( defined $password
      0        
45             && $self->valid_nonce($auth)
46             && $self->digest($password, $auth) eq $auth->{response}) {
47              
48 0 0         if ($self->stale_nonce($auth)) {
49 0           return $self->unauthorized(stale => "true");
50             }
51              
52 0           $env->{REMOTE_USER} = $auth->{username};
53 0           return $self->app->($env);
54             }
55             }
56              
57 0           return $self->unauthorized;
58             }
59              
60             sub parse_challenge {
61 0     0 0   my($self, $header) = @_;
62              
63 0           my $auth;
64 0           while ($header =~ /(\w+)\=("[^\"]+"|[^,]+)/g ) {
65 0           $auth->{$1} = dequote($2);
66             }
67              
68 0           return $auth;
69             }
70              
71             sub dequote {
72 0     0 0   my $s = shift;
73 0           $s =~ s/^"(.*)"$/$1/;
74 0           $s =~ s/\\(.)/$1/g;
75 0           $s;
76             }
77              
78             sub digest {
79 0     0 0   my($self, $password, $auth) = @_;
80              
81 0 0         my $hashed = $self->password_hashed
82             ? $password : hash($auth->{username}, $auth->{realm}, $password);
83              
84 0           return hash($hashed, @{$auth}{qw(nonce nc cnonce qop)}, hash("$auth->{method}:$auth->{uri}"));
  0            
85             }
86              
87             sub unauthorized {
88 0     0 0   my $self = shift;
89 0           my %params = @_;
90              
91 0           my $body = '401 Authorization required';
92 0   0       my $realm = $self->realm || "restricted area";
93 0           my $nonce = $self->generate_nonce(time);
94 0           my $algorithm = 'MD5';
95 0           my $qop = 'auth';
96              
97 0           my $challenge = qq|Digest realm="$realm", nonce="$nonce", algorithm=$algorithm, qop="$qop"|;
98 0 0         $challenge .= qq(, stale=true) if $params{stale};
99              
100             return [
101 0           401,
102             [
103             'Content-Type' => 'text/plain',
104             'Content-Length' => length $body,
105             'WWW-Authenticate' => $challenge,
106             ],
107             [ $body ],
108             ];
109             }
110              
111             sub valid_nonce {
112 0     0 0   my($self, $auth) = @_;
113              
114 0           my($time, $digest) = split / /, MIME::Base64::decode_base64($auth->{nonce});
115 0           $auth->{_nonce_time} = $time; # cache for stale check
116              
117 0   0       return $time && $digest && $digest eq $self->hmac($time);
118             }
119              
120             sub stale_nonce {
121 0     0 0   my($self, $auth) = @_;
122              
123 0   0       $auth->{_nonce_time} < time - ($self->nonce_ttl || 60);
124             }
125              
126             sub generate_nonce {
127 0     0 0   my($self, $time) = @_;
128              
129 0           my $nonce = MIME::Base64::encode_base64(join " ", $time, $self->hmac($time));
130 0           chomp $nonce;
131              
132 0           return $nonce;
133             }
134              
135             sub hmac {
136 0     0 0   my($self, $time) = @_;
137 0           Digest::HMAC_SHA1::hmac_sha1_hex($time, $self->secret);
138             }
139              
140             1;
141             __END__