File Coverage

blib/lib/Net/OAuth2/AuthorizationServer/Defaults.pm
Criterion Covered Total %
statement 136 144 94.4
branch 61 72 84.7
condition 24 42 57.1
subroutine 31 31 100.0
pod 0 8 0.0
total 252 297 84.8


line stmt bran cond sub pod time code
1             package Net::OAuth2::AuthorizationServer::Defaults;
2              
3 10     10   103907 use strict;
  10         34  
  10         327  
4 10     10   108 use warnings;
  10         26  
  10         309  
5              
6 10     10   65 use Moo::Role;
  10         25  
  10         75  
7              
8 10     10   4941 use Types::Standard qw/ :all /;
  10         83722  
  10         1279  
9 10     10   486187 use Carp qw/ croak /;
  10         30  
  10         844  
10 10     10   2466 use Crypt::JWT qw/ encode_jwt decode_jwt /;
  10         164932  
  10         694  
11 10     10   89 use Crypt::PRNG qw/ random_string /;
  10         92  
  10         1691  
12 10     10   5612 use Try::Tiny;
  10         12316  
  10         601  
13 10     10   78 use Time::HiRes qw/ gettimeofday /;
  10         24  
  10         110  
14 10     10   7695 use MIME::Base64 qw/ encode_base64 /;
  10         4848  
  10         23566  
