File Coverage

lib/LWP/Authen/OAuth2/ServiceProvider.pm
Criterion Covered Total %
statement 61 166 36.7
branch 5 52 9.6
condition 0 6 0.0
subroutine 24 35 68.5
pod 10 26 38.4
total 100 285 35.0


line stmt bran cond sub pod time code
1             package LWP::Authen::OAuth2::ServiceProvider;
2              
3             # ABSTRACT: ServiceProvider base class
4             our $VERSION = '0.19'; # VERSION
5              
6 8     8   55883 use 5.006;
  8         32  
7 8     8   69 use strict;
  8         77  
  8         203  
8 8     8   39 use warnings;
  8         16  
  8         250  
9              
10 8     8   42 use Carp qw(confess croak);
  8         16  
  8         394  
11 8     8   492 use JSON qw(decode_json);
  8         9903  
  8         50  
12 8     8   4923 use Memoize qw(memoize);
  8         15323  
  8         416  
13 8     8   434 use Module::Load qw(load);
  8         850  
  8         47  
14 8     8   801 use URI;
  8         3747  
  8         311  
15              
16             our @CARP_NOT = qw(LWP::Authen::OAuth2 LWP::Authen::OAuth2::Args);
17              
18 8         13708 use LWP::Authen::OAuth2::Args qw(
19             extract_option copy_option assert_options_empty
20 8     8   460 );
  8         12  
