File Coverage

lib/LWP/Authen/OAuth2/ServiceProvider.pm
Criterion Covered Total %
statement 82 166 49.4
branch 11 54 20.3
condition 0 6 0.0
subroutine 27 35 77.1
pod 10 26 38.4
total 130 287 45.3


line stmt bran cond sub pod time code
1             package LWP::Authen::OAuth2::ServiceProvider;
2              
3             # ABSTRACT: ServiceProvider base class
4             our $VERSION = '0.20'; # VERSION
5              
6 8     8   212 use 5.006;
  8         34  
7 8     8   56 use strict;
  8         32  
  8         294  
8 8     8   61 use warnings;
  8         30  
  8         279  
9              
10 8     8   58 use Carp qw(confess croak);
  8         20  
  8         512  
11 8     8   63 use JSON qw(decode_json);
  8         29  
  8         63  
12 8     8   6237 use Memoize qw(memoize);
  8         20754  
  8         482  
13 8     8   74 use Module::Load qw(load);
  8         116  
  8         50  
14 8     8   462 use URI;
  8         68  
  8         367  
15              
16             our @CARP_NOT = qw(LWP::Authen::OAuth2 LWP::Authen::OAuth2::Args);
17              
18 8         18272 use LWP::Authen::OAuth2::Args qw(
19             extract_option copy_option assert_options_empty
20 8     8   49 );
  8         18  
