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   102463 use strict;
  10         31  
  10         322  
4 10     10   137 use warnings;
  10         54  
  10         347  
5              
6 10     10   69 use Moo::Role;
  10         26  
  10         84  
7              
8 10     10   4994 use Types::Standard qw/ :all /;
  10         79603  
  10         1205  
9 10     10   487676 use Carp qw/ croak /;
  10         31  
  10         825  
10 10     10   2393 use Crypt::JWT qw/ encode_jwt decode_jwt /;
  10         164979  
  10         1985  
11 10     10   110 use Crypt::PRNG qw/ random_string /;
  10         84  
  10         1735  
12 10     10   6483 use Try::Tiny;
  10         10929  
  10         604  
13 10     10   75 use Time::HiRes qw/ gettimeofday /;
  10         22  
  10         111  
14 10     10   7535 use MIME::Base64 qw/ encode_base64 /;
  10         4934  
  10         22347  
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   400 sub _has_clients { return keys %{ shift->clients // {} } ? 1 : 0 }
  25         509  
71 1     1   703 sub _uses_auth_codes { die "You must override _uses_auth_codes" };
72              
73             sub verify_client {
74 72     72 0 82203 _delegate_to_cb_or_private( 'verify_client', @_ );
75             }
76              
77             sub store_access_token {
78 16     16 0 66 _delegate_to_cb_or_private( 'store_access_token', @_ );
79             }
80              
81             sub verify_access_token {
82 102     102 0 45276 _delegate_to_cb_or_private( 'verify_access_token', @_ );
83             }
84              
85             sub login_resource_owner {
86 8     8 0 3023 _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 27481 my ( $self, %args ) = @_;
95              
96             my ( $refresh_token, $scopes_ref, $auth_header ) =
97 33         118 @args{ qw/ refresh_token scopes auth_header / };
98              
99 33         66 my $access_token;
100              
101 33 100       96 if ( !$refresh_token ) {
102 25 100       66 if ( $auth_header ) {
103 17         118 my ( $auth_type, $auth_access_token ) = split( / /, $auth_header );
104              
105 17 100       65 if ( $auth_type ne 'Bearer' ) {
106 1         5 return ( 0, 'invalid_request' );
107             }
108             else {
109 16         42 $access_token = $auth_access_token;
110             }
111             }
112             else {
113 8         39 return ( 0, 'invalid_request' );
114             }
115             }
116             else {
117 8         19 $access_token = $refresh_token;
118             }
119              
120 24         102 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   568 my $method = shift;
131 252         428 my $self = shift;
132 252         915 my %args = @_;
133              
134 252         599 my $cb_method = "${method}_cb";
135 252         569 my $p_method = "_$method";
136              
137 252 100       1153 if ( my $cb = $self->$cb_method ) {
138 116         450 return $cb->( %args );
139             }
140             else {
141 136         600 return $self->$p_method( %args );
142             }
143             }
144              
145 8     8   59 sub _login_resource_owner { 1 }
146              
147             sub _confirm_by_resource_owner {
148 8     8   45 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     78 ? ( 1,undef,$args{scopes} // [] )
155             : 1;
156             }
157              
158             sub _verify_client {
159 16     16   70 my ( $self, %args ) = @_;
160              
161 16         42 my ( $client_id, $scopes_ref ) = @args{ qw/ client_id scopes / };
162              
163 16 100 50     88 if ( my $client = $self->clients->{ $client_id // '' } ) {
164 12         27 my $client_scopes = [];
165              
166 12   50     25 foreach my $scope ( @{ $scopes_ref // [] } ) {
  12         44  
167 32 100       142 if ( ! exists($self->clients->{ $client_id }{ scopes }{ $scope }) ) {
    100          
168 4         30 return ( 0, 'invalid_scope' );
169             }
170             elsif ( $self->clients->{ $client_id }{ scopes }{ $scope } ) {
171 24         40 push @{$client_scopes}, $scope;
  24         83  
172             }
173             }
174              
175 8         57 return ( 1, undef, $client_scopes );
176             }
177              
178 4         30 return ( 0, 'unauthorized_client' );
179             }
180              
181             sub _store_access_token {
182 16     16   100 my ( $self, %args ) = @_;
183              
184             my ( $c_id, $auth_code, $access_token, $refresh_token, $expires_in, $scope, $old_refresh_token )
185             = @args{
186 16         77 qw/ client_id auth_code access_token refresh_token expires_in scopes old_refresh_token / };
187              
188 16   33     110 $expires_in //= $self->get_access_token_ttl(
189             scopes => $scope,
190             client_id => $c_id,
191             );
192              
193 16 100       105 return 1 if $self->jwt_secret;
194              
195 8 50 66     54 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       30 delete( $self->refresh_tokens->{ $old_refresh_token } )
213             if $old_refresh_token;
214              
215 8   100     77 $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       29 if ( $refresh_token ) {
223              
224 4 100       21 $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       27 if ( $self->_uses_auth_codes ) {
233 2         10 $self->auth_codes->{ $auth_code }{ access_token } = $access_token;
234             }
235              
236 8         60 return $c_id;
237             }
238              
239             sub _verify_access_token {
240 62     62   265 my ( $self, %args ) = @_;
241 62 100       259 return $self->_verify_access_token_jwt( %args ) if $self->jwt_secret;
242              
243             my ( $a_token, $scopes_ref, $is_refresh_token ) =
244 30         84 @args{ qw/ access_token scopes is_refresh_token / };
245              
246 30 100 100     200 if ( $is_refresh_token
    100          
247             && exists( $self->refresh_tokens->{ $a_token } ) )
248             {
249              
250 8 50       22 if ( $scopes_ref ) {
251 8   50     16 foreach my $scope ( @{ $scopes_ref // [] } ) {
  8         31  
252             return ( 0, 'invalid_grant' )
253 16 50       47 if !$self->_has_scope( $scope, $self->refresh_tokens->{ $a_token }{ scope } );
254             }
255             }
256              
257 8         63 return ( $self->refresh_tokens->{ $a_token }, undef );
258             }
259             elsif ( exists( $self->access_tokens->{ $a_token } ) ) {
260              
261 16 50       69 if ( $self->access_tokens->{ $a_token }{ expires } <= time ) {
    50          
262 0         0 $self->_revoke_access_token( $a_token );
263 0         0 return ( 0, 'invalid_grant' );
264             }
265             elsif ( $scopes_ref ) {
266              
267 16   50     25 foreach my $scope ( @{ $scopes_ref // [] } ) {
  16         71  
268             return ( 0, 'invalid_grant' )
269 24 100       71 if !$self->_has_scope( $scope, $self->access_tokens->{ $a_token }{ scope } );
270             }
271              
272             }
273              
274 8         62 return ( $self->access_tokens->{ $a_token }, undef );
275             }
276              
277 6         38 return ( 0, 'invalid_grant' );
278             }
279              
280             sub _has_scope {
281 120     120   262 my ( $self, $scope, $available_scopes ) = @_;
282 120   50     186 return scalar grep { $_ eq $scope } @{ $available_scopes // [] };
  240         881  
  120         329  
283             }
284              
285             sub _verify_access_token_jwt {
286 52     52   148 my ( $self, %args ) = @_;
287              
288             my ( $access_token, $scopes_ref, $is_refresh_token ) =
289 52         140 @args{ qw/ access_token scopes is_refresh_token / };
290              
291 52         96 my $access_token_payload;
292              
293             my $invalid_jwt;
294             try {
295 52     52   3941 $access_token_payload = decode_jwt(
296             alg => $self->jwt_algorithm,
297             key => $self->jwt_secret,
298             token => $access_token,
299             );
300             }
301             catch {
302 16     16   5785 $invalid_jwt = 1;
303 52         418 };
304 52 100       7243 return ( 0, 'invalid_grant' ) if $invalid_jwt;
305              
306 36 50 66     255 if (
      66        
307             $access_token_payload
308             && ( $access_token_payload->{ type } eq 'access'
309             || $is_refresh_token && $access_token_payload->{ type } eq 'refresh' )
310             )
311             {
312              
313 36 50       105 if ( $scopes_ref ) {
314 36   50     68 foreach my $scope ( @{ $scopes_ref // [] } ) {
  36         113  
315             return ( 0, 'invalid_grant' )
316 60 100       166 if !$self->_has_scope( $scope, $access_token_payload->{ scopes } );
317             }
318             }
319              
320 24         223 return ( $access_token_payload, undef, $access_token_payload->{scopes} );
321             }
322              
323 0         0 return ( 0, 'invalid_grant' );
324             }
325              
326             sub _revoke_access_token {
327 2     2   7 my ( $self, $access_token ) = @_;
328 2         11 delete( $self->access_tokens->{ $access_token } );
329             }
330              
331             sub get_access_token_ttl {
332 46     46 0 2412 my ( $self, %args ) = @_;
333              
334 46 100       295 return ref $self->access_token_ttl eq 'CODE'
335             ? $self->access_token_ttl->( %args )
336             : $self->access_token_ttl;
337             }
338              
339             sub token {
340 31     31 0 115239 my ( $self, %args ) = @_;
341              
342             my ( $client_id, $scopes, $type, $redirect_uri, $user_id, $claims ) =
343 31         144 @args{ qw/ client_id scopes type redirect_uri user_id jwt_claims_cb / };
344              
345 31 50 66     143 if (
346             ! $self->_uses_auth_codes
347             && $type eq 'auth'
348             ) {
349 0         0 croak "Invalid type for ->token ($type)";
350             }
351              
352 31 100       193 my $ttl = $type eq 'auth' ? $self->auth_code_ttl : $self->get_access_token_ttl(
353             scopes => $scopes,
354             client_id => $client_id,
355             );
356              
357 31 100       98 undef( $ttl ) if $type eq 'refresh';
358 31         72 my $code;
359              
360 31 100       112 if ( !$self->jwt_secret ) {
361 14         65 my ( $sec, $usec ) = gettimeofday;
362 14         232 $code = encode_base64( join( '-', $sec, $usec, rand(), random_string( 30 ) ), '' );
363             }
364             else {
365 17 100       115 if ( $self->jwt_algorithm =~ /none/i ) {
366 1         26 croak "A jwt_algorithm of 'none' is not supported as this is insecure";
367             }
368              
369 16         78 my $jti = random_string( 32 );
370              
371 16 100       4758 $code = encode_jwt(
    100          
372             allow_none => 0, # do NOT allow the "none" algorithm, as this is massively insecure
373             # we actually already check this above, but this is here a backup
374             alg => $self->jwt_algorithm,
375             key => $self->jwt_secret,
376             enc => $self->jwt_encoding,
377              
378             ( $ttl ? ( relative_exp => $ttl ) : () ),
379             auto_iat => 1,
380              
381             # https://tools.ietf.org/html/rfc7519#section-4
382             payload => {
383             # Registered Claim Names
384             aud => $redirect_uri, # the "audience"
385             jti => $jti,
386              
387             # Private Claim Names
388             user_id => $user_id,
389             client => $client_id,
390             type => $type,
391             scopes => $scopes,
392              
393             ( $claims
394             ? ( $claims->({
395             user_id => $user_id,
396             client_id => $client_id,
397             type => $type,
398             scopes => $scopes,
399             redirect_uri => $redirect_uri,
400             jti => $jti,
401             }) )
402             : ()
403             ),
404             },
405             );
406             }
407              
408 30         29424 return $code;
409             }
410              
411             __PACKAGE__->meta->make_immutable;