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