File Coverage

blib/lib/JSON/WebToken.pm
Criterion Covered Total %
statement 111 112 99.1
branch 41 42 97.6
condition 20 22 90.9
subroutine 22 22 100.0
pod 5 7 71.4
total 199 205 97.0


line stmt bran cond sub pod time code
1             package JSON::WebToken;
2              
3 9     9   349574 use strict;
  9         19  
  9         334  
4 9     9   49 use warnings;
  9         22  
  9         302  
5 9     9   1357 use 5.008_001;
  9         39  
  9         3527  
6              
7             our $VERSION = '0.08';
8              
9 9     9   8434 use parent 'Exporter';
  9         3154  
  9         48  
10              
11 9     9   628 use Carp qw(croak);
  9         19  
  9         642  
12 9     9   8080 use JSON qw(encode_json decode_json);
  9         138855  
  9         68  
13 9     9   11188 use MIME::Base64 qw(encode_base64 decode_base64);
  9         7658  
  9         792  
14 9     9   7633 use Module::Runtime qw(use_module);
  9         13617  
  9         68  
15              
16 9     9   5814 use JSON::WebToken::Constants;
  9         23  
  9         700  
17 9     9   5772 use JSON::WebToken::Exception;
  9         27  
  9         12530  
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 24     24 1 13904 my ($class, $claims, $secret, $algorithm, $extra_headers) = @_;
58 24 100       136 unless (ref $claims eq 'HASH') {
59 2         16 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 22   100     87 $algorithm ||= 'HS256';
66 22   100     134 $extra_headers ||= {};
67              
68 22         82 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 22         53 $algorithm = $header->{alg};
76 22 100 100     128 if ($algorithm ne 'none' && !defined $secret) {
77 1         6 JSON::WebToken::Exception->throw(
78             code => ERROR_JWT_MISSING_SECRET,
79             message => 'secret must be specified',
80             );
81             }
82              
83 21         220 my $header_segment = encode_base64url(encode_json $header);
84 21         113 my $claims_segment = encode_base64url(encode_json $claims);
85 21         66 my $signature_input = join '.', $header_segment, $claims_segment;
86              
87 21         79 my $signature = $class->_sign($algorithm, $signature_input, $secret);
88              
89 20         94 return join '.', $signature_input, encode_base64url($signature);
90             }
91              
92             sub encode_jwt {
93 21     21 1 38357 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
94 21         112 __PACKAGE__->encode(@_);
95             }
96              
97             sub decode {
98 24     24 1 2716 my ($class, $jwt, $secret, $is_verify) = @_;
99 24 100       80 unless (defined $jwt) {
100 1         34 JSON::WebToken::Exception->throw(
101             code => ERROR_JWT_INVALID_PARAMETER,
102             message => 'Usage: JSON::WebToken->decode($jwt [, $secret, $is_verify ])',
103             );
104             }
105              
106 23 100       73 $is_verify = 1 unless defined $is_verify;
107 23 100 100     143 if ($is_verify && !defined $secret) {
108 1         8 JSON::WebToken::Exception->throw(
109             code => ERROR_JWT_MISSING_SECRET,
110             message => 'secret must be specified',
111             );
112             }
113              
114 22         114 my $segments = [ split '\.', $jwt ];
115 22 100 100     136 unless (@$segments >= 2 && @$segments <= 4) {
116 2         14 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 20         45 my ($header_segment, $claims_segment, $crypto_segment) = @$segments;
123 20         58 my $signature_input = join '.', $header_segment, $claims_segment;
124              
125 20         158 my ($header, $claims, $signature);
126 20         35 eval {
127 20         62 $header = decode_json decode_base64url($header_segment);
128 19         55 $claims = decode_json decode_base64url($claims_segment);
129 19 100 100     153 $signature = decode_base64url($crypto_segment) if $header->{alg} ne 'none' && $is_verify;
130             };
131 20 100       70 if (my $e = $@) {
132 1         5 JSON::WebToken::Exception->throw(
133             code => ERROR_JWT_INVALID_SEGMENT_ENCODING,
134             message => 'Invalid segment encoding',
135             );
136             }
137              
138 19 100       54 return $claims unless $is_verify;
139              
140 18 100       53 if (ref $secret eq 'CODE') {
141 2         6 $secret = $secret->($header, $claims);
142             }
143              
144 18         46 my $algorithm = $header->{alg};
145 18 100 100     75 if ($algorithm eq 'none' and $crypto_segment) {
146 1         6 JSON::WebToken::Exception->throw(
147             code => ERROR_JWT_UNWANTED_SIGNATURE,
148             message => 'Signature must be the empty string when alg is none',
149             );
150             }
151              
152 17 100       63 unless ($class->_verify($algorithm, $signature_input, $secret, $signature)) {
153 1         11 JSON::WebToken::Exception->throw(
154             code => ERROR_JWT_INVALID_SIGNATURE,
155             message => "Invalid signature by $signature",
156             );
157             }
158              
159 16         120 return $claims;
160             }
161              
162             sub decode_jwt {
163 21     21 1 7832 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
164 21         90 __PACKAGE__->decode(@_);
165             }
166              
167             sub add_signing_algorithm {
168 2     2 1 1440 my ($class, $algorithm, $signing_class) = @_;
169 2 50 33     14 unless ($algorithm && $signing_class) {
170 0         0 JSON::WebToken::Exception->throw(
171             code => ERROR_JWT_INVALID_PARAMETER,
172             message => 'Usage: JSON::WebToken->add_signing_algorithm($algorithm, $signing_class)',
173             );
174             }
175 2         8 $ALGORITHM_MAP->{$algorithm} = $signing_class;
176             }
177              
178             sub _sign {
179 21     21   38 my ($class, $algorithm, $message, $secret) = @_;
180 21 100       75 return '' if $algorithm eq 'none';
181              
182 18         35 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
183 18         58 $class->_ensure_class_loaded($algorithm)->sign($algorithm, $message, $secret);
184             }
185              
186             sub _verify {
187 17     17   36 my ($class, $algorithm, $message, $secret, $signature) = @_;
188 17 100       52 return 1 if $algorithm eq 'none';
189              
190 15         29 local $Carp::CarpLevel = $Carp::CarpLevel + 1;
191 15         45 $class->_ensure_class_loaded($algorithm)->verify($algorithm, $message, $secret, $signature);
192             }
193              
194             my (%class_loaded, %alg_to_class);
195             sub _ensure_class_loaded {
196 33     33   56 my ($class, $algorithm) = @_;
197 33 100       170 return $alg_to_class{$algorithm} if $alg_to_class{$algorithm};
198              
199 17         40 my $klass = $ALGORITHM_MAP->{$algorithm};
200 17 100       46 unless ($klass) {
201 1         8 JSON::WebToken::Exception->throw(
202             code => ERROR_JWT_NOT_SUPPORTED_SIGNING_ALGORITHM,
203             message => "`$algorithm` is Not supported siging algorithm",
204             );
205             }
206              
207 16 100       71 my $signing_class = $klass =~ s/^\+// ? $klass : "JSON::WebToken::Crypt::$klass";
208 16 100       79 return $signing_class if $class_loaded{$signing_class};
209              
210 8 100       30 use_module $signing_class unless $class->_is_inner_package($signing_class);
211              
212 8         61 $class_loaded{$signing_class} = 1;
213 8         23 $alg_to_class{$algorithm} = $signing_class;
214              
215 8         64 return $signing_class;
216             }
217              
218             sub _is_inner_package {
219 8     8   14 my ($class, $klass) = @_;
220 9     9   63 no strict 'refs';
  9         28  
  9         2448  
221 8 100       15 %{ "$klass\::" } ? 1 : 0;
  8         119  
222             }
223              
224             ####################################################
225             # Taken from newer MIME::Base64
226             # In order to support older version of MIME::Base64
227             ####################################################
228             sub encode_base64url {
229 62     62 0 307 my $e = encode_base64(shift, "");
230 62         255 $e =~ s/=+\z//;
231 62         114 $e =~ tr[+/][-_];
232 62         272 return $e;
233             }
234              
235             sub decode_base64url {
236 54     54 0 81 my $s = shift;
237 54         87 $s =~ tr[-_][+/];
238 54         232 $s .= '=' while length($s) % 4;
239 54         376 return decode_base64($s);
240             }
241              
242             1;
243             __END__