21              
22             # Construct a new object.
23             sub new {
24 8     8 0 27 my ($class, $opts) = @_;
25              
26             # I start as an empty hashref.
27 8         21 my $self = {};
28              
29             # But what class am I supposed to actually be?
30 8 100       33 if (not exists $opts->{service_provider}) {
31 2         4 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         149 $class = service_provider_class(delete $opts->{service_provider});
37 6         64 my $client_type = delete $opts->{client_type};
38 6 50       24 if (not defined($client_type)) {
39 6         15 $client_type = "default";
40             }
41 6         38 bless $self, $class->client_type_class($client_type);
42             }
43              
44 8         39 $self->init($opts);
45             }
46              
47             sub init {
48 8     8 1 23 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 8 50       37 $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 8         21 for my $field (qw(token_endpoint authorization_endpoint)) {
58 16 50       115 if ($self->can($field)) {
59 16         68 $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 8         31 for my $field (
68             qw(required_init optional_init),
69             map {
70 24         88 ("$_\_required_params", "$_\_optional_params")
71             } qw(authorization request refresh)
72             ) {
73 64         252 $self->copy_option($opts, $field, [$self->$field]);
74             }
75              
76             # And hashrefs for default key/value pairs.
77 8         47 for my $field (
78             map "$_\_default_params", qw(authorization request refresh)
79             ) {
80 24         85 $self->copy_option($opts, $field, {$self->$field});
81             }
82              
83 8         32 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 1     1 1 5 my $self = shift;
130 1         2 my $action = shift;
131 1         2 my $oauth2 = shift;
132 1         3 my $oauth2_args = $oauth2->for_service_provider;
133 1         3 my @rest = @_;
134 1         2 my $opt = {@_};
135              
136 1         3 my $default = $self->{"$action\_default_params"};
137              
138 1 50       4 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             map {
191 6 100       28 exists $oauth2_args->{$_} ? ($_, $oauth2_args->{$_}) : ()
192 1         18 } @{ $self->{"$action\_required_params"} },
193 1         5 @{ $self->{"$action\_optional_params"} }
  1         12  
194             ),
195             %$opt
196             };
197 1         6 for my $key (keys %$result) {
198 4 100       10 if (not defined($result->{$key})) {
199 1         3 delete $result->{$key};
200             }
201             }
202 1         5 return $result;
203             }
204             }
205              
206             sub post_to_token_endpoint {
207 0     0 1 0 my ($self, $oauth2, $param) = @_;
208 0         0 my $ua = $oauth2->user_agent();
209 0         0 return $ua->post($self->token_endpoint(), [%$param]);
210             }
211              
212 0     0 0 0 sub api_url_base { return '' } # override in subclass
213              
214             sub access_token_class {
215 0     0 1 0 my ($self, $type) = @_;
216              
217 0 0       0 if ("bearer" eq $type) {
218 0         0 return "LWP::Authen::OAuth2::AccessToken::Bearer";
219             }
220             else {
221 0         0 return "Token type '$type' not yet implemented";
222             }
223             }
224              
225             # Attempts to construct tokens, returns the access_token (which may have a
226             # request token embedded).
227             sub construct_tokens {
228 0     0 0 0 my ($self, $oauth2, $response) = @_;
229              
230             # The information that I need.
231 0         0 my $content = eval {$response->decoded_content};
  0         0  
232 0 0       0 if (not defined($content)) {
233 0         0 $content = '';
234             }
235 0         0 my $data = eval {decode_json($content)};
  0         0  
236 0         0 my $parse_error = $@;
237 0         0 my $token_endpoint = $self->token_endpoint();
238              
239             # Can this have done wrong? Let me list the ways...
240 0 0       0 if ($parse_error) {
    0          
    0          
241             # "Should not happen", hopefully just network.
242             # Tell the programmer everything.
243 0         0 my $status = $response->status_line;
244             return <<"EOT"
245             Token endpoint gave invalid JSON in response.
246              
247             Endpoint: $token_endpoint
248             Status: $status
249             Parse error: $parse_error
250             JSON:
251             $content
252             EOT
253 0         0 }
254             elsif ($data->{error}) {
255             # Assume a valid OAuth 2 error message.
256 0         0 my $message = "OAuth2 error: $data->{error}";
257              
258             # Do we have a mythical service provider that gives us more?
259 0 0       0 if ($data->{error_uri}) {
260             # They seem to have a web page with detail.
261 0         0 $message .= "\n$data->{error_uri} may say more.\n";
262             }
263              
264 0 0       0 if ($data->{error_description}) {
265             # Wow! Thank you!
266 0         0 $message .= "\n\nDescription: $data->{error_description}\n";
267             }
268 0         0 return $message;
269             }
270             elsif (not $data->{token_type}) {
271             # Someone failed to follow the spec...
272 0         0 return <<"EOT";
273             Token endpoint missing expected token_type in successful response.
274              
275             Endpoint: $token_endpoint
276             JSON:
277             $content
278             EOT
279             }
280              
281 0         0 my $type = $self->access_token_class(lc($data->{token_type}));
282 0 0       0 if ($type !~ /^[\w\:]+\z/) {
283             # We got an error. :-(
284 0         0 return $type;
285             }
286              
287 0         0 eval {load($type)};
  0         0  
288 0 0       0 if ($@) {
289             # MAKE THIS FATAL. (Clearly Perl code is simply wrong.)
290 0         0 confess("Loading $type for $data->{token_type} gave error: $@");
291             }
292              
293             # Try to make an access token.
294 0         0 my $access_token = $type->from_ref($data);
295              
296 0 0       0 if (not ref($access_token)) {
297             # This should be an error message of some sort.
298 0         0 return $access_token;
299             }
300             else {
301             # WE SURVIVED! EVERYTHING IS GOOD!
302 0 0       0 if ($oauth2->access_token) {
303 0         0 $access_token->copy_refresh_from($oauth2->access_token);
304             }
305 0         0 return $access_token;
306             }
307             }
308              
309             # Override for your client_types if you have multiple.
310             sub client_type_class {
311 5     5 1 14 my ($class, $name) = @_;
312 5 50       18 if ("default" eq $name) {
313 5         16 return $class;
314             }
315             else {
316 0         0 croak("Flow '$name' not defined for '$class'");
317             }
318             }
319              
320             # Override should you need the front-end LWP::Authen::OAuth object to have
321             # methods for service provider specific functionality.
322             #
323             # This is not expected to be a common need.
324             sub oauth2_class {
325 7     7 1 39 return "LWP::Authen::OAuth2";
326             }
327              
328             memoize("service_provider_class");
329             sub service_provider_class {
330             my $short_name = shift;
331             eval {
332             load("LWP::Authen::OAuth2::ServiceProvider::$short_name");
333             };
334             if (not $@) {
335             return "LWP::Authen::OAuth2::ServiceProvider::$short_name";
336             }
337             elsif ($@ =~ /Compilation failed/) {
338             confess($@);
339             }
340             else {
341             eval {
342             load($short_name);
343             };
344             if (not $@) {
345             return $short_name;
346             }
347             elsif ($@ =~ /Compilation failed/) {
348             confess($@);
349             }
350             else {
351             croak("Service provider '$short_name' not found");
352             }
353             }
354             }
355              
356             # DEFAULTS (can be overridden)
357             sub authorization_endpoint {
358 2     2 1 4 my $self = shift;
359 2         5 return $self->{"authorization_endpoint"};
360             }
361              
362             sub token_endpoint {
363 2     2 1 5 my $self = shift;
364 2         8 return $self->{"token_endpoint"};
365             }
366              
367             # DEFAULTS (should be overridden)
368             sub required_init {
369 6     6 1 25 return qw(client_id client_secret);
370             }
371              
372             sub optional_init {
373 7     7 1 29 return qw(redirect_uri scope);
374             }
375              
376             sub authorization_required_params {
377 6     6 0 30 return qw(response_type client_id);
378             }
379              
380             sub authorization_optional_params {
381 8     8 0 42 return qw(redirect_uri state scope);
382             }
383              
384             sub authorization_default_params {
385 6     6 0 50 return qw(response_type code);
386             }
387              
388             sub request_required_params {
389 6     6 0 28 return qw(grant_type client_id client_secret code);
390             }
391              
392             sub request_optional_params {
393 8     8 0 38 return qw(state redirect_uri);
394             }
395              
396             sub request_default_params {
397 6     6 0 41 return qw(grant_type authorization_code);
398             }
399              
400             sub refresh_required_params {
401 7     7 0 43 return qw(grant_type refresh_token client_id client_secret);
402             }
403              
404             sub refresh_optional_params {
405 8     8 0 32 return qw();
406             }
407              
408             sub refresh_default_params {
409 7     7 0 35 return qw(grant_type refresh_token);
410             }
411              
412              
413             1
414              
415             __END__