File Coverage

lib/Google/Ads/Common/OAuth2ServiceAccountsHandler.pm
Criterion Covered Total %
statement 39 109 35.7
branch 0 14 0.0
condition 0 27 0.0
subroutine 13 21 61.9
pod 1 2 50.0
total 53 173 30.6


line stmt bran cond sub pod time code
1             # Copyright 2013, Google Inc. All Rights Reserved.
2             #
3             # Licensed under the Apache License, Version 2.0 (the "License");
4             # you may not use this file except in compliance with the License.
5             # You may obtain a copy of the License at
6             #
7             # http://www.apache.org/licenses/LICENSE-2.0
8             #
9             # Unless required by applicable law or agreed to in writing, software
10             # distributed under the License is distributed on an "AS IS" BASIS,
11             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12             # See the License for the specific language governing permissions and
13             # limitations under the License.
14              
15             package Google::Ads::Common::OAuth2ServiceAccountsHandler;
16              
17 1     1   711 use strict;
  1         2  
  1         23  
18 1     1   4 use warnings;
  1         2  
  1         24  
19 1     1   4 use version;
  1         2  
  1         5  
20 1     1   55 use base qw(Google::Ads::Common::OAuth2BaseHandler);
  1         2  
  1         252  
21              
22             # The following needs to be on one line because CPAN uses a particularly hacky
23             # eval() to determine module versions.
24 1     1   6 use Google::Ads::Common::Constants; our $VERSION = ${Google::Ads::Common::Constants::VERSION};
  1         2  
  1         37  
25              
26 1     1   445 use Crypt::OpenSSL::RSA;
  1         5021  
  1         40  
27 1     1   8 use HTTP::Request;
  1         3  
  1         26  
28 1     1   385 use JSON::Parse qw(parse_json);
  1         857  
  1         54  
29 1     1   8 use LWP::UserAgent;
  1         8  
  1         29  
30 1     1   5 use MIME::Base64 qw(encode_base64 decode_base64);
  1         1  
  1         53  
31 1     1   5 use utf8;
  1         2  
  1         8  
32              
33 1     1   25 use constant OAUTH2_BASE_URL => "https://accounts.google.com/o/oauth2";
  1         3  
  1         272  
34              
35             # Class::Std-style attributes. Need to be kept in the same line.
36             # These need to go in the same line for older Perl interpreters to understand.
37             my %email_address_of : ATTR(:name :default<>);
38             my %delegated_email_address_of :
39             ATTR(:name :default<>);
40             my %additional_scopes_of : ATTR(:name :default<>);
41             my %pem_file_of : ATTR(:name :default<>);
42             my %json_file_of : ATTR(:name :default<>);
43             my %__crypt_module_of : ATTR(:name<__crypt_module> :default<>);
44              
45             # Constructor
46             sub START {
47 0     0 0   my ($self, $ident) = @_;
48              
49 0   0       $__crypt_module_of{$ident} ||= "Crypt::OpenSSL::RSA";
50             }
51              
52             sub initialize : CUMULATIVE(BASE FIRST) {
53 0     0 1   my ($self, $api_client, $properties) = @_;
54 0           my $ident = ident $self;
55              
56             $email_address_of{$ident} = $properties->{oAuth2ServiceAccountEmailAddress}
57 0   0       || $email_address_of{$ident};
58             $delegated_email_address_of{$ident} =
59             $properties->{oAuth2ServiceAccountDelegateEmailAddress}
60 0   0       || $delegated_email_address_of{$ident};
61             $pem_file_of{$ident} = $properties->{oAuth2ServiceAccountPEMFile}
62 0   0       || $pem_file_of{$ident};
63             $json_file_of{$ident} = $properties->{oAuth2ServiceAccountJSONFile}
64 0   0       || $json_file_of{$ident};
65             $additional_scopes_of{$ident} = $properties->{oAuth2AdditionalScopes}
66 0   0       || $additional_scopes_of{$ident};
67 1     1   6 }
  1         3  
  1         9  
