File Coverage

blib/lib/Authen/HTTP/Signature/Parser.pm
Criterion Covered Total %
statement 67 67 100.0
branch 27 30 90.0
condition 2 3 66.6
subroutine 9 9 100.0
pod 1 1 100.0
total 106 110 96.3


line stmt bran cond sub pod time code
1             package Authen::HTTP::Signature::Parser;
2              
3 3     3   50441 use strict;
  3         7  
  3         77  
4 3     3   16 use warnings;
  3         5  
  3         76  
5              
6 3     3   783 use Moo;
  3         14827  
  3         20  
7 3     3   2728 use Authen::HTTP::Signature;
  3         6  
  3         89  
8 3     3   17 use HTTP::Date qw(str2time);
  3         5  
  3         164  
9 3     3   16 use Scalar::Util qw(blessed);
  3         6  
  3         162  
10 3     3   13 use Carp qw(confess);
  3         6  
  3         3420  
11              
12             =head1 NAME
13              
14             Authen::HTTP::Signature::Parser - Parse HTTP signature headers
15              
16             =cut
17              
18             our $VERSION = '0.03';
19              
20             =head1 PURPOSE
21              
22             This class parses a HTTP signature 'Authorization' header (if one exists) from a request
23             object and populates attributes in a L object.
24              
25             =head1 ATTRIBUTES
26              
27             =over
28              
29             =item request
30              
31             The request to be parsed.
32              
33             =back
34              
35             =cut
36              
37             has 'request' => (
38             is => 'rw',
39             isa => sub { confess "'request' must be blessed" unless blessed($_[0]) },
40             predicate => 'has_request',
41             );
42              
43             around BUILDARGS => sub {
44             my $orig = shift;
45             my $class = shift;
46              
47             if ( @_ == 1 ) {
48             unshift @_, "request";
49             }
50              
51             return $class->$orig(@_);
52             };
53              
54             =over
55              
56             =item get_header
57              
58             A call back to get a header from C.
59              
60             =back
61              
62             =cut
63              
64             has 'get_header' => (
65             is => 'rw',
66             isa => sub { die "'get_header' expects a CODE ref\n" unless ref($_[0]) eq "CODE" },
67             predicate => 'has_get_header',
68             default => sub {
69             sub {
70             confess "Didn't get 2 arguments" unless @_ == 2;
71             my $request = shift;
72             confess "'request' isn't blessed" unless blessed $request;
73             my $name = shift;
74              
75             if( $name eq 'request-line' ) {
76             sprintf("%s %s",
77             $request->uri->path_query,
78             $request->protocol);
79             } elsif( $name eq '(request-target)' ) {
80             sprintf("%s %s",
81             lc($request->method),
82             $request->uri->path_query);
83             } else {
84             $request->header($name);
85             }
86             };
87             },
88             lazy => 1,
89             );
90              
91             =over
92              
93             =item skew
94              
95             Defaults to 300 seconds in either direction from your clock. If the Date header data is outside of this range,
96             the request is considered invalid.
97              
98             Set this value to 0 to disable skew checks for testing purposes.
99              
100             =back
101              
102             =cut
103              
104             has 'skew' => (
105             is => 'rw',
106             isa => sub { die "$_[0] isn't an integer" unless $_[0] =~ /[0-9]+/ },
107             default => sub { 300 },
108             );
109              
110              
111             =head1 METHOD
112              
113             Errors are fatal.
114              
115             =over
116              
117             =item parse()
118              
119             This method parses signature header components.
120              
121             =back
122              
123             =cut
124              
125             sub parse {
126 15     15 1 158 my $self = shift;
127 15   66     242 my $request = shift || $self->request;
128              
129 15 50       1723 confess "There was no request to parse!" unless $request;
130              
131 15         258 my $sig_str = $self->get_header->($request, 'authorization');
132 15 100       751 confess 'No authorization header value was returned!' unless $sig_str;
133              
134 14         39 $self->_check_skew($request);
135              
136 13         53 my ( $sig_text ) = $sig_str =~ /^(Signature).*$/;
137 13 100       151 confess "does not match required string 'Signature'" unless $sig_text;
138              
139 12         63 my ( $b64_str ) = $sig_str =~ /^Signature.*signature="(.*?)".*$/;
140 12 100       150 confess "No signature data found!" unless $b64_str;
141              
142 11         54 my ( $key_id ) = $sig_str =~ /^Signature.*(keyId=".*?").*$/;
143 11         65 my ( $algo ) = $sig_str =~ /^Signature.*(algorithm=".*?").*$/;
144 11         50 my ( $ext ) = $sig_str =~ /^Signature.*(ext=".*?").*$/;
145 11         49 my ( $hdrs ) = $sig_str =~ /^Signature.*(headers=".*?").*$/;
146              
147 11         53 $key_id =~ s/^keyId="(.*)"$/$1/;
148 11         40 $algo =~ s/^algorithm="(.*)"$/$1/;
149 11 100       33 $ext =~ s/^ext="(.*)"/$1/ if $ext;
150              
151 11 100       136 confess "No key id found!" unless $key_id;
152 10 100       133 confess "No algorithm found" unless $algo;
153              
154 9         15 my @headers;
155 9 100       23 if ( $hdrs ) {
156 7         31 $hdrs =~ s/^headers="(.*)"$/$1/;
157 7         28 @headers = split / /, $hdrs;
158             }
159              
160 9 100       28 push @headers, "date" unless @headers;
161              
162             # die on duplicate headers
163 9         15 my %h;
164 9         19 foreach my $hdr ( @headers ) {
165 26 100       61 if ( exists $h{$hdr} ) {
166 1         122 confess "Duplicate header '$hdr' found in signature header parameter. Aborting.";
167             }
168 25         57 $h{$hdr}++;
169             }
170              
171             # normalize headers to lower-case
172 8         17 @headers = map { lc } @headers;
  23         62  
173              
174             my $ss = join "\n", map {
175 8 100       22 if( $self->get_header->($request, $_) ) {
  23         943  
176 22         1426 sprintf("%s: %s", $_, $self->get_header->($request, $_) );
177             } else {
178 1         171 confess "Couldn't get header value for $_\n";
179             } } @headers;
180              
181 7         412 return Authen::HTTP::Signature->new(
182             key_id => $key_id,
183             headers => \@headers,
184             signing_string => $ss,
185             algorithm => $algo,
186             extensions => $ext,
187             signature => $b64_str,
188             request => $request,
189             );
190             }
191              
192             sub _check_skew {
193 14     14   21 my $self = shift;
194              
195 14 100       244 if ( $self->skew ) {
196 12         1748 my $request = shift;
197 12 50       69 confess "No request found" unless $request;
198 12         247 my $header_time = str2time( $self->get_header->($request, 'date') );
199 12 50       946 confess "No Date header was returned (or could be parsed)" unless $header_time;
200              
201 12         48 my $diff = abs(time - $header_time);
202 12 100       258 if ( $diff >= $self->skew ) {
203 1         29 confess "Request is outside of clock skew tolerance: $diff seconds computed, " . $self->skew . " seconds allowed.\n";
204             }
205             }
206              
207 13         98 return 1;
208              
209             }
210              
211              
212             =head1 SEE ALSO
213              
214             L
215              
216             =cut
217              
218             1;