File Coverage

blib/lib/Dancer2/Plugin/JWT.pm
Criterion Covered Total %
statement 25 25 100.0
branch 4 4 100.0
condition 2 2 100.0
subroutine 7 7 100.0
pod n/a
total 38 38 100.0


line stmt bran cond sub pod time code
1 5     5   3857073 use strict;
  5         30  
  5         137  
2 5     5   24 use warnings;
  5         7  
  5         209  
3             package Dancer2::Plugin::JWT;
4             # ABSTRACT: JSON Web Token made simple for Dancer2
5             $Dancer2::Plugin::JWT::VERSION = '0.020';
6             # VERSION: generated by DZP::OurPkgVersion
7              
8 5     5   2691 use Dancer2::Plugin;
  5         59319  
  5         35  
9 5     5   15332 use Crypt::JWT qw(encode_jwt decode_jwt);
  5         48013  
  5         274  
10 5     5   31 use URI;
  5         9  
  5         133  
11 5     5   2476 use URI::QueryParam;
  5         3477  
  5         7471  
12              
13             register_hook qw(jwt_exception);
14              
15             my $fourWeeks = 4 * 24 * 60 * 60;
16             my $DEFAULT_SET_AUTHORIZATION_HEADER = 1;
17             my $DEFAULT_EXPOSE_AUTHORIZATION_HEADER = 1;
18             my $DEFAULT_SET_COOKIE_HEADER = 1;
19             my $DEFAULT_SET_LOCATION_HEADER = 1;
20              
21             my $secret;
22             my $alg;
23             my $enc;
24             my $need_iat = undef;
25             my $need_nbf = undef;
26             my $need_exp = undef;
27             my $need_leeway = undef;
28             my $cookie_domain = undef;
29             my $set_authorization_header = undef;
30             my $expose_authorization_header = undef;
31             my $set_cookie_header = undef;
32             my $set_location_header = undef;
33              
34             register jwt => sub {
35 14     14   1584 my $dsl = shift;
36 14         35 my @args = @_;
37              
38 14 100       51 if (@args) {
39 8         88 $dsl->app->request->var(jwt => $args[0]);
40             }
41             else {
42 6 100       53 if ($dsl->app->request->var('jwt_status') eq "missing") {
43 3         83 $dsl->app->execute_hook('plugin.jwt.jwt_exception' => 'No JWT is present');
44             }
45             }
46 14   100     669 return $dsl->app->request->var('jwt') || undef;
47             };
48              
49             on_plugin_import {
50             my $dsl = shift;
51              
52             my $config = plugin_setting;
53             die "JWT cannot be used without a secret!" unless (exists $config->{secret} && defined $config->{secret});
54             # For RSA and ES algorithms - path to keyfile or JWK string, others algorithms - just secret string
55             $secret = $config->{secret};
56              
57             $cookie_domain = $config->{cookie_domain};
58             $set_authorization_header = defined $config->{set_authorization_header}
59             ? $config->{set_authorization_header} : $DEFAULT_SET_AUTHORIZATION_HEADER;
60             $expose_authorization_header = defined $config->{expose_authorization_header}
61             ? $config->{expose_authorization_header} : $DEFAULT_EXPOSE_AUTHORIZATION_HEADER;
62             $set_cookie_header = defined $config->{set_cookie_header}
63             ? $config->{set_cookie_header} : $DEFAULT_SET_COOKIE_HEADER;
64             $set_location_header = defined $config->{set_location_header}
65             ? $config->{set_location_header} : $DEFAULT_SET_LOCATION_HEADER;
66              
67             $alg = 'HS256';
68              
69             if ( exists $config->{alg} && defined $config->{alg} ) {
70             my $need_enc = undef;
71             my $need_key = undef;
72              
73             if ( $config->{alg} =~ /^([EHPR])S(256|384|512)$/ ) {
74             my $type = $1;
75              
76             if ( $type eq 'P' || $type eq 'R' ) {
77             $need_key = 1;
78             } elsif ( $type eq 'E' ) {
79             $need_key = 2;
80             }
81              
82             $alg = $config->{alg};
83             } elsif ( $config->{alg} =~ /^A(128|192|256)(GCM)?KW$/ ) {
84             my $len = $1;
85              
86             if ( ( length( unpack( "H*", $secret ) ) * 4 ) != $len ) {
87             die "Secret key length must be equal " . $len / 8 . " bytes for selected algoritm";
88             }
89              
90             $alg = $config->{alg};
91             $need_enc = 1;
92             } elsif ( $config->{alg} =~ /^PBES2-HS(256|384|512)\+A(128|192|256)KW$/ ) {
93             my $hs = $1;
94             my $a = $2;
95              
96             if ( ( ( $a * 2 ) - $hs ) != 0 ) {
97             die "Incompatible A and HS values";
98             }
99              
100             $alg = $config->{alg};
101             $need_enc = 1;
102             } elsif ( $config->{alg} =~ /^RSA((-OAEP(-265)?)|1_5)$/ ) {
103             $alg = $config->{alg};
104             $need_enc = 1;
105             $need_key = 1;
106             } elsif ( $config->{alg} =~ /^ECDH-ES(\+A(128|192|256)KW)?$/ ) {
107             $alg = $config->{alg};
108             $need_enc = 1;
109             $need_key = 2;
110             } else {
111             die "Unknown algoritm";
112             }
113              
114             if ( $need_enc ) {
115             unless ( exists $config->{enc} && defined $config->{enc} ) {
116             die "JWE cannot be used with empty encryption method";
117             }
118              
119             if ( $config->{enc} =~ /^A(128|192|256)GCM$/ ) {
120             $enc = $config->{enc};
121             } elsif ( $config->{enc} =~ /^A(128|192|256)CBC-HS(256|384|512)$/ ) {
122             my $a = $1;
123             my $hs = $2;
124              
125             if ( ( ( $a * 2 ) - $hs ) != 0 ) {
126             die "Incompatible A and HS values";
127             }
128              
129             $enc = $config->{enc};
130             }
131             }
132              
133             if ( defined $need_key ) {
134             if ( $need_key eq 1 ) {
135             # TODO: add code to handle RSA keys or parse JWK hash string:
136             ##instance of Crypt::PK::RSA
137             #my $data = decode_jwt(token=>$t, key=>Crypt::PK::RSA->new('keyfile.pem'));
138             #
139             ##instance of Crypt::X509 (public key only)
140             #my $data = decode_jwt(token=>$t, key=>Crypt::X509->new(cert=>$cert));
141             #
142             ##instance of Crypt::OpenSSL::X509 (public key only)
143             #my $data = decode_jwt(token=>$t, key=>Crypt::OpenSSL::X509->new_from_file('cert.pem'));
144             } elsif ( $need_key eq 2 ) {
145             # TODO: add code to handle ECC keys or parse JWK hash string:
146             #instance of Crypt::PK::ECC
147             #my $data = decode_jwt(token=>$t, key=>Crypt::PK::ECC->new('keyfile.pem'));
148             }
149             }
150             }
151              
152             if ( exists $config->{need_iat} && defined $config->{need_iat} ) {
153             $need_iat = $config->{need_iat};
154             }
155              
156             if ( exists $config->{need_nbf} && defined $config->{need_nbf} ) {
157             $need_nbf = $config->{need_nbf};
158             }
159              
160             if ( exists $config->{need_exp} && defined $config->{need_exp} ) {
161             $need_exp = $config->{need_exp};
162             }
163              
164             if ( exists $config->{need_leeway} && defined $config->{need_leeway} ) {
165             $need_leeway = $config->{need_leeway};
166             }
167              
168             $dsl->app->add_hook(
169             Dancer2::Core::Hook->new(
170             name => 'before_template_render',
171             code => sub {
172             my $tokens = shift;
173             $tokens->{jwt} = $dsl->app->request->var('jwt');
174             }
175             )
176             );
177              
178             $dsl->app->add_hook(
179             Dancer2::Core::Hook->new(
180             name => 'after',
181             code => sub {
182             if($expose_authorization_header) {
183             my $response = shift;
184             $response = $response->isa('Dancer2::Core::Response') ? $response : $response->response;
185             $response->push_header('Access-Control-Expose-Headers' => 'Authorization');
186             }
187             }
188             )
189             );
190              
191             $dsl->app->add_hook(
192             Dancer2::Core::Hook->new(
193             name => 'before',
194             code => sub {
195             my $app = shift;
196             my $encoded = $app->request->headers->authorization;
197              
198             if( defined $encoded ) {
199             # Remove "Bearer " (sic) from the beginning of the Authorization header if present.
200             # "Bearer" signifies the schema and should be present
201             # but due to backwards compatibility we support also without it.
202             # https://jwt.io/introduction/ (How do JSON Web Tokens work?)
203             $encoded =~ m/^ (?: Bearer [[:space:]]{1} | ) (?<token> [^[:space:]]{0,} ) $/msx;
204             $encoded = $+{token};
205             }
206              
207             if ($app->request->cookies->{_jwt}) {
208             $encoded = $app->request->cookies->{_jwt}->value ;
209             }
210             elsif ($app->request->param('_jwt')) {
211             $encoded = $app->request->param('_jwt');
212             }
213              
214             if ($encoded) {
215             my $decoded;
216             eval {
217             $decoded = decode_jwt( token => $encoded,
218             key => $secret,
219             verify_iat => $need_iat,
220             verify_nbf => $need_nbf,
221             verify_exp => defined $need_exp ? 1 : 0 ,
222             leeway => $need_leeway,
223             accepted_alg => $alg,
224             accepted_enc => $enc );
225             };
226             if ($@) {
227             $app->execute_hook('plugin.jwt.jwt_exception' => ($a = $@));
228             };
229             $app->request->var('jwt', $decoded);
230             $app->request->var('jwt_status' => 'present');
231              
232             }
233             else {
234             ## no token
235             $app->request->var('jwt_status' => 'missing');
236             }
237             }
238             )
239             );
240              
241             $dsl->app->add_hook(
242             Dancer2::Core::Hook->new(
243             name => 'after',
244             code => sub {
245             my $response = shift;
246             my $decoded = $dsl->app->request->var('jwt');
247             if($set_authorization_header || $set_cookie_header || $set_location_header) {
248             # If all are disabled, then skip also encoding!
249             if (defined($decoded)) {
250             my $encoded = encode_jwt( payload => $decoded,
251             key => $secret,
252             alg => $alg,
253             enc => $enc,
254             auto_iat => $need_iat,
255             relative_exp => $need_exp,
256             relative_nbf => $need_nbf );
257              
258             if($set_authorization_header) {
259             $response->headers->authorization($encoded);
260             }
261              
262             if($set_cookie_header) {
263             my %cookie = (
264             value => $encoded,
265             name => '_jwt',
266             expires => time + ($need_exp // $fourWeeks),
267             path => '/',
268             http_only => 0);
269             $cookie{domain} = $cookie_domain if defined $cookie_domain;
270             $response->push_header('Set-Cookie'
271             => Dancer2::Core::Cookie->new(%cookie)->to_header());
272             }
273              
274             if ($set_location_header && $response->status =~ /^3/) {
275             my $u = URI->new( $response->header("Location") );
276             $u->query_param( _jwt => $encoded);
277             $response->header(Location => $u);
278             }
279             }
280             } # ! $set_authorization_header && ! $set_cookie_header && ! $set_location_header
281             }
282             )
283             );
284             };
285              
286              
287              
288             register_plugin;
289              
290             1;
291              
292             =encoding UTF-8
293              
294             =head1 NAME
295              
296             Dancer2::Plugin::JWT - JSON Web Token made simple for Dancer2
297              
298             =head1 SYNOPSIS
299              
300             use Dancer2;
301             use Dancer2::Plugin::JWT;
302              
303             post '/login' => sub {
304             if (is_valid(param("username"), param("password"))) {
305             jwt { username => param("username") };
306             template 'index';
307             }
308             else {
309             redirect '/';
310             }
311             };
312              
313             get '/private' => sub {
314             my $data = jwt;
315             redirect '/ unless exists $data->{username};
316              
317             ...
318             };
319              
320             hook 'plugin.jwt.jwt_exception' => sub {
321             my $error = shift;
322             # do something
323             };
324              
325             =head1 DESCRIPTION
326              
327             Registers the C<jwt> keyword that can be used to set or retrieve the payload
328             of a JSON Web Token.
329              
330             To this to work it is required to have a secret defined in your config.yml file:
331              
332             plugins:
333             JWT:
334             secret: "string or path to private RSA/EC key"
335             # default, or others supported by Crypt::JWT
336             alg: HS256
337             # required only for JWE
338             enc:
339             # add issued at time (iat) field
340             need_iat: 1
341             # check not before field
342             need_nbf: 1
343             # in seconds
344             need_exp: 600
345             # timeshift for expiration
346             need_leeway: 30
347             # JWT cookie domain, in case you need to override it
348             cookie_domain: my_domain.com
349             # Attach Authorization header to HTTP response
350             set_authorization_header: 0
351             # Attach 'Access-Control-Expose-Headers: Authorization' header to HTTP response
352             expose_authorization_header: 0
353             # Attach Set-Cookie header to HTTP response
354             set_cookie_header: 0
355             # Attach Location header to HTTP response when response is 300-399
356             # e.g. redirect
357             set_location_header: 0
358              
359             B<NOTE:> A empty call (without arguments) to jwt will trigger the
360             exception hook if there is no jwt defined.
361              
362             B<NOTE:> If you are using JWT to authenticate an API call to return, e.g. JSON,
363             not a web page to display, be sure to set the config items
364             I<set_authorization_header>, I<expose_authorization_header>,
365             I<set_cookie_header> and I<set_location_header>
366             so you don't return any unnecessary headers.
367              
368             =head1 BUGS
369              
370             I am sure a lot. Please use GitHub issue tracker
371             L<here|https://github.com/ambs/Dancer2-Plugin-JWT/>.
372              
373             =head1 ACKNOWLEDGEMENTS
374              
375             To Lee Johnson for his talk "JWT JWT JWT" in YAPC::EU::2015.
376              
377             To Nuno Carvalho for brainstorming and help with testing.
378              
379             To user2014, thanks for making the module use Crypt::JWT.
380              
381             =head1 COPYRIGHT AND LICENSE
382              
383             Copyright 2015-2018 Alberto Simões, all rights reserved.
384              
385             This module is free software and is published under the same terms as Perl itself.
386              
387             =head1 AUTHOR
388              
389             Alberto Simões C<< <ambs@cpan.org> >>
390              
391             =cut