File Coverage

blib/lib/Mojo/JWT.pm
Criterion Covered Total %
statement 99 112 88.3
branch 37 56 66.0
condition 8 15 53.3
subroutine 15 16 93.7
pod 7 7 100.0
total 166 206 80.5


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