68              
69             sub _refresh_access_token {
70 0     0     my $self = shift;
71              
72 0 0 0       if ($self->get_json_file() and $self->get_pem_file()) {
73 0           warn("Only one of oAuth2ServiceAccountPEMFile or " .
74             "oAuth2ServiceAccountJSONFile can be specified in adwords.properties.");
75 0           return 0;
76             }
77              
78 0   0       my $file = $self->__read_certificate_file() || return 0;
79              
80 0           my $iat = time;
81 0           my $exp = $iat + 3600;
82 0           my $iss = $self->get_email_address();
83 0           my $delegated_email_address = $self->get_delegated_email_address();
84 0           my $scope = $self->_formatted_scopes();
85              
86 0           my $header = '{"alg":"RS256","typ":"JWT"}';
87 0           my $claims = "{
88             \"iss\":\"${iss}\",
89             \"sub\":\"${delegated_email_address}\",
90             \"scope\":\"${scope}\",
91             \"aud\":\"" . OAUTH2_BASE_URL . "/token\",
92             \"exp\":${exp},
93             \"iat\":${iat}
94             }";
95              
96 0           my $encoded_header = __encode_base64_url($header);
97 0           my $encoded_claims = __encode_base64_url($claims);
98              
99 0   0       my $key = $self->get___crypt_module()->new_private_key($file) || return 0;
100 0           $key->use_pkcs1_padding();
101 0           $key->use_sha256_hash();
102              
103 0           my $signature = $key->sign("${encoded_header}.${encoded_claims}");
104 0           my $encoded_signature = __encode_base64_url($signature);
105 0           my $assertion = "${encoded_header}.${encoded_claims}.${encoded_signature}";
106 0           my $body =
107             "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" .
108             "&assertion=" . $assertion;
109 0           push my @headers, "Content-Type" => "application/x-www-form-urlencoded";
110 0           my $request =
111             HTTP::Request->new("POST", OAUTH2_BASE_URL . "/token", \@headers, $body);
112 0           my $user_agent = $self->get___user_agent();
113 0           my $res = $user_agent->request($request);
114              
115 0 0         if (!$res->is_success()) {
116 0           warn($res->decoded_content());
117 0           return 0;
118             }
119              
120 0           my $content_hash = $self->__parse_auth_response($res->decoded_content());
121              
122 0           $self->set_access_token($content_hash->{access_token});
123 0           $self->set_access_token_expires($iat + $content_hash->{expires_in});
124             }
125              
126             # Return the private key string from either the PEM file or JSON file specified.
127             sub __read_certificate_file {
128 0     0     my $self = shift;
129 0           my $private_key;
130              
131 0 0 0       if (!$self->get_pem_file() and !$self->get_json_file()) {
132 0           return 0;
133             }
134              
135             # JSON File
136 0 0         if ($self->get_json_file()) {
137 0           my $file_str;
138 0 0         open(MYFILE, $self->get_json_file()) || return 0;
139 0           while () {
140 0           $file_str .= $_;
141             }
142 0           my $json_values = parse_json ($file_str);
143 0           $private_key = $json_values->{'private_key'};
144 0           $self->set_email_address($json_values->{'client_email'});
145 0           close(MYFILE);
146             }
147             # PEM File
148             else {
149 0 0         open(MYFILE, $self->get_pem_file()) || return 0;
150 0           while () {
151 0           $private_key .= $_;
152             }
153 0           close(MYFILE);
154             }
155              
156 0           return $private_key;
157             }
158              
159             sub __encode_base64_url($) {
160 0     0     my ($s) = shift;
161 0           $s = encode_base64($s);
162 0           $s =~ tr{+/}{-_};
163 0           $s =~ s/=*$//;
164 0           $s =~ s/\n//g;
165 0           return $s;
166             }
167              
168             sub _scope {
169 0     0     my $self = shift;
170 0           die "Need to be implemented by subclass";
171             }
172              
173             sub _formatted_scopes {
174 0     0     my $self = shift;
175 0           die "Need to be implemented by subclass";
176             }
177              
178             sub _throw_error {
179 0     0     my ($self, $err_msg) = @_;
180              
181 0 0         $self->get_api_client()->get_die_on_faults() ? die($err_msg) : warn($err_msg);
182             }
183              
184             1;
185              
186             =pod
187              
188             =head1 NAME
189              
190             Google::Ads::Common::OAuth2ServiceAccountsHandler
191              
192             =head1 DESCRIPTION
193              
194             A generic abstract implementation of L
195             that supports OAuth2 for Service Accounts semantics.
196              
197             It is meant to be specialized and its L<_scope> and L<_formatted_scopes> methods
198             be properly implemented.
199              
200             =head1 ATTRIBUTES
201              
202             Each of these attributes can be set via
203             Google::Ads::Common::OAuth2ServiceAccountsHandler->new().
204              
205             Alternatively, there is a get_ and set_ method associated with each attribute
206             for retrieving or setting them dynamically.
207              
208             =head2 api_client
209              
210             A reference to the API client used to send requests.
211              
212             =head2 client_id
213              
214             OAuth2 client id obtained from the Google APIs Console.
215              
216             =head2 email_address
217              
218             Service account email address as found in the Google API Console.
219              
220             =head2 delegated_email_address
221              
222             Delegated email address of the accounts that has access to the API.
223              
224             =head2 pem_file
225              
226             Private key PEM file path. Keep in mind that the Google API Console generates
227             files in PKCS12 format and it should be converted to PEM format with no password
228             for this module to function.
229              
230             =head2 json_file
231              
232             JOSN file path. This contains the private key and client id needed for
233             authentication.
234              
235             =head2 access_token
236              
237             Stores an OAuth2 access token after the authorization flow is followed or for
238             you to manually set it in case you had it previously stored.
239             If this is manually set this handler will verify its validity before preparing
240             a request.
241              
242             =head2 additional_scopes
243              
244             Stores additional OAuth2 scopes as a comma-separated string.
245             The scope defines which services the tokens
246             are allowed to access e.g. https://www.googleapis.com/auth/analytics
247              
248             =head1 METHODS
249              
250             =head2 initialize
251              
252             Initializes the handler with properties for generating authorization requests.
253              
254             =head3 Parameters
255              
256             =over
257              
258             =item *
259              
260             A required I with a reference to the API client object handling the
261             requests against the API.
262              
263             =item *
264              
265             A hash reference with the following keys (with this example being for a simple
266             JSON keyfile):
267             {
268             # Refer to the documentation of the L property.
269             oAuth2ServiceAccountDelegateEmailAddress => "delegated-email-address",
270             # Refer to the documentation of the L property.
271             oAuth2ServiceAccountJSONFile => "json-file-path",
272             }
273              
274             =back
275              
276             =head2 is_auth_enabled
277              
278             Refer to L documentation of this
279             method.
280              
281             =head2 prepare_request
282              
283             Refer to L documentation of this
284             method.
285              
286             =head1 LICENSE AND COPYRIGHT
287              
288             Copyright 2013 Google Inc.
289              
290             Licensed under the Apache License, Version 2.0 (the "License");
291             you may not use this file except in compliance with the License.
292             You may obtain a copy of the License at
293              
294             http://www.apache.org/licenses/LICENSE-2.0
295              
296             Unless required by applicable law or agreed to in writing, software
297             distributed under the License is distributed on an "AS IS" BASIS,
298             WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
299             See the License for the specific language governing permissions and
300             limitations under the License.
301              
302             =head1 REPOSITORY INFORMATION
303              
304             $Rev: $
305             $LastChangedBy: $
306             $Id: $
307              
308             =cut