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 function.
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