File Coverage

blib/lib/JSON/WebToken.pm
Criterion Covered Total %
statement 113 114 99.1
branch 43 44 97.7
condition 23 25 92.0
subroutine 22 22 100.0
pod 5 7 71.4
total 206 212 97.1


line stmt bran cond sub pod time code
1             package JSON::WebToken;
2              
3 8     8   162653 use strict;
  8         15  
  8         267  
4 8     8   30 use warnings;
  8         12  
  8         183  
5 8     8   133 use 5.008_001;
  8         28  
  8         342  
6              
7             our $VERSION = '0.09';
8              
9 8     8   3579 use parent 'Exporter';
  8         2226  
  8         33  
10              
11 8     8   373 use Carp qw(croak);
  8         10  
  8         464  
12 8     8   4377 use JSON qw(encode_json decode_json);
  8         82946  
  8         36  
13 8     8   4933 use MIME::Base64 qw(encode_base64 decode_base64);
  8         3996  
  8         633  
14 8     8   3270 use Module::Runtime qw(use_module);
  8         8668  
  8         42  
15              
16 8     8   3326 use JSON::WebToken::Constants;
  8         16  
  8         504  
17 8     8   2784 use JSON::WebToken::Exception;
  8         16  
  8         7754  
18              
19             our @EXPORT = qw(encode_jwt decode_jwt);
20              
21             our $ALGORITHM_MAP = {
22             # for JWS
23             HS256 => 'HMAC',
24             HS384 => 'HMAC',
25             HS512 => 'HMAC',
26             RS256 => 'RSA',
27             RS384 => 'RSA',
28             RS512 => 'RSA',
29             # ES256 => 'EC',
30             # ES384 => 'EC',
31             # ES512 => 'EC',
32             none => 'NONE',
33              
34             # for JWE
35             RSA1_5 => 'RSA',
36             # 'RSA-OAEP' => 'OAEP',
37             # A128KW => '',
38             # A256KW => '',
39             dir => 'NONE',
40             # 'ECDH-ES' => '',
41             # 'ECDH-ES+A128KW' => '',
42             # 'ECDH-ES+A256KW' => '',
43              
44             # for JWK
45             # EC => 'EC',
46             RSA => 'RSA',
47             };
48              
49             #our $ENCRIPTION_ALGORITHM_MAP = {
50             # 'A128CBC+HS256' => 'AES_CBC',
51             # 'A256CBC+HS512' => 'AES_CBC',
52             # A128GCM => '',
53             # A256GCM => '',
54             #};
55              
56             sub encode {
57 20     20 1 441 my ($class, $claims, $secret, $algorithm, $extra_headers) = @_;
58 20 100       67 unless (ref $claims eq 'HASH') {
59 2         9 JSON::WebToken::Exception->throw(
60             code => ERROR_JWT_INVALID_PARAMETER,
61             message => 'Usage: JSON::WebToken->encode(\%claims [, $secret, $algorithm, \%$extra_headers ])',
62             );
63             }
64              
65 18   100     65 $algorithm ||= 'HS256';
66 18   100     77 $extra_headers ||= {};
67              
68 18         55 my $header = {
69             # typ parameter is OPTIONAL ("JWT" or "urn:ietf:params:oauth:token-type:jwt")
70             # typ => 'JWT',
71             alg => $algorithm,
72             %$extra_headers,
73             };
74              
75 18         31 $algorithm = $header->{alg};
76 18 100 100     77 if ($algorithm ne 'none' && !defined $secret) {
77 1         3 JSON::WebToken::Exception->throw(
78             code => ERROR_JWT_MISSING_SECRET,
79             message => 'secret must be specified',
80             );
81             }
82              
83 17         130 my $header_segment = encode_base64url(encode_json $header);
84 17         69 my $claims_segment = encode_base64url(encode_json $claims);
85 17         41 my $signature_input = join '.', $header_segment, $claims_segment;
86              
87 17         47 my $signature = $class->_sign($algorithm, $signature_input, $secret);
88              
89 16         57 return join '.', $signature_input, encode_base64url($signature);
90             }
91              
92             sub encode_jwt {
93 18     18 1 23997 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
94 18         77 __PACKAGE__->encode(@_);
95             }
96              
97             sub decode {
98 20     20 1 1686 my ($class, $jwt, $secret, $verify_signature, $accept_algorithm_none) = @_;
99 20 100       56 unless (defined $jwt) {
100 1         4 JSON::WebToken::Exception->throw(
101             code => ERROR_JWT_INVALID_PARAMETER,
102             message => 'Usage: JSON::WebToken->decode($jwt [, $secret, $verify_signature, $accept_algorithm_none ])',
103             );
104             }
105              
106 19 100       47 $verify_signature = 1 unless defined $verify_signature;
107 19 100 100     94 if ($verify_signature && !defined $secret) {
108 1         5 JSON::WebToken::Exception->throw(
109             code => ERROR_JWT_MISSING_SECRET,
110             message => 'secret must be specified',
111             );
112             }
113              
114 18         72 my $segments = [ split '\.', $jwt ];
115 18 100 100     108 unless (@$segments >= 2 && @$segments <= 4) {
116 2         9 JSON::WebToken::Exception->throw(
117             code => ERROR_JWT_INVALID_SEGMENT_COUNT,
118             message => "Not enough or too many segments by $jwt",
119             );
120             }
121              
122 16         30 my ($header_segment, $claims_segment, $crypto_segment) = @$segments;
123 16         37 my $signature_input = join '.', $header_segment, $claims_segment;
124              
125 16         17 my ($header, $claims, $signature);
126 16         20 eval {
127 16         37 $header = decode_json decode_base64url($header_segment);
128 15         31 $claims = decode_json decode_base64url($claims_segment);
129 15 100 100     103 $signature = decode_base64url($crypto_segment) if $header->{alg} ne 'none' && $verify_signature;
130             };
131 16 100       38 if (my $e = $@) {
132 1         4 JSON::WebToken::Exception->throw(
133             code => ERROR_JWT_INVALID_SEGMENT_ENCODING,
134             message => 'Invalid segment encoding',
135             );
136             }
137              
138 15 100       35 return $claims unless $verify_signature;
139              
140             # https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-37#section-3.6
141 14 100 100     55 if ( $header->{alg} eq 'none' && ! $accept_algorithm_none ) {
142 1         6 JSON::WebToken::Exception->throw(
143             code => ERROR_JWT_UNACCEPTABLE_ALGORITHM,
144             message => 'Algorithm "none" is not acceptable by default',
145             );
146             }
147              
148 13 100       33 if (ref $secret eq 'CODE') {
149 2         4 $secret = $secret->($header, $claims);
150             }
151              
152 13         26 my $algorithm = $header->{alg};
153 13 100 100     41 if ($algorithm eq 'none' and $crypto_segment) {
154 1         6 JSON::WebToken::Exception->throw(
155             code => ERROR_JWT_UNWANTED_SIGNATURE,
156             message => 'Signature must be the empty string when alg is none',
157             );
158             }
159              
160 12 100       33 unless ($class->_verify($algorithm, $signature_input, $secret, $signature)) {
161 1         15 JSON::WebToken::Exception->throw(
162             code => ERROR_JWT_INVALID_SIGNATURE,
163             message => "Invalid signature by $signature",
164             );
165             }
166              
167 11         62 return $claims;
168             }
169              
170             sub decode_jwt {
171 18     18 1 6279 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
172 18         59 __PACKAGE__->decode(@_);
173             }
174              
175             sub add_signing_algorithm {
176 2     2 1 853 my ($class, $algorithm, $signing_class) = @_;
177 2 50 33     11 unless ($algorithm && $signing_class) {
178 0         0 JSON::WebToken::Exception->throw(
179             code => ERROR_JWT_INVALID_PARAMETER,
180             message => 'Usage: JSON::WebToken->add_signing_algorithm($algorithm, $signing_class)',
181             );
182             }
183 2         6 $ALGORITHM_MAP->{$algorithm} = $signing_class;
184             }
185              
186             sub _sign {
187 17     17   25 my ($class, $algorithm, $message, $secret) = @_;
188 17 100       46 return '' if $algorithm eq 'none';
189              
190 14         21 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
191 14         30 $class->_ensure_class_loaded($algorithm)->sign($algorithm, $message, $secret);
192             }
193              
194             sub _verify {
195 12     12   21 my ($class, $algorithm, $message, $secret, $signature) = @_;
196 12 100       35 return 1 if $algorithm eq 'none';
197              
198 11         16 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
199 11         22 $class->_ensure_class_loaded($algorithm)->verify($algorithm, $message, $secret, $signature);
200             }
201              
202             my (%class_loaded, %alg_to_class);
203             sub _ensure_class_loaded {
204 25     25   33 my ($class, $algorithm) = @_;
205 25 100       100 return $alg_to_class{$algorithm} if $alg_to_class{$algorithm};
206              
207 11         22 my $klass = $ALGORITHM_MAP->{$algorithm};
208 11 100       24 unless ($klass) {
209 1         4 JSON::WebToken::Exception->throw(
210             code => ERROR_JWT_NOT_SUPPORTED_SIGNING_ALGORITHM,
211             message => "`$algorithm` is Not supported siging algorithm",
212             );
213             }
214              
215 10 100       35 my $signing_class = $klass =~ s/^\+// ? $klass : "JSON::WebToken::Crypt::$klass";
216 10 100       34 return $signing_class if $class_loaded{$signing_class};
217              
218 6 100       18 use_module $signing_class unless $class->_is_inner_package($signing_class);
219              
220 6         37 $class_loaded{$signing_class} = 1;
221 6         12 $alg_to_class{$algorithm} = $signing_class;
222              
223 6         40 return $signing_class;
224             }
225              
226             sub _is_inner_package {
227 6     6   7 my ($class, $klass) = @_;
228 8     8   62 no strict 'refs';
  8         13  
  8         1374  
229 6 100       9 %{ "$klass\::" } ? 1 : 0;
  6         67  
230             }
231              
232             ####################################################
233             # Taken from newer MIME::Base64
234             # In order to support older version of MIME::Base64
235             ####################################################
236             sub encode_base64url {
237 50     50 0 196 my $e = encode_base64(shift, "");
238 50         163 $e =~ s/=+\z//;
239 50         70 $e =~ tr[+/][-_];
240 50         133 return $e;
241             }
242              
243             sub decode_base64url {
244 42     42 0 48 my $s = shift;
245 42         47 $s =~ tr[-_][+/];
246 42         156 $s .= '=' while length($s) % 4;
247 42         198 return decode_base64($s);
248             }
249              
250             1;
251             __END__