File Coverage

blib/lib/Mojo/JWT.pm
Criterion Covered Total %
statement 101 115 87.8
branch 43 68 63.2
condition 16 21 76.1
subroutine 17 18 94.4
pod 7 7 100.0
total 184 229 80.3


line stmt bran cond sub pod time code
1             package Mojo::JWT;
2              
3 1     1   376627 use Mojo::Base -base;
  1         3  
  1         10  
4              
5             our $VERSION = '1.01';
6             $VERSION = eval $VERSION;
7              
8 1     1   305 use Scalar::Util ();
  1         2  
  1         14  
9 1     1   9 use List::Util ();
  1         1  
  1         35  
10 1     1   4 use Mojo::JSON ();
  1         2  
  1         16  
11              
12 1     1   4 use CryptX;
  1         1  
  1         16  
13 1     1   4 use Crypt::Misc ();
  1         2  
  1         9  
14              
15 1     1   4 use Carp ();
  1         2  
  1         2245  
16              
17             my $isa = sub { Scalar::Util::blessed $_[0] && $_[0]->isa($_[1]) };
18              
19             has header => sub { {} };
20             has algorithm => 'HS256';
21             has [qw/allow_none set_iat/] => 0;
22             has claims => sub { {} };
23             has jwks => sub { [] };
24             has [qw/expires not_before/];
25             has [qw/public secret/] => '';
26              
27             my $re_hs = qr/^HS(\d+)$/;
28             my $re_rs = qr/^RS(\d+)$/;
29              
30             sub add_jwkset {
31 2     2 1 1495 my ($self, $jwkset) = @_;
32 2 50       7 if (ref $jwkset eq 'HASH') {
33 2         3 push @{ $self->jwks }, @{ $jwkset->{keys} };
  2         5  
  2         5  
34             }
35 2 50       5 if (ref $jwkset eq 'ARRAY') {
36 0         0 push @{ $self->jwks }, @{ $jwkset };
  0         0  
  0         0  
37             }
38 2         3 return $self;
39             }
40              
41             sub decode {
42 17     17 1 5802 my ($self, $token, $peek) = @_;
43 17         39 $self->{token} = $token;
44              
45             # reset
46 17         61 $self->algorithm(undef);
47 17         146 delete $self->{$_} for qw/claims expires not_before header/;
48              
49 17         77 my ($hstring, $cstring, $signature) = split /\./, $token;
50 17         123 my $header = Mojo::JSON::decode_json Crypt::Misc::decode_b64u($hstring);
51 17         193 my $claims = Mojo::JSON::decode_json Crypt::Misc::decode_b64u($cstring);
52 17         173 $signature = Crypt::Misc::decode_b64u $signature;
53              
54             # typ header is only recommended and is ignored
55             # https://tools.ietf.org/html/rfc7519#section-5.1
56 17         34 delete $header->{typ};
57             Carp::croak 'Required header field "alg" not specified'
58 17 50       44 unless my $algo = $self->algorithm(delete $header->{alg})->algorithm;
59 17         203 $self->header($header);
60              
61 17         114 $self->_try_jwks($algo, $header);
62              
63 17 50       59 $self->$peek($claims) if $peek;
64              
65             # check signature
66 17         31 my $payload = "$hstring.$cstring";
67 17 100       194 if ($algo eq 'none') {
    100          
    50          
68 1 50       3 Carp::croak 'Algorithm "none" is prohibited'
69             unless $self->allow_none;
70             } elsif ($algo =~ $re_rs) {
71 8 100       29 Carp::croak 'Failed RS validation'
72             unless $self->verify_rsa($1, $payload, $signature);
73             } elsif ($algo =~ $re_hs) {
74 8 100       28 Carp::croak 'Failed HS validation'
75             unless $signature eq $self->sign_hmac($1, $payload);
76             } else {
77 0         0 Carp::croak 'Unsupported signing algorithm';
78             }
79              
80             # check timing
81 12         44 my $now = $self->now;
82 12 50       44 if (defined(my $exp = $claims->{exp})) {
83 0 0       0 Carp::croak 'JWT has expired' if $now > $exp;
84 0         0 $self->expires($exp);
85             }
86 12 50       29 if (defined(my $nbf = $claims->{nbf})) {
87 0 0       0 Carp::croak 'JWT is not yet valid' if $now < $nbf;
88 0         0 $self->not_before($nbf);
89             }
90              
91 12         38 return $self->claims($claims)->claims;
92             }
93              
94             sub _try_jwks {
95 17     17   36 my ($self, $algo, $header) = @_;
96 17 100 100     21 return unless @{$self->jwks} && $header->{kid};
  17         51  
97              
98             # Check we have the JWK for this JWT
99 4 50   4   38 my $jwk = List::Util::first { exists $header->{kid} && $_->{kid} eq $header->{kid} } @{$self->jwks};
  4         33  
  4         6  
100 4 100       16 return unless $jwk;
101              
102 3 50       55 if ($algo =~ $re_rs) {
    0          
103 3         17 require Crypt::PK::RSA;
104 3         13 my $pubkey = Crypt::PK::RSA->new($jwk);
105 3         774 $self->public($pubkey);
106             } elsif ($algo =~ $re_hs) {
107             $self->secret( Crypt::Misc::decode_b64u $jwk->{k} )
108 0         0 }
109             }
110              
111             sub encode {
112 12     12 1 2634505 my $self = shift;
113 12         39 delete $self->{token};
114              
115 12         44 my $claims = $self->claims;
116 12 100       101 if ($self->set_iat) { $claims->{iat} = $self->now }
  1         7  
117 12 50       113 if (defined(my $exp = $self->expires)) { $claims->{exp} = $exp }
  0         0  
118 12 50       93 if (defined(my $nbf = $self->not_before)) { $claims->{nbf} = $nbf }
  0         0  
119              
120 12         55 my $header = { %{ $self->header }, typ => 'JWT', alg => $self->algorithm };
  12         38  
121 12         174 my $hstring = Crypt::Misc::encode_b64u Mojo::JSON::encode_json($header);
122 12         220 my $cstring = Crypt::Misc::encode_b64u Mojo::JSON::encode_json($claims);
123 12         61 my $payload = "$hstring.$cstring";
124 12         18 my $signature;
125 12         22 my $algo = $self->algorithm;
126 12 100       187 if ($algo eq 'none') {
    100          
    50          
127 1         1 $signature = '';
128             } elsif ($algo =~ $re_rs) {
129 3         18 $signature = $self->sign_rsa($1, $payload);
130             } elsif ($algo =~ $re_hs) {
131 8         25 $signature = $self->sign_hmac($1, $payload);
132             } else {
133 0         0 Carp::croak 'Unknown algorithm';
134             }
135              
136 11         152 return $self->{token} = "$payload." . Crypt::Misc::encode_b64u $signature;
137             }
138              
139 11     11   24 sub now { time }
140              
141             sub sign_hmac {
142 16     16 1 66 my ($self, $size, $payload) = @_;
143 16 100       36 Carp::croak 'symmetric secret not specified' unless my $secret = $self->secret;
144 15 100 66     298 Carp::croak 'Unsupported HS signing algorithm' unless $size == 256 || $size == 384 || $size == 512;
      100        
145 14         773 require Crypt::Mac::HMAC;
146 14         3427 return Crypt::Mac::HMAC::hmac("SHA$size", $secret, $payload);
147             }
148              
149             sub sign_rsa {
150 3     3 1 21 my ($self, $size, $payload) = @_;
151 3 50 66     54 Carp::croak 'Unsupported RS signing algorithm' unless $size == 256 || $size == 384 || $size == 512;
      66        
152 3 50       12 Carp::croak 'secret key not specified' unless my $secret = $self->secret;
153 3         46 my $crypt = _inflate_rsa_key($secret);
154 3         20062 return $crypt->sign_message($payload, "SHA$size", 'v1.5');
155             }
156              
157 0     0 1 0 sub token { shift->{token} }
158              
159             sub verify_rsa {
160 8     8 1 53 my ($self, $size, $payload, $signature) = @_;
161 8 50 66     51 Carp::croak 'Unsupported RS verification algorithm' unless $size == 256 || $size == 384 || $size == 512;
      66        
162 8 100       21 Carp::croak 'public key not specified' unless my $public = $self->public;
163 6         44 my $crypt = _inflate_rsa_key($public);
164 6         2392 return $crypt->verify_message($signature, $payload, "SHA$size", 'v1.5');
165             }
166              
167             sub _inflate_rsa_key {
168 9     9   23 my ($key) = @_;
169 9         104 require Crypt::PK::RSA;
170 9 100       29 return $key if $key->$isa('Crypt::PK::RSA');
171 4 50       11 if ($key->$isa('Crypt::OpenSSL::RSA')) {
172 0 0       0 $key = $key->is_private ? $key->get_private_key_string : $key->get_public_key_string;
173             }
174 4         29 return Crypt::PK::RSA->new(\$key);
175             }
176              
177             1;
178              
179             =head1 NAME
180              
181             Mojo::JWT - JSON Web Token the Mojo way
182              
183             =head1 SYNOPSIS
184              
185             my $jwt = Mojo::JWT->new(claims => {...}, secret => 's3cr3t')->encode;
186             my $claims = Mojo::JWT->new(secret => 's3cr3t')->decode($jwt);
187              
188             =head1 DESCRIPTION
189              
190             JSON Web Token is described in L.
191             L implements that standard with an API that should feel familiar to L users (though of course it is useful elsewhere).
192             Indeed, JWT is much like L except that the result is a url-safe text string rather than a cookie.
193              
194             In JWT, the primary payload is called the C, and a few claims are reserved, as seen in the IETF document.
195             The header and the claims are signed when stringified to guard against tampering.
196             Note that while signed, the data is not encrypted, so don't use it to send secrets over clear channels.
197              
198             =head1 ATTRIBUTES
199              
200             L inherits all of the attributes from L and implements the following new ones.
201              
202             =head2 algorithm
203              
204             The algorithm to be used to sign a JWT during encoding or else the algorithm that was used for the most recent decoding.
205             Defaults to C until a decode is performed.
206              
207             C is an acceptable encoding algorithm, however for it to be used to decode, L must be set.
208              
209             =head2 allow_none
210              
211             To prevent spoofing attacks, C must be explicitly set to a true value otherwise decoding a JWT which specifies the C algorithm will result in an exception.
212             The default is of course false.
213              
214             =head2 claims
215              
216             The payload to be encoded or else the claims from the most recent decoding.
217             This must be a hash reference, array references are not allowed as the top-level JWT claims.
218              
219             =head2 expires
220              
221             The epoch time value after which the JWT value should not be considered valid.
222             This value (if set and not undefined) will be used as the C key in the claims or was extracted from the claims during the most recent decoding.
223              
224             =head2 header
225              
226             You may set your own headers when encoding the JWT bypassing a hash reference to the L attribute. Please note that there are two default headers set. B is set to the value of L or 'HS256' and B is set to 'JWT'. These cannot be overridden.
227              
228             =head2 not_before
229              
230             The epoch time value before which the JWT value should not be considered valid.
231             This value (if set and not undefined) will be used as the C key in the claims or was extracted from the claims during the most recent decoding.
232              
233             =head2 public
234              
235             The public key to be used in decoding an asymmetrically signed JWT (eg. RSA).
236             This can be any public key in a string format accepted by L or a L object (if used a L object will be converted).
237              
238             =head2 secret
239              
240             The symmetric secret (eg. HMAC) or else the private key used in encoding an asymmetrically signed JWT (eg. RSA).
241             Symmetric secrets should be a string.
242             A private key can be in a string format accepted by L or a L object (if used a L object will be converted).
243              
244             =head2 set_iat
245              
246             If true (false by default), then the C claim will be set to the value of L during L.
247              
248             =head2 jwks
249              
250             An arrayref of JWK objects used by C to verify the input token when matching with the JWTs C field.
251              
252             my $jwks = Mojo::UserAgent->new->get('https://example.com/oidc/jwks.json')->result->json('/keys');
253             my $jwt = Mojo::JWT->new(jwks => $jwks);
254             $jwk->decode($token);
255              
256             =head1 METHODS
257              
258             L inherits all of the methods from L and implements the following new ones.
259              
260             =head2 add_jwkset
261              
262             my $jwkset = Mojo::UserAgent->new->get('https://example.com/oidc/jwks.json')->result->json;
263             my $jwt = Mojo::JWT->new->add_jwkset($jwksset);
264             $jwk->decode($token);
265              
266             Helper for appending a jwkset to the L.
267             Accepts a hashref with a C field that is an arrayref of jwks and also an arrayref directly.
268             Appends the JWKs to L.
269             Returns the instance.
270              
271             =head2 decode
272              
273             my $claims = $jwt->decode($token);
274              
275             my $peek = sub { my ($jwt, $claims) = @_; ... };
276             my $claims = $jwt->decode($token, $peek);
277              
278             Decode and parse a JSON Web Token string and return the claims hashref.
279             Calling this function immediately sets the L to the passed in token.
280             It also sets L to C and unsets L, L and L.
281             These values are then set as part of the parsing process.
282              
283             Parsing occurs as follows
284              
285             =over
286              
287             =item *
288              
289             The L is extracted from the header and set, if not present or permissible an exception is thrown
290              
291             =item *
292              
293             Any JWKs in C are checked against the headers and if one is found then it is set in L or L as appropriate to the L
294              
295             =item *
296              
297             If a C<$peek> callback is provided, it is called with the instance and claims as arguments
298              
299             =item *
300              
301             The signature is verified or an exception is thrown
302              
303             =item *
304              
305             The timing claims (L and L), if present, are evaluated, failures result in exceptions. On success the values are set in the relevant attributes
306              
307             =item *
308              
309             The L attribute is set and the claims are returned.
310              
311             =back
312              
313             Note that when the C<$peek> callback is invoked, the claims have not yet been verified.
314             This callback is most likely to be used to inspect the C or issuer claim to determine a secret or key for decoding.
315             The return value is ignored, changes should be made to the instances attributes directly.
316             Since the L has already been parsed, it is available via the instance attribute as well.
317              
318             =head2 encode
319              
320             my $token = $jwt->encode;
321              
322             Encode the data expressed in the instance attributes: L, L, L, L.
323             Note that if the timing attributes are given, they override existing keys in the L.
324             Calling C immediately clears the L and upon completion sets it to the result as well as returning it.
325              
326             Note also that due to Perl's hash randomization, repeated encoding is not guaranteed to result in the same encoded string.
327             However any encoded string will survive an encode/decode roundtrip.
328              
329             =head2 header
330              
331             my $header = $jwt->header;
332              
333             Returns a hash reference representing the JWT header, constructed from instance attributes (see L).
334              
335             =head2 now
336              
337             my $time = $jwt->now;
338              
339             Returns the current time, currently implemented as the core C
340              
341             =head2 sign_hmac
342              
343             my $signature = $jwt->sign_hmac($size, $payload);
344              
345             Returns the HMAC SHA signature for the given size and payload.
346             The L attribute is used as the symmetric key.
347             The result is not yet base64 encoded.
348             This method is provided mostly for the purposes of subclassing.
349              
350             =head2 sign_rsa
351              
352             my $signature = $jwt->sign_rsa($size, $payload);
353              
354             Returns the RSA signature for the given size and payload.
355             The L attribute is used as the private key.
356             The result is not yet base64 encoded.
357             This method is provided mostly for the purposes of subclassing.
358              
359             =head2 token
360              
361             The most recently encoded or decoded token.
362             Note that any attribute modifications are not taken into account until L is called again.
363              
364             =head2 verify_rsa
365              
366             my $bool = $jwt->verify_rsa($size, $payload, $signature);
367              
368             Returns true if the given RSA size algorithm validates the given payload and signature.
369             The L attribute is used as the public key.
370             This method is provided mostly for the purposes of subclassing.
371              
372             =head1 SEE ALSO
373              
374             =over
375              
376             =item L
377              
378             =item L
379              
380             =item L
381              
382             =back
383              
384             =head1 SOURCE REPOSITORY
385              
386             L
387              
388             =head1 AUTHOR
389              
390             Joel Berger, Ejoel.a.berger@gmail.comE
391              
392             =head1 CONTRIBUTORS
393              
394             Christopher Raa (mishanti1)
395              
396             Cameron Daniel (ccakes)
397              
398             =head1 COPYRIGHT AND LICENSE
399              
400             Copyright (C) 2015 by L and L.
401              
402             This library is free software; you can redistribute it and/or modify
403             it under the same terms as Perl itself.
404