| OLD | NEW |
| 1 # Copyright 2014 Google Inc. All rights reserved. | 1 # Copyright 2014 Google Inc. All rights reserved. |
| 2 # | 2 # |
| 3 # Licensed under the Apache License, Version 2.0 (the "License"); | 3 # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 # you may not use this file except in compliance with 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 | 5 # You may obtain a copy of the License at |
| 6 # | 6 # |
| 7 # http://www.apache.org/licenses/LICENSE-2.0 | 7 # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 # | 8 # |
| 9 # Unless required by applicable law or agreed to in writing, software | 9 # Unless required by applicable law or agreed to in writing, software |
| 10 # distributed under the License is distributed on an "AS IS" BASIS, | 10 # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 # See the License for the specific language governing permissions and | 12 # See the License for the specific language governing permissions and |
| 13 # limitations under the License. | 13 # limitations under the License. |
| 14 | 14 |
| 15 """A service account credentials class. | 15 """oauth2client Service account credentials class.""" |
| 16 | |
| 17 This credentials class is implemented on top of rsa library. | |
| 18 """ | |
| 19 | 16 |
| 20 import base64 | 17 import base64 |
| 18 import copy |
| 19 import datetime |
| 20 import json |
| 21 import time | 21 import time |
| 22 | 22 |
| 23 from pyasn1.codec.ber import decoder | |
| 24 from pyasn1_modules.rfc5208 import PrivateKeyInfo | |
| 25 import rsa | |
| 26 | |
| 27 from oauth2client import GOOGLE_REVOKE_URI | 23 from oauth2client import GOOGLE_REVOKE_URI |
| 28 from oauth2client import GOOGLE_TOKEN_URI | 24 from oauth2client import GOOGLE_TOKEN_URI |
| 29 from oauth2client._helpers import _json_encode | 25 from oauth2client._helpers import _json_encode |
| 30 from oauth2client._helpers import _to_bytes | 26 from oauth2client._helpers import _from_bytes |
| 31 from oauth2client._helpers import _urlsafe_b64encode | 27 from oauth2client._helpers import _urlsafe_b64encode |
| 32 from oauth2client import util | 28 from oauth2client import util |
| 33 from oauth2client.client import AssertionCredentials | 29 from oauth2client.client import AssertionCredentials |
| 34 | 30 from oauth2client.client import EXPIRY_FORMAT |
| 35 | 31 from oauth2client.client import SERVICE_ACCOUNT |
| 36 class _ServiceAccountCredentials(AssertionCredentials): | 32 from oauth2client import crypt |
| 37 """Class representing a service account (signed JWT) credential.""" | 33 |
| 38 | 34 |
| 39 MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds | 35 _PASSWORD_DEFAULT = 'notasecret' |
| 40 | 36 _PKCS12_KEY = '_private_key_pkcs12' |
| 41 def __init__(self, service_account_id, service_account_email, | 37 _PKCS12_ERROR = r""" |
| 42 private_key_id, private_key_pkcs8_text, scopes, | 38 This library only implements PKCS#12 support via the pyOpenSSL library. |
| 43 user_agent=None, token_uri=GOOGLE_TOKEN_URI, | 39 Either install pyOpenSSL, or please convert the .p12 file |
| 44 revoke_uri=GOOGLE_REVOKE_URI, **kwargs): | 40 to .pem format: |
| 45 | 41 $ cat key.p12 | \ |
| 46 super(_ServiceAccountCredentials, self).__init__( | 42 > openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \ |
| 47 None, user_agent=user_agent, token_uri=token_uri, | 43 > openssl rsa > key.pem |
| 48 revoke_uri=revoke_uri) | 44 """ |
| 49 | 45 |
| 50 self._service_account_id = service_account_id | 46 |
| 47 class ServiceAccountCredentials(AssertionCredentials): |
| 48 """Service Account credential for OAuth 2.0 signed JWT grants. |
| 49 |
| 50 Supports |
| 51 |
| 52 * JSON keyfile (typically contains a PKCS8 key stored as |
| 53 PEM text) |
| 54 * ``.p12`` key (stores PKCS12 key and certificate) |
| 55 |
| 56 Makes an assertion to server using a signed JWT assertion in exchange |
| 57 for an access token. |
| 58 |
| 59 This credential does not require a flow to instantiate because it |
| 60 represents a two legged flow, and therefore has all of the required |
| 61 information to generate and refresh its own access tokens. |
| 62 |
| 63 Args: |
| 64 service_account_email: string, The email associated with the |
| 65 service account. |
| 66 signer: ``crypt.Signer``, A signer which can be used to sign content. |
| 67 scopes: List or string, (Optional) Scopes to use when acquiring |
| 68 an access token. |
| 69 private_key_id: string, (Optional) Private key identifier. Typically |
| 70 only used with a JSON keyfile. Can be sent in the |
| 71 header of a JWT token assertion. |
| 72 client_id: string, (Optional) Client ID for the project that owns the |
| 73 service account. |
| 74 user_agent: string, (Optional) User agent to use when sending |
| 75 request. |
| 76 kwargs: dict, Extra key-value pairs (both strings) to send in the |
| 77 payload body when making an assertion. |
| 78 """ |
| 79 |
| 80 MAX_TOKEN_LIFETIME_SECS = 3600 |
| 81 """Max lifetime of the token (one hour, in seconds).""" |
| 82 |
| 83 NON_SERIALIZED_MEMBERS = ( |
| 84 frozenset(['_signer']) | |
| 85 AssertionCredentials.NON_SERIALIZED_MEMBERS) |
| 86 """Members that aren't serialized when object is converted to JSON.""" |
| 87 |
| 88 # Can be over-ridden by factory constructors. Used for |
| 89 # serialization/deserialization purposes. |
| 90 _private_key_pkcs8_pem = None |
| 91 _private_key_pkcs12 = None |
| 92 _private_key_password = None |
| 93 |
| 94 def __init__(self, |
| 95 service_account_email, |
| 96 signer, |
| 97 scopes='', |
| 98 private_key_id=None, |
| 99 client_id=None, |
| 100 user_agent=None, |
| 101 **kwargs): |
| 102 |
| 103 super(ServiceAccountCredentials, self).__init__( |
| 104 None, user_agent=user_agent) |
| 105 |
| 51 self._service_account_email = service_account_email | 106 self._service_account_email = service_account_email |
| 107 self._signer = signer |
| 108 self._scopes = util.scopes_to_string(scopes) |
| 52 self._private_key_id = private_key_id | 109 self._private_key_id = private_key_id |
| 53 self._private_key = _get_private_key(private_key_pkcs8_text) | 110 self.client_id = client_id |
| 54 self._private_key_pkcs8_text = private_key_pkcs8_text | |
| 55 self._scopes = util.scopes_to_string(scopes) | |
| 56 self._user_agent = user_agent | 111 self._user_agent = user_agent |
| 57 self._token_uri = token_uri | |
| 58 self._revoke_uri = revoke_uri | |
| 59 self._kwargs = kwargs | 112 self._kwargs = kwargs |
| 60 | 113 |
| 114 def _to_json(self, strip, to_serialize=None): |
| 115 """Utility function that creates JSON repr. of a credentials object. |
| 116 |
| 117 Over-ride is needed since PKCS#12 keys will not in general be JSON |
| 118 serializable. |
| 119 |
| 120 Args: |
| 121 strip: array, An array of names of members to exclude from the |
| 122 JSON. |
| 123 to_serialize: dict, (Optional) The properties for this object |
| 124 that will be serialized. This allows callers to modify |
| 125 before serializing. |
| 126 |
| 127 Returns: |
| 128 string, a JSON representation of this instance, suitable to pass to |
| 129 from_json(). |
| 130 """ |
| 131 if to_serialize is None: |
| 132 to_serialize = copy.copy(self.__dict__) |
| 133 pkcs12_val = to_serialize.get(_PKCS12_KEY) |
| 134 if pkcs12_val is not None: |
| 135 to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val) |
| 136 return super(ServiceAccountCredentials, self)._to_json( |
| 137 strip, to_serialize=to_serialize) |
| 138 |
| 139 @classmethod |
| 140 def _from_parsed_json_keyfile(cls, keyfile_dict, scopes): |
| 141 """Helper for factory constructors from JSON keyfile. |
| 142 |
| 143 Args: |
| 144 keyfile_dict: dict-like object, The parsed dictionary-like object |
| 145 containing the contents of the JSON keyfile. |
| 146 scopes: List or string, Scopes to use when acquiring an |
| 147 access token. |
| 148 |
| 149 Returns: |
| 150 ServiceAccountCredentials, a credentials object created from |
| 151 the keyfile contents. |
| 152 |
| 153 Raises: |
| 154 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. |
| 155 KeyError, if one of the expected keys is not present in |
| 156 the keyfile. |
| 157 """ |
| 158 creds_type = keyfile_dict.get('type') |
| 159 if creds_type != SERVICE_ACCOUNT: |
| 160 raise ValueError('Unexpected credentials type', creds_type, |
| 161 'Expected', SERVICE_ACCOUNT) |
| 162 |
| 163 service_account_email = keyfile_dict['client_email'] |
| 164 private_key_pkcs8_pem = keyfile_dict['private_key'] |
| 165 private_key_id = keyfile_dict['private_key_id'] |
| 166 client_id = keyfile_dict['client_id'] |
| 167 |
| 168 signer = crypt.Signer.from_string(private_key_pkcs8_pem) |
| 169 credentials = cls(service_account_email, signer, scopes=scopes, |
| 170 private_key_id=private_key_id, |
| 171 client_id=client_id) |
| 172 credentials._private_key_pkcs8_pem = private_key_pkcs8_pem |
| 173 return credentials |
| 174 |
| 175 @classmethod |
| 176 def from_json_keyfile_name(cls, filename, scopes=''): |
| 177 """Factory constructor from JSON keyfile by name. |
| 178 |
| 179 Args: |
| 180 filename: string, The location of the keyfile. |
| 181 scopes: List or string, (Optional) Scopes to use when acquiring an |
| 182 access token. |
| 183 |
| 184 Returns: |
| 185 ServiceAccountCredentials, a credentials object created from |
| 186 the keyfile. |
| 187 |
| 188 Raises: |
| 189 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. |
| 190 KeyError, if one of the expected keys is not present in |
| 191 the keyfile. |
| 192 """ |
| 193 with open(filename, 'r') as file_obj: |
| 194 client_credentials = json.load(file_obj) |
| 195 return cls._from_parsed_json_keyfile(client_credentials, scopes) |
| 196 |
| 197 @classmethod |
| 198 def from_json_keyfile_dict(cls, keyfile_dict, scopes=''): |
| 199 """Factory constructor from parsed JSON keyfile. |
| 200 |
| 201 Args: |
| 202 keyfile_dict: dict-like object, The parsed dictionary-like object |
| 203 containing the contents of the JSON keyfile. |
| 204 scopes: List or string, (Optional) Scopes to use when acquiring an |
| 205 access token. |
| 206 |
| 207 Returns: |
| 208 ServiceAccountCredentials, a credentials object created from |
| 209 the keyfile. |
| 210 |
| 211 Raises: |
| 212 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. |
| 213 KeyError, if one of the expected keys is not present in |
| 214 the keyfile. |
| 215 """ |
| 216 return cls._from_parsed_json_keyfile(keyfile_dict, scopes) |
| 217 |
| 218 @classmethod |
| 219 def _from_p12_keyfile_contents(cls, service_account_email, |
| 220 private_key_pkcs12, |
| 221 private_key_password=None, scopes=''): |
| 222 """Factory constructor from JSON keyfile. |
| 223 |
| 224 Args: |
| 225 service_account_email: string, The email associated with the |
| 226 service account. |
| 227 private_key_pkcs12: string, The contents of a PKCS#12 keyfile. |
| 228 private_key_password: string, (Optional) Password for PKCS#12 |
| 229 private key. Defaults to ``notasecret``. |
| 230 scopes: List or string, (Optional) Scopes to use when acquiring an |
| 231 access token. |
| 232 |
| 233 Returns: |
| 234 ServiceAccountCredentials, a credentials object created from |
| 235 the keyfile. |
| 236 |
| 237 Raises: |
| 238 NotImplementedError if pyOpenSSL is not installed / not the |
| 239 active crypto library. |
| 240 """ |
| 241 if private_key_password is None: |
| 242 private_key_password = _PASSWORD_DEFAULT |
| 243 if crypt.Signer is not crypt.OpenSSLSigner: |
| 244 raise NotImplementedError(_PKCS12_ERROR) |
| 245 signer = crypt.Signer.from_string(private_key_pkcs12, |
| 246 private_key_password) |
| 247 credentials = cls(service_account_email, signer, scopes=scopes) |
| 248 credentials._private_key_pkcs12 = private_key_pkcs12 |
| 249 credentials._private_key_password = private_key_password |
| 250 return credentials |
| 251 |
| 252 @classmethod |
| 253 def from_p12_keyfile(cls, service_account_email, filename, |
| 254 private_key_password=None, scopes=''): |
| 255 """Factory constructor from JSON keyfile. |
| 256 |
| 257 Args: |
| 258 service_account_email: string, The email associated with the |
| 259 service account. |
| 260 filename: string, The location of the PKCS#12 keyfile. |
| 261 private_key_password: string, (Optional) Password for PKCS#12 |
| 262 private key. Defaults to ``notasecret``. |
| 263 scopes: List or string, (Optional) Scopes to use when acquiring an |
| 264 access token. |
| 265 |
| 266 Returns: |
| 267 ServiceAccountCredentials, a credentials object created from |
| 268 the keyfile. |
| 269 |
| 270 Raises: |
| 271 NotImplementedError if pyOpenSSL is not installed / not the |
| 272 active crypto library. |
| 273 """ |
| 274 with open(filename, 'rb') as file_obj: |
| 275 private_key_pkcs12 = file_obj.read() |
| 276 return cls._from_p12_keyfile_contents( |
| 277 service_account_email, private_key_pkcs12, |
| 278 private_key_password=private_key_password, scopes=scopes) |
| 279 |
| 280 @classmethod |
| 281 def from_p12_keyfile_buffer(cls, service_account_email, file_buffer, |
| 282 private_key_password=None, scopes=''): |
| 283 """Factory constructor from JSON keyfile. |
| 284 |
| 285 Args: |
| 286 service_account_email: string, The email associated with the |
| 287 service account. |
| 288 file_buffer: stream, A buffer that implements ``read()`` |
| 289 and contains the PKCS#12 key contents. |
| 290 private_key_password: string, (Optional) Password for PKCS#12 |
| 291 private key. Defaults to ``notasecret``. |
| 292 scopes: List or string, (Optional) Scopes to use when acquiring an |
| 293 access token. |
| 294 |
| 295 Returns: |
| 296 ServiceAccountCredentials, a credentials object created from |
| 297 the keyfile. |
| 298 |
| 299 Raises: |
| 300 NotImplementedError if pyOpenSSL is not installed / not the |
| 301 active crypto library. |
| 302 """ |
| 303 private_key_pkcs12 = file_buffer.read() |
| 304 return cls._from_p12_keyfile_contents( |
| 305 service_account_email, private_key_pkcs12, |
| 306 private_key_password=private_key_password, scopes=scopes) |
| 307 |
| 61 def _generate_assertion(self): | 308 def _generate_assertion(self): |
| 62 """Generate the assertion that will be used in the request.""" | 309 """Generate the assertion that will be used in the request.""" |
| 63 | |
| 64 header = { | |
| 65 'alg': 'RS256', | |
| 66 'typ': 'JWT', | |
| 67 'kid': self._private_key_id | |
| 68 } | |
| 69 | |
| 70 now = int(time.time()) | 310 now = int(time.time()) |
| 71 payload = { | 311 payload = { |
| 72 'aud': self._token_uri, | 312 'aud': self.token_uri, |
| 73 'scope': self._scopes, | 313 'scope': self._scopes, |
| 74 'iat': now, | 314 'iat': now, |
| 75 'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS, | 315 'exp': now + self.MAX_TOKEN_LIFETIME_SECS, |
| 76 'iss': self._service_account_email | 316 'iss': self._service_account_email, |
| 77 } | 317 } |
| 78 payload.update(self._kwargs) | 318 payload.update(self._kwargs) |
| 79 | 319 return crypt.make_signed_jwt(self._signer, payload, |
| 80 first_segment = _urlsafe_b64encode(_json_encode(header)) | 320 key_id=self._private_key_id) |
| 81 second_segment = _urlsafe_b64encode(_json_encode(payload)) | |
| 82 assertion_input = first_segment + b'.' + second_segment | |
| 83 | |
| 84 # Sign the assertion. | |
| 85 rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key, | |
| 86 'SHA-256') | |
| 87 signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=') | |
| 88 | |
| 89 return assertion_input + b'.' + signature | |
| 90 | 321 |
| 91 def sign_blob(self, blob): | 322 def sign_blob(self, blob): |
| 92 # Ensure that it is bytes | 323 """Cryptographically sign a blob (of bytes). |
| 93 blob = _to_bytes(blob, encoding='utf-8') | 324 |
| 94 return (self._private_key_id, | 325 Implements abstract method |
| 95 rsa.pkcs1.sign(blob, self._private_key, 'SHA-256')) | 326 :meth:`oauth2client.client.AssertionCredentials.sign_blob`. |
| 327 |
| 328 Args: |
| 329 blob: bytes, Message to be signed. |
| 330 |
| 331 Returns: |
| 332 tuple, A pair of the private key ID used to sign the blob and |
| 333 the signed contents. |
| 334 """ |
| 335 return self._private_key_id, self._signer.sign(blob) |
| 96 | 336 |
| 97 @property | 337 @property |
| 98 def service_account_email(self): | 338 def service_account_email(self): |
| 339 """Get the email for the current service account. |
| 340 |
| 341 Returns: |
| 342 string, The email associated with the service account. |
| 343 """ |
| 99 return self._service_account_email | 344 return self._service_account_email |
| 100 | 345 |
| 101 @property | 346 @property |
| 102 def serialization_data(self): | 347 def serialization_data(self): |
| 348 # NOTE: This is only useful for JSON keyfile. |
| 103 return { | 349 return { |
| 104 'type': 'service_account', | 350 'type': 'service_account', |
| 105 'client_id': self._service_account_id, | |
| 106 'client_email': self._service_account_email, | 351 'client_email': self._service_account_email, |
| 107 'private_key_id': self._private_key_id, | 352 'private_key_id': self._private_key_id, |
| 108 'private_key': self._private_key_pkcs8_text | 353 'private_key': self._private_key_pkcs8_pem, |
| 354 'client_id': self.client_id, |
| 109 } | 355 } |
| 110 | 356 |
| 357 @classmethod |
| 358 def from_json(cls, json_data): |
| 359 """Deserialize a JSON-serialized instance. |
| 360 |
| 361 Inverse to :meth:`to_json`. |
| 362 |
| 363 Args: |
| 364 json_data: dict or string, Serialized JSON (as a string or an |
| 365 already parsed dictionary) representing a credential. |
| 366 |
| 367 Returns: |
| 368 ServiceAccountCredentials from the serialized data. |
| 369 """ |
| 370 if not isinstance(json_data, dict): |
| 371 json_data = json.loads(_from_bytes(json_data)) |
| 372 |
| 373 private_key_pkcs8_pem = None |
| 374 pkcs12_val = json_data.get(_PKCS12_KEY) |
| 375 password = None |
| 376 if pkcs12_val is None: |
| 377 private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem'] |
| 378 signer = crypt.Signer.from_string(private_key_pkcs8_pem) |
| 379 else: |
| 380 # NOTE: This assumes that private_key_pkcs8_pem is not also |
| 381 # in the serialized data. This would be very incorrect |
| 382 # state. |
| 383 pkcs12_val = base64.b64decode(pkcs12_val) |
| 384 password = json_data['_private_key_password'] |
| 385 signer = crypt.Signer.from_string(pkcs12_val, password) |
| 386 |
| 387 credentials = cls( |
| 388 json_data['_service_account_email'], |
| 389 signer, |
| 390 scopes=json_data['_scopes'], |
| 391 private_key_id=json_data['_private_key_id'], |
| 392 client_id=json_data['client_id'], |
| 393 user_agent=json_data['_user_agent'], |
| 394 **json_data['_kwargs'] |
| 395 ) |
| 396 if private_key_pkcs8_pem is not None: |
| 397 credentials._private_key_pkcs8_pem = private_key_pkcs8_pem |
| 398 if pkcs12_val is not None: |
| 399 credentials._private_key_pkcs12 = pkcs12_val |
| 400 if password is not None: |
| 401 credentials._private_key_password = password |
| 402 credentials.invalid = json_data['invalid'] |
| 403 credentials.access_token = json_data['access_token'] |
| 404 credentials.token_uri = json_data['token_uri'] |
| 405 credentials.revoke_uri = json_data['revoke_uri'] |
| 406 token_expiry = json_data.get('token_expiry', None) |
| 407 if token_expiry is not None: |
| 408 credentials.token_expiry = datetime.datetime.strptime( |
| 409 token_expiry, EXPIRY_FORMAT) |
| 410 return credentials |
| 411 |
| 111 def create_scoped_required(self): | 412 def create_scoped_required(self): |
| 112 return not self._scopes | 413 return not self._scopes |
| 113 | 414 |
| 114 def create_scoped(self, scopes): | 415 def create_scoped(self, scopes): |
| 115 return _ServiceAccountCredentials(self._service_account_id, | 416 result = self.__class__(self._service_account_email, |
| 116 self._service_account_email, | 417 self._signer, |
| 117 self._private_key_id, | 418 scopes=scopes, |
| 118 self._private_key_pkcs8_text, | 419 private_key_id=self._private_key_id, |
| 119 scopes, | 420 client_id=self.client_id, |
| 120 user_agent=self._user_agent, | 421 user_agent=self._user_agent, |
| 121 token_uri=self._token_uri, | 422 **self._kwargs) |
| 122 revoke_uri=self._revoke_uri, | 423 result.token_uri = self.token_uri |
| 123 **self._kwargs) | 424 result.revoke_uri = self.revoke_uri |
| 124 | 425 result._private_key_pkcs8_pem = self._private_key_pkcs8_pem |
| 125 | 426 result._private_key_pkcs12 = self._private_key_pkcs12 |
| 126 def _get_private_key(private_key_pkcs8_text): | 427 result._private_key_password = self._private_key_password |
| 127 """Get an RSA private key object from a pkcs8 representation.""" | 428 return result |
| 128 private_key_pkcs8_text = _to_bytes(private_key_pkcs8_text) | 429 |
| 129 der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY') | 430 def create_delegated(self, sub): |
| 130 asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo()) | 431 """Create credentials that act as domain-wide delegation of authority. |
| 131 return rsa.PrivateKey.load_pkcs1( | 432 |
| 132 asn1_private_key.getComponentByName('privateKey').asOctets(), | 433 Use the ``sub`` parameter as the subject to delegate on behalf of |
| 133 format='DER') | 434 that user. |
| 435 |
| 436 For example:: |
| 437 |
| 438 >>> account_sub = 'foo@email.com' |
| 439 >>> delegate_creds = creds.create_delegated(account_sub) |
| 440 |
| 441 Args: |
| 442 sub: string, An email address that this service account will |
| 443 act on behalf of (via domain-wide delegation). |
| 444 |
| 445 Returns: |
| 446 ServiceAccountCredentials, a copy of the current service account |
| 447 updated to act on behalf of ``sub``. |
| 448 """ |
| 449 new_kwargs = dict(self._kwargs) |
| 450 new_kwargs['sub'] = sub |
| 451 result = self.__class__(self._service_account_email, |
| 452 self._signer, |
| 453 scopes=self._scopes, |
| 454 private_key_id=self._private_key_id, |
| 455 client_id=self.client_id, |
| 456 user_agent=self._user_agent, |
| 457 **new_kwargs) |
| 458 result.token_uri = self.token_uri |
| 459 result.revoke_uri = self.revoke_uri |
| 460 result._private_key_pkcs8_pem = self._private_key_pkcs8_pem |
| 461 result._private_key_pkcs12 = self._private_key_pkcs12 |
| 462 result._private_key_password = self._private_key_password |
| 463 return result |
| OLD | NEW |