File Coverage

blib/lib/Net/OAuth2/AuthorizationServer/Defaults.pm
Criterion Covered Total %
statement 135 143 94.4
branch 60 70 85.7
condition 22 39 56.4
subroutine 31 31 100.0
pod 0 8 0.0
total 248 291 85.2


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