21              
22             # Construct a new object.
23             sub new {
24 6     6 0 16 my ($class, $opts) = @_;
25              
26             # I start as an empty hashref.
27 6         12 my $self = {};
28              
29             # But what class am I supposed to actually be?
30 6 50       22 if (not exists $opts->{service_provider}) {
31 0         0 bless $self, $class;
32             }
33             else {
34             # Convert "Google" to "LWP::Authen::OAuth2::ServiceProvider::Google"
35             # Not a method because no object yet exists.
36 6         123 $class = service_provider_class(delete $opts->{service_provider});
37 6         56 my $client_type = delete $opts->{client_type};
38 6 50       20 if (not defined($client_type)) {
39 6         12 $client_type = "default";
40             }
41 6         32 bless $self, $class->client_type_class($client_type);
42             }
43              
44 6         27 $self->init($opts);
45             }
46              
47             sub init {
48 6     6 1 13 my ($self, $opts) = @_;
49              
50             # Now let us consume options. 2 args = required, 3 = defaulted.
51             # In general subclasses should Just Work.
52              
53             # need to read this first, since the later opts depend on it
54 6 50       33 $self->copy_option($opts, 'use_test_urls') if defined $opts->{use_test_urls};
55              
56             # These are required, NOT provided by this class, but are by subclasses.
57 6         23 for my $field (qw(token_endpoint authorization_endpoint)) {
58 12 50       66 if ($self->can($field)) {
59 12         51 $self->copy_option($opts, $field, $self->$field);
60             }
61             else {
62 0         0 $self->copy_option($opts, $field);
63             }
64             }
65              
66             # These are defaulted by this class, maybe overridden by subclasses.
67 6         14 for my $field (
68             qw(required_init optional_init),
69             map {
70 18         55 ("$_\_required_params", "$_\_optional_params")
71             } qw(authorization request refresh)
72             ) {
73 48         181 $self->copy_option($opts, $field, [$self->$field]);
74             }
75              
76             # And hashrefs for default key/value pairs.
77 6         30 for my $field (
78             map "$_\_default_params", qw(authorization request refresh)
79             ) {
80 18         78 $self->copy_option($opts, $field, {$self->$field});
81             }
82              
83 6         20 return $self;
84             }
85              
86             sub authorization_url {
87 0     0 0 0 my ($self, $oauth2, @rest) = @_;
88 0         0 my $param
89             = $self->collect_action_params("authorization", $oauth2, @rest);
90 0         0 my $uri = URI->new($self->authorization_endpoint());
91 0         0 $uri->query_form(%$param);
92 0         0 return $uri->as_string;
93             }
94              
95             sub request_tokens {
96 0     0 0 0 my ($self, $oauth2, @rest) = @_;
97 0         0 my $param = $self->collect_action_params("request", $oauth2, @rest);
98 0         0 my $response = $self->post_to_token_endpoint($oauth2, $param);
99 0         0 return $self->construct_tokens($oauth2, $response);
100             }
101              
102             sub can_refresh_tokens {
103 0     0 0 0 my ($self, $oauth2, %opt) = @_;
104 0         0 my %default = $self->refresh_default_params;
105 0         0 my $oauth2_args = $oauth2->for_service_provider;
106 0         0 for my $param ($self->refresh_required_params) {
107 0 0 0     0 if ( exists $default{$param}
      0        
108             or exists $oauth2_args->{$param}
109             or exists $opt{$param}
110             ) {
111 0         0 next;
112             }
113             else {
114 0         0 return 0;
115             }
116             }
117 0         0 return 1;
118             }
119              
120             sub refreshed_tokens {
121 0     0 0 0 my ($self, $oauth2, @rest) = @_;
122 0         0 my $param = $self->collect_action_params("refresh", $oauth2, @rest);
123 0         0 my $response = $self->post_to_token_endpoint($oauth2, $param);
124             # Error message or object, this is what we return.
125 0         0 return $self->construct_tokens($oauth2, $response);
126             }
127              
128             sub collect_action_params {
129 0     0 1 0 my $self = shift;
130 0         0 my $action = shift;
131 0         0 my $oauth2 = shift;
132 0         0 my $oauth2_args = $oauth2->for_service_provider;
133 0         0 my @rest = @_;
134 0         0 my $opt = {@_};
135              
136 0         0 my $default = $self->{"$action\_default_params"};
137              
138 0 0       0 if ($oauth2->is_strict) {
139             # We copy one by one with testing.
140 0         0 my $result = {};
141 0         0 for my $param (@{ $self->{"$action\_required_params"}}) {
  0         0  
142 0 0       0 if (exists $opt->{$param}) {
    0          
    0          
143 0 0       0 if (defined $opt->{$param}) {
144 0         0 $result->{$param} = delete $opt->{$param};
145             }
146             else {
147 0         0 croak("Cannot pass undef for required param '$param'");
148             }
149             }
150             elsif (defined $oauth2_args->{$param}) {
151 0         0 $result->{$param} = $oauth2_args->{$param};
152             }
153             elsif (defined $default->{$param}) {
154 0         0 $result->{$param} = $default->{$param};
155             }
156             else {
157 0         0 croak("Missing required param '$param'");
158             }
159             }
160              
161 0         0 for my $param (@{ $self->{"$action\_optional_params"} }) {
  0         0  
162 0         0 for my $source ($result, $opt, $oauth2_args, $default) {
163 0 0       0 if (exists $source->{$param}) {
164             # Only add it if it is not undef. Else hide.
165 0 0       0 if (defined $source->{$param}) {
166 0         0 $result->{$param} = $source->{$param};
167             }
168              
169             # For opt only, delete if it was found.
170 0 0       0 if ($opt == $source) {
171 0         0 delete $opt->{$param};
172             }
173              
174 0         0 last; # source
175             # (undef is deliberate override, which is OK)
176             }
177             }
178             }
179              
180 0         0 $self->assert_options_empty($opt);
181              
182             # End of strict section.
183 0         0 return $result;
184             }
185             else {
186             # Not strict just bulk copy.
187             my $result = {
188             %$default,
189             (
190 0         0 map {($_, $oauth2_args->{$_})}
191 0         0 @{ $self->{"$action\_required_params"} },
192 0         0 @{ $self->{"$action\_optional_params"} }
  0         0  
193             ),
194             %$opt
195             };
196 0         0 for my $key (keys %$result) {
197 0 0       0 if (not defined($result->{$key})) {
198 0         0 delete $result->{$key};
199             }
200             }
201 0         0 return $result;
202             }
203             }
204              
205             sub post_to_token_endpoint {
206 0     0 1 0 my ($self, $oauth2, $param) = @_;
207 0         0 my $ua = $oauth2->user_agent();
208 0         0 return $ua->post($self->token_endpoint(), [%$param]);
209             }
210              
211 0     0 0 0 sub api_url_base { return '' } # override in subclass
212              
213             sub access_token_class {
214 0     0 1 0 my ($self, $type) = @_;
215              
216 0 0       0 if ("bearer" eq $type) {
217 0         0 return "LWP::Authen::OAuth2::AccessToken::Bearer";
218             }
219             else {
220 0         0 return "Token type '$type' not yet implemented";
221             }
222             }
223              
224             # Attempts to construct tokens, returns the access_token (which may have a
225             # request token embedded).
226             sub construct_tokens {
227 0     0 0 0 my ($self, $oauth2, $response) = @_;
228              
229             # The information that I need.
230 0         0 my $content = eval {$response->decoded_content};
  0         0  
231 0 0       0 if (not defined($content)) {
232 0         0 $content = '';
233             }
234 0         0 my $data = eval {decode_json($content)};
  0         0  
235 0         0 my $parse_error = $@;
236 0         0 my $token_endpoint = $self->token_endpoint();
237              
238             # Can this have done wrong? Let me list the ways...
239 0 0       0 if ($parse_error) {
    0          
    0          
240             # "Should not happen", hopefully just network.
241             # Tell the programmer everything.
242 0         0 my $status = $response->status_line;
243             return <<"EOT"
244             Token endpoint gave invalid JSON in response.
245              
246             Endpoint: $token_endpoint
247             Status: $status
248             Parse error: $parse_error
249             JSON:
250             $content
251             EOT
252 0         0 }
253             elsif ($data->{error}) {
254             # Assume a valid OAuth 2 error message.
255 0         0 my $message = "OAuth2 error: $data->{error}";
256              
257             # Do we have a mythical service provider that gives us more?
258 0 0       0 if ($data->{error_uri}) {
259             # They seem to have a web page with detail.
260 0         0 $message .= "\n$data->{error_uri} may say more.\n";
261             }
262              
263 0 0       0 if ($data->{error_description}) {
264             # Wow! Thank you!
265 0         0 $message .= "\n\nDescription: $data->{error_description}\n";
266             }
267 0         0 return $message;
268             }
269             elsif (not $data->{token_type}) {
270             # Someone failed to follow the spec...
271 0         0 return <<"EOT";
272             Token endpoint missing expected token_type in successful response.
273              
274             Endpoint: $token_endpoint
275             JSON:
276             $content
277             EOT
278             }
279              
280 0         0 my $type = $self->access_token_class(lc($data->{token_type}));
281 0 0       0 if ($type !~ /^[\w\:]+\z/) {
282             # We got an error. :-(
283 0         0 return $type;
284             }
285              
286 0         0 eval {load($type)};
  0         0  
287 0 0       0 if ($@) {
288             # MAKE THIS FATAL. (Clearly Perl code is simply wrong.)
289 0         0 confess("Loading $type for $data->{token_type} gave error: $@");
290             }
291              
292             # Try to make an access token.
293 0         0 my $access_token = $type->from_ref($data);
294              
295 0 0       0 if (not ref($access_token)) {
296             # This should be an error message of some sort.
297 0         0 return $access_token;
298             }
299             else {
300             # WE SURVIVED! EVERYTHING IS GOOD!
301 0 0       0 if ($oauth2->access_token) {
302 0         0 $access_token->copy_refresh_from($oauth2->access_token);
303             }
304 0         0 return $access_token;
305             }
306             }
307              
308             # Override for your client_types if you have multiple.
309             sub client_type_class {
310 5     5 1 29 my ($class, $name) = @_;
311 5 50       12 if ("default" eq $name) {
312 5         15 return $class;
313             }
314             else {
315 0         0 croak("Flow '$name' not defined for '$class'");
316             }
317             }
318              
319             # Override should you need the front-end LWP::Authen::OAuth object to have
320             # methods for service provider specific functionality.
321             #
322             # This is not expected to be a common need.
323             sub oauth2_class {
324 6     6 1 18 return "LWP::Authen::OAuth2";
325             }
326              
327             memoize("service_provider_class");
328             sub service_provider_class {
329             my $short_name = shift;
330             eval {
331             load("LWP::Authen::OAuth2::ServiceProvider::$short_name");
332             };
333             if (not $@) {
334             return "LWP::Authen::OAuth2::ServiceProvider::$short_name";
335             }
336             elsif ($@ =~ /Compilation failed/) {
337             confess($@);
338             }
339             else {
340             eval {
341             load($short_name);
342             };
343             if (not $@) {
344             return $short_name;
345             }
346             elsif ($@ =~ /Compilation failed/) {
347             confess($@);
348             }
349             else {
350             croak("Service provider '$short_name' not found");
351             }
352             }
353             }
354              
355             # DEFAULTS (can be overridden)
356             sub authorization_endpoint {
357 0     0 1 0 my $self = shift;
358 0         0 return $self->{"authorization_endpoint"};
359             }
360              
361             sub token_endpoint {
362 0     0 1 0 my $self = shift;
363 0         0 return $self->{"token_endpoint"};
364             }
365              
366             # DEFAULTS (should be overridden)
367             sub required_init {
368 4     4 1 21 return qw(client_id client_secret);
369             }
370              
371             sub optional_init {
372 5     5 1 15 return qw(redirect_uri scope);
373             }
374              
375             sub authorization_required_params {
376 4     4 0 28 return qw(response_type client_id);
377             }
378              
379             sub authorization_optional_params {
380 6     6 0 37 return qw(redirect_uri state scope);
381             }
382              
383             sub authorization_default_params {
384 4     4 0 18 return qw(response_type code);
385             }
386              
387             sub request_required_params {
388 4     4 0 21 return qw(grant_type client_id client_secret code);
389             }
390              
391             sub request_optional_params {
392 6     6 0 16 return qw(state redirect_uri);
393             }
394              
395             sub request_default_params {
396 4     4 0 23 return qw(grant_type authorization_code);
397             }
398              
399             sub refresh_required_params {
400 5     5 0 26 return qw(grant_type refresh_token client_id client_secret);
401             }
402              
403             sub refresh_optional_params {
404 6     6 0 22 return qw();
405             }
406              
407             sub refresh_default_params {
408 5     5 0 13 return qw(grant_type refresh_token);
409             }
410              
411              
412             1
413              
414             __END__