15              
16             has 'jwt_secret' => (
17             is => 'ro',
18             isa => Any,
19             required => 0,
20             );
21              
22             has 'jwt_algorithm' => (
23             is => 'ro',
24             isa => Any,
25             required => 0,
26             default => sub { 'HS256' }, # back compat with Mojo::JWT
27             );
28              
29             has 'jwt_encoding' => (
30             is => 'ro',
31             isa => Str,
32             required => 0,
33             default => sub { 'A128GCM' },
34             );
35              
36             has 'access_token_ttl' => (
37             is => 'ro',
38             isa => Maybe[Int|CodeRef],
39             required => 0,
40             default => sub { 3600 },
41             );
42              
43             has [
44             qw/
45             clients
46             access_tokens
47             refresh_tokens
48             /
49             ] => (
50             is => 'ro',
51             isa => Maybe [HashRef],
52             required => 0,
53             default => sub { {} },
54             );
55              
56             has [
57             qw/
58             verify_client_cb
59             store_access_token_cb
60             verify_access_token_cb
61             login_resource_owner_cb
62             confirm_by_resource_owner_cb
63             /
64             ] => (
65             is => 'ro',
66             isa => Maybe [CodeRef],
67             required => 0,
68             );
69              
70 25 100 50 25   369 sub _has_clients { return keys %{ shift->clients // {} } ? 1 : 0 }
  25         510  
71 1     1   751 sub _uses_auth_codes { die "You must override _uses_auth_codes" };
72              
73             sub verify_client {
74 72     72 0 78868 _delegate_to_cb_or_private( 'verify_client', @_ );
75             }
76              
77             sub store_access_token {
78 16     16 0 74 _delegate_to_cb_or_private( 'store_access_token', @_ );
79             }
80              
81             sub verify_access_token {
82 102     102 0 43081 _delegate_to_cb_or_private( 'verify_access_token', @_ );
83             }
84              
85             sub login_resource_owner {
86 8     8 0 2963 _delegate_to_cb_or_private( 'login_resource_owner', @_ );
87             }
88              
89             sub confirm_by_resource_owner {
90 8     8 0 39 _delegate_to_cb_or_private( 'confirm_by_resource_owner', @_ );
91             }
92              
93             sub verify_token_and_scope {
94 37     37 0 29972 my ( $self, %args ) = @_;
95              
96             my ( $refresh_token, $scopes_ref, $auth_header ) =
97 37         139 @args{ qw/ refresh_token scopes auth_header / };
98              
99 37         65 my $access_token;
100              
101 37 100       120 if ( !$refresh_token ) {
102 29 100       85 if ( $auth_header ) {
103 21         105 my ( $auth_type, $auth_access_token ) = split( / /, $auth_header );
104              
105 21 100       79 if ( $auth_type ne 'Bearer' ) {
106 1         5 return ( 0, 'invalid_request' );
107             }
108             else {
109 20         49 $access_token = $auth_access_token;
110             }
111             }
112             else {
113 8         61 return ( 0, 'invalid_request' );
114             }
115             }
116             else {
117 8         19 $access_token = $refresh_token;
118             }
119              
120             # we must have some form of token to continue
121 28 50 66     111 return ( 0, 'invalid_request' )
122             if ( ! $access_token && ! $refresh_token );
123              
124 24         105 return $self->verify_access_token(
125             %args,
126             access_token => $access_token,
127             scopes => $scopes_ref,
128             is_refresh_token => $refresh_token,
129             );
130             }
131              
132             sub _delegate_to_cb_or_private {
133              
134 252     252   565 my $method = shift;
135 252         418 my $self = shift;
136 252         868 my %args = @_;
137              
138 252         588 my $cb_method = "${method}_cb";
139 252         562 my $p_method = "_$method";
140              
141 252 100       1164 if ( my $cb = $self->$cb_method ) {
142 116         441 return $cb->( %args );
143             }
144             else {
145 136         672 return $self->$p_method( %args );
146             }
147             }
148              
149 8     8   61 sub _login_resource_owner { 1 }
150              
151             sub _confirm_by_resource_owner {
152 8     8   46 my ( $self,%args ) = @_;
153              
154             # out of the box we just pass back "yes you can" and the list of scopes
155             # note the wantarray is here for backwards compat as this method used
156             # to just return 1 but now passing the scopes back requires an array
157             return wantarray
158 8 50 50     77 ? ( 1,undef,$args{scopes} // [] )
159             : 1;
160             }
161              
162             sub _verify_client {
163 16     16   69 my ( $self, %args ) = @_;
164              
165 16         42 my ( $client_id, $scopes_ref ) = @args{ qw/ client_id scopes / };
166              
167 16 100 50     87 if ( my $client = $self->clients->{ $client_id // '' } ) {
168 12         26 my $client_scopes = [];
169              
170 12   50     22 foreach my $scope ( @{ $scopes_ref // [] } ) {
  12         43  
171 32 100       127 if ( ! exists($self->clients->{ $client_id }{ scopes }{ $scope }) ) {
    100          
172 4         32 return ( 0, 'invalid_scope' );
173             }
174             elsif ( $self->clients->{ $client_id }{ scopes }{ $scope } ) {
175 24         39 push @{$client_scopes}, $scope;
  24         57  
176             }
177             }
178              
179 8         51 return ( 1, undef, $client_scopes );
180             }
181              
182 4         25 return ( 0, 'unauthorized_client' );
183             }
184              
185             sub _store_access_token {
186 16     16   86 my ( $self, %args ) = @_;
187              
188             my ( $c_id, $auth_code, $access_token, $refresh_token, $expires_in, $scope, $old_refresh_token )
189             = @args{
190 16         83 qw/ client_id auth_code access_token refresh_token expires_in scopes old_refresh_token / };
191              
192 16   33     102 $expires_in //= $self->get_access_token_ttl(
193             scopes => $scope,
194             client_id => $c_id,
195             );
196              
197 16 100       108 return 1 if $self->jwt_secret;
198              
199 8 50 66     43 if ( !defined( $auth_code ) && $old_refresh_token ) {
200              
201             # must have generated an access token via a refresh token so revoke the old
202             # access token and refresh token and update the auth_codes hash to store the
203             # new one (also copy across scopes if missing)
204 0         0 $auth_code = $self->refresh_tokens->{ $old_refresh_token }{ auth_code };
205              
206 0         0 my $prev_access_token = $self->refresh_tokens->{ $old_refresh_token }{ access_token };
207              
208             # access tokens can be revoked, whilst refresh tokens can remain so we
209             # need to get the data from the refresh token as the access token may
210             # no longer exist at the point that the refresh token is used
211 0   0     0 $scope //= $self->refresh_tokens->{ $old_refresh_token }{ scope };
212              
213 0         0 $self->_revoke_access_token( $prev_access_token );
214             }
215              
216 8 50       23 delete( $self->refresh_tokens->{ $old_refresh_token } )
217             if $old_refresh_token;
218              
219 8   100     77 $self->access_tokens->{ $access_token } = {
220             scope => $scope,
221             expires => time + $expires_in,
222             refresh_token => $refresh_token // undef,
223             client_id => $c_id,
224             };
225              
226 8 100       28 if ( $refresh_token ) {
227              
228 4 100       17 $self->refresh_tokens->{ $refresh_token } = {
229             scope => $scope,
230             client_id => $c_id,
231             access_token => $access_token,
232             ( $self->_uses_auth_codes ? ( auth_code => $auth_code ) : () ),
233             };
234             }
235              
236 8 100       38 if ( $self->_uses_auth_codes ) {
237 2         9 $self->auth_codes->{ $auth_code }{ access_token } = $access_token;
238             }
239              
240 8         71 return $c_id;
241             }
242              
243             sub _verify_access_token {
244 62     62   251 my ( $self, %args ) = @_;
245 62 100       246 return $self->_verify_access_token_jwt( %args ) if $self->jwt_secret;
246              
247             my ( $a_token, $scopes_ref, $is_refresh_token ) =
248 30         86 @args{ qw/ access_token scopes is_refresh_token / };
249              
250 30 100 100     194 if ( $is_refresh_token
    100          
251             && exists( $self->refresh_tokens->{ $a_token } ) )
252             {
253              
254 8 50       25 if ( $scopes_ref ) {
255 8   50     15 foreach my $scope ( @{ $scopes_ref // [] } ) {
  8         29  
256             return ( 0, 'invalid_grant' )
257 16 50       48 if !$self->_has_scope( $scope, $self->refresh_tokens->{ $a_token }{ scope } );
258             }
259             }
260              
261             return (
262             $self->refresh_tokens->{ $a_token },
263             undef,
264             $self->refresh_tokens->{ $a_token }{ scope },
265             $self->refresh_tokens->{ $a_token }{ user_id },
266 8         97 );
267             }
268             elsif ( exists( $self->access_tokens->{ $a_token } ) ) {
269              
270 16 50       71 if ( $self->access_tokens->{ $a_token }{ expires } <= time ) {
    50          
271 0         0 $self->_revoke_access_token( $a_token );
272 0         0 return ( 0, 'invalid_grant' );
273             }
274             elsif ( $scopes_ref ) {
275              
276 16   50     31 foreach my $scope ( @{ $scopes_ref // [] } ) {
  16         70  
277             return ( 0, 'invalid_grant' )
278 24 100       72 if !$self->_has_scope( $scope, $self->access_tokens->{ $a_token }{ scope } );
279             }
280              
281             }
282              
283             return (
284             $self->access_tokens->{ $a_token },
285             undef,
286             $self->access_tokens->{ $a_token }{ scope },
287             $self->access_tokens->{ $a_token }{ user_id },
288 8         74 );
289             }
290              
291 6         39 return ( 0, 'invalid_grant' );
292             }
293              
294             sub _has_scope {
295 120     120   259 my ( $self, $scope, $available_scopes ) = @_;
296 120   50     192 return scalar grep { $_ eq $scope } @{ $available_scopes // [] };
  240         880  
  120         316  
297             }
298              
299             sub _verify_access_token_jwt {
300 52     52   140 my ( $self, %args ) = @_;
301              
302             my ( $access_token, $scopes_ref, $is_refresh_token ) =
303 52         142 @args{ qw/ access_token scopes is_refresh_token / };
304              
305 52         102 my $access_token_payload;
306              
307             my $invalid_jwt;
308             try {
309 52     52   3989 $access_token_payload = decode_jwt(
310             alg => $self->jwt_algorithm,
311             key => $self->jwt_secret,
312             token => $access_token,
313             );
314             }
315             catch {
316 16     16   5984 $invalid_jwt = 1;
317 52         411 };
318 52 100       7504 return ( 0, 'invalid_grant' ) if $invalid_jwt;
319              
320 36 50 66     256 if (
      66        
321             $access_token_payload
322             && ( $access_token_payload->{ type } eq 'access'
323             || $is_refresh_token && $access_token_payload->{ type } eq 'refresh' )
324             )
325             {
326              
327 36 50       89 if ( $scopes_ref ) {
328 36   50     64 foreach my $scope ( @{ $scopes_ref // [] } ) {
  36         122  
329             return ( 0, 'invalid_grant' )
330 60 100       172 if !$self->_has_scope( $scope, $access_token_payload->{ scopes } );
331             }
332             }
333              
334 24         208 return ( $access_token_payload, undef, $access_token_payload->{scopes} );
335             }
336              
337 0         0 return ( 0, 'invalid_grant' );
338             }
339              
340             sub _revoke_access_token {
341 2     2   6 my ( $self, $access_token ) = @_;
342 2         14 delete( $self->access_tokens->{ $access_token } );
343             }
344              
345             sub get_access_token_ttl {
346 46     46 0 2512 my ( $self, %args ) = @_;
347              
348 46 100       326 return ref $self->access_token_ttl eq 'CODE'
349             ? $self->access_token_ttl->( %args )
350             : $self->access_token_ttl;
351             }
352              
353             sub token {
354 31     31 0 116637 my ( $self, %args ) = @_;
355              
356             my ( $client_id, $scopes, $type, $redirect_uri, $user_id, $claims ) =
357 31         150 @args{ qw/ client_id scopes type redirect_uri user_id jwt_claims_cb / };
358              
359 31 50 66     144 if (
360             ! $self->_uses_auth_codes
361             && $type eq 'auth'
362             ) {
363 0         0 croak "Invalid type for ->token ($type)";
364             }
365              
366 31 100       212 my $ttl = $type eq 'auth' ? $self->auth_code_ttl : $self->get_access_token_ttl(
367             scopes => $scopes,
368             client_id => $client_id,
369             );
370              
371 31 100       103 undef( $ttl ) if $type eq 'refresh';
372 31         62 my $code;
373              
374 31 100       126 if ( !$self->jwt_secret ) {
375 14         71 my ( $sec, $usec ) = gettimeofday;
376 14         225 $code = encode_base64( join( '-', $sec, $usec, rand(), random_string( 30 ) ), '' );
377             }
378             else {
379 17 100       101 if ( $self->jwt_algorithm =~ /none/i ) {
380 1         36 croak "A jwt_algorithm of 'none' is not supported as this is insecure";
381             }
382              
383 16         80 my $jti = random_string( 32 );
384              
385 16 100       4934 $code = encode_jwt(
    100          
386             allow_none => 0, # do NOT allow the "none" algorithm, as this is massively insecure
387             # we actually already check this above, but this is here a backup
388             alg => $self->jwt_algorithm,
389             key => $self->jwt_secret,
390             enc => $self->jwt_encoding,
391              
392             ( $ttl ? ( relative_exp => $ttl ) : () ),
393             auto_iat => 1,
394              
395             # https://tools.ietf.org/html/rfc7519#section-4
396             payload => {
397             # Registered Claim Names
398             aud => $redirect_uri, # the "audience"
399             jti => $jti,
400              
401             # Private Claim Names
402             user_id => $user_id,
403             client => $client_id,
404             type => $type,
405             scopes => $scopes,
406              
407             ( $claims
408             ? ( $claims->({
409             user_id => $user_id,
410             client_id => $client_id,
411             type => $type,
412             scopes => $scopes,
413             redirect_uri => $redirect_uri,
414             jti => $jti,
415             }) )
416             : ()
417             ),
418             },
419             );
420             }
421              
422 30         29806 return $code;
423             }
424              
425             __PACKAGE__->meta->make_immutable;