File Coverage

blib/lib/Plack/Middleware/Auth/JWT.pm
Criterion Covered Total %
statement 64 66 96.9
branch 38 42 90.4
condition 5 5 100.0
subroutine 11 11 100.0
pod 2 3 66.6
total 120 127 94.4


line stmt bran cond sub pod time code
1             package Plack::Middleware::Auth::JWT;
2              
3             # ABSTRACT: Token-based Auth (aka Bearer Token) using JSON Web Tokens (JWT)
4             our $VERSION = '0.905'; # VERSION
5              
6 2     2   4200 use 5.010;
  2         10  
7 2     2   11 use strict;
  2         4  
  2         40  
8 2     2   10 use warnings;
  2         3  
  2         59  
9 2     2   11 use parent qw(Plack::Middleware);
  2         3  
  2         14  
10 2     2   147 use Plack::Util;
  2         4  
  2         85  
11             use Plack::Util::Accessor
12 2     2   14 qw(decode_args decode_callback psgix_claims psgix_token token_required ignore_invalid_token token_header_name token_query_name);
  2         3  
  2         20  
13 2     2   1274 use Plack::Request;
  2         126460  
  2         91  
14 2     2   686 use Crypt::JWT 0.020 qw(decode_jwt);
  2         55403  
  2         1162  
15              
16             sub prepare_app {
17 10     10 1 39826 my $self = shift;
18              
19             # some defaults
20 10 100       42 $self->psgix_claims('claims') unless $self->psgix_claims;
21 10 100       235 $self->psgix_token('token') unless $self->psgix_token;
22              
23 10 100       116 $self->token_header_name('bearer')
24             unless defined $self->token_header_name;
25 10 100       95 $self->token_header_name(undef) unless $self->token_header_name;
26 10 100       67 $self->token_query_name('token') unless defined $self->token_query_name;
27 10 100       107 $self->token_query_name(undef) unless $self->token_query_name;
28 10 100       72 $self->token_required(0) unless defined $self->token_required;
29 10 100       117 $self->ignore_invalid_token(0) unless defined $self->ignore_invalid_token;
30              
31             # either decode_args or decode_callback is required
32 10 100       120 if ( my $cb = $self->decode_callback ) {
    50          
33 5 50       50 die "decode_callback must be a code reference"
34             unless ref($cb) eq 'CODE';
35             }
36             elsif ( my $args = $self->decode_args ) {
37 5         58 $args->{decode_payload} = 1;
38 5         18 $args->{decode_header} = 0;
39 5 100       16 $args->{verify_exp} = 1 unless exists $args->{verify_exp};
40 5 50       21 $args->{leeway} = 5 unless exists $args->{leeway};
41             }
42             else {
43 0         0 die
44             "Either decode_callback or decode_args has to be defined when loading this Middleware";
45             }
46             }
47              
48             sub call {
49 30     30 1 158549 my ( $self, $env ) = @_;
50              
51 30         53 my $token;
52              
53 30 100 100     97 if ( $self->token_header_name && $env->{HTTP_AUTHORIZATION} ) {
    100          
54 15         151 my $name = $self->token_header_name;
55 15         74 my $auth = $env->{HTTP_AUTHORIZATION};
56 15 50       236 $token = $1 if $auth =~ /^\s*$name\s+(.+)/i;
57             }
58             elsif ( my $name = $self->token_query_name ) {
59 13         228 my $req = Plack::Request->new($env);
60 13         140 $token = $req->query_parameters->get($name);
61             }
62              
63 30 100       1267 unless ($token) {
64 8 100       24 return $self->unauthorized if $self->token_required;
65              
66             # no token found, but non required, so just call the app
67 2         17 return $self->app->($env);
68             }
69              
70 22         38 my $claims = eval {
71 22 100       63 if ( my $cb = $self->decode_callback ) {
72 8         56 return $cb->( $token, $env );
73             }
74             else {
75 14         69 return decode_jwt( token => $token, %{ $self->decode_args } );
  14         36  
76             }
77             };
78 22 100       3496 if ($@) {
79 9 100       34 if ($self->ignore_invalid_token) {
80 1         9 return $self->app->($env);
81             }
82             else {
83             # TODO hm, if token cannot be decoded: 401 or 400?
84 8         63 return $self->unauthorized( 'Cannot decode JWT: ' . $@ );
85             }
86             }
87             else {
88 13         45 $env->{ 'psgix.' . $self->psgix_token } = $token;
89 13         94 $env->{ 'psgix.' . $self->psgix_claims } = $claims;
90 13         93 return $self->app->($env);
91             }
92              
93             # should never be reached, but just to make sure...
94 0         0 return $self->unauthorized;
95             }
96              
97             sub unauthorized {
98 14     14 0 57 my $self = shift;
99 14   100     49 my $body = shift || 'Authorization required';
100              
101             return [
102 14         117 401,
103             [ 'Content-Type' => 'text/plain',
104             'Content-Length' => length $body
105             ],
106             [$body]
107             ];
108             }
109              
110             1;
111              
112             __END__
113              
114             =pod
115              
116             =encoding UTF-8
117              
118             =head1 NAME
119              
120             Plack::Middleware::Auth::JWT - Token-based Auth (aka Bearer Token) using JSON Web Tokens (JWT)
121              
122             =head1 VERSION
123              
124             version 0.905
125              
126             =head1 SYNOPSIS
127              
128             # use Crypt::JWT to decode the JWT
129             use Plack::Builder;
130             builder {
131             enable "Plack::Middleware::Auth::JWT",
132             decode_args => { key => '12345' },
133             ;
134             $app;
135             };
136              
137             # or provide your own decoder in a callback
138             use Plack::Builder;
139             builder {
140             enable "Plack::Middleware::Auth::JWT",
141             decode_callback => sub {
142             my $token = shift;
143             ....
144             },
145             ;
146             $app;
147             };
148              
149              
150             # curl -H 'Authorization: Bearer eyJhbG...'
151             # if the JWT is valid, two keys will be added to $env->{psgix}
152             # $env->{'psgix.token'} = 'original_token'
153             # $env->{'psgix.claims'} = { sub => 'bart' } # claims as hashref
154              
155             =head1 DESCRIPTION
156              
157             C<Plack::Middleware::Auth::JWT> helps you to use L<JSON Web
158             Tokens|https://en.wikipedia.org/wiki/JSON_Web_Token> (or JWT) for
159             authentificating HTTP requests. Tokens can be provided in the
160             C<Authorization> HTTP Header, or as a query parameter (though passing
161             the JWT via the header is the prefered method).
162              
163             =head2 Configuration
164              
165             TODO
166              
167             =head3 decode_args
168              
169             See L<Crypt::JWT/decode_jwt>
170              
171             Please note that C<key> might has to be passed as a string-ref or an object, see L<Crypt::JWT>
172              
173             It is B<very much recommended> that you only allow the algorithms you are actually using by setting C<accepted_alg>! Per default, 'none' is B<not> allowed.
174              
175             Hardcoded:
176              
177             decode_payload = 1
178             decode_header = 0
179              
180             Different defaults:
181              
182             verify_exp = 1
183             leeway = 5
184              
185             You either have to use C<decode_args>, or provide a L<decode_callback>.
186              
187             =head3 decode_callback
188              
189             Callback to decode the token. Gets the token as a string and the psgi-env, has to return a hashref with claims.
190              
191             You have to either provide a callback, or use L<decode_args>.
192              
193             =head3 psgix_claims
194              
195             Default: C<claims>
196              
197             Name of the entry in C<psgix> were the claims are stored, so you can get the (for example) C<sub> claim via
198              
199             $env->{'psgix.claims'}->{sub}
200              
201             =head3 psgix_token
202              
203             Default: C<token>
204              
205             Name of the entry in C<psgix> were the raw token is stored.
206              
207             =head3 token_required
208              
209             Default: C<false>
210              
211             If set to a true value, all requests need to include a valid JWT. Default false, so you have to check in your application code if a token was submitted.
212              
213             =head3 ignore_invalid_token
214              
215             Default: C<false>
216              
217             If set to a true value, passing an invalid JWT will not abort the
218             requerst with status 401. Instead the app will be called as if no
219             token was passed at all.
220              
221             You can use this to implement another token check in a later
222             middleware, or even in your app. Of course you will then have to check
223             for C<< $env->{psgix.token} >> in your controller actions.
224              
225             =head3 token_header_name
226              
227             Default: C<Bearer>
228              
229             Name of the token in the HTTP C<Authorization> header. If you set it to C<0>, headers will be ignored.
230              
231             =head3 token_query_name
232              
233             Default: C<token>
234              
235             Name of the HTTP query param that contains the token. If you set it to C<0>, tokens in the query will be ignored.
236              
237             =head2 Example
238              
239             TODO, in the meantime you can take a look at the tests.
240              
241             =head1 SEE ALSO
242              
243             =over
244              
245             =item * L<Cryp::JWT|https://metacpan.org/pod/Crypt::JWT> - encode / decode JWTs using various algorithms. Very complete!
246              
247             =item * L<Introduction to JSON Web Tokens|https://jwt.io/introduction> - good overview.
248              
249             =item * L<Plack::Middleware::Auth::AccessToken|https://metacpan.org/pod/Plack::Middleware::Auth::AccessToken> - a more generic solution handling any kind of token. Does not handle token payload (C<claims>).
250              
251             =back
252              
253             =head1 THANKS
254              
255             Thanks to
256              
257             =over
258              
259             =item *
260              
261             L<validad.com|https://www.validad.com/> for supporting Open Source.
262              
263             =item * L<jwright|https://github.com/jwrightecs> for fixing a
264             regression in the tests caused by an update in L<Crypt::JWT> error
265             messages. The same issue was also reported by SREZIC.
266              
267             =back
268              
269             =head1 AUTHOR
270              
271             Thomas Klausner <domm@plix.at>
272              
273             =head1 COPYRIGHT AND LICENSE
274              
275             This software is copyright (c) 2017 - 2021 by Thomas Klausner.
276              
277             This is free software; you can redistribute it and/or modify it under
278             the same terms as the Perl 5 programming language system itself.
279              
280             =cut