1# Copyright 2014 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"""oauth2client Service account credentials class.""" 16 17import base64 18import copy 19import datetime 20import json 21import time 22 23import oauth2client 24from oauth2client import _helpers 25from oauth2client import client 26from oauth2client import crypt 27from oauth2client import transport 28from oauth2client import util 29 30 31_PASSWORD_DEFAULT = 'notasecret' 32_PKCS12_KEY = '_private_key_pkcs12' 33_PKCS12_ERROR = r""" 34This library only implements PKCS#12 support via the pyOpenSSL library. 35Either install pyOpenSSL, or please convert the .p12 file 36to .pem format: 37 $ cat key.p12 | \ 38 > openssl pkcs12 -nodes -nocerts -passin pass:notasecret | \ 39 > openssl rsa > key.pem 40""" 41 42 43class ServiceAccountCredentials(client.AssertionCredentials): 44 """Service Account credential for OAuth 2.0 signed JWT grants. 45 46 Supports 47 48 * JSON keyfile (typically contains a PKCS8 key stored as 49 PEM text) 50 * ``.p12`` key (stores PKCS12 key and certificate) 51 52 Makes an assertion to server using a signed JWT assertion in exchange 53 for an access token. 54 55 This credential does not require a flow to instantiate because it 56 represents a two legged flow, and therefore has all of the required 57 information to generate and refresh its own access tokens. 58 59 Args: 60 service_account_email: string, The email associated with the 61 service account. 62 signer: ``crypt.Signer``, A signer which can be used to sign content. 63 scopes: List or string, (Optional) Scopes to use when acquiring 64 an access token. 65 private_key_id: string, (Optional) Private key identifier. Typically 66 only used with a JSON keyfile. Can be sent in the 67 header of a JWT token assertion. 68 client_id: string, (Optional) Client ID for the project that owns the 69 service account. 70 user_agent: string, (Optional) User agent to use when sending 71 request. 72 token_uri: string, URI for token endpoint. For convenience defaults 73 to Google's endpoints but any OAuth 2.0 provider can be 74 used. 75 revoke_uri: string, URI for revoke endpoint. For convenience defaults 76 to Google's endpoints but any OAuth 2.0 provider can be 77 used. 78 kwargs: dict, Extra key-value pairs (both strings) to send in the 79 payload body when making an assertion. 80 """ 81 82 MAX_TOKEN_LIFETIME_SECS = 3600 83 """Max lifetime of the token (one hour, in seconds).""" 84 85 NON_SERIALIZED_MEMBERS = ( 86 frozenset(['_signer']) | 87 client.AssertionCredentials.NON_SERIALIZED_MEMBERS) 88 """Members that aren't serialized when object is converted to JSON.""" 89 90 # Can be over-ridden by factory constructors. Used for 91 # serialization/deserialization purposes. 92 _private_key_pkcs8_pem = None 93 _private_key_pkcs12 = None 94 _private_key_password = None 95 96 def __init__(self, 97 service_account_email, 98 signer, 99 scopes='', 100 private_key_id=None, 101 client_id=None, 102 user_agent=None, 103 token_uri=oauth2client.GOOGLE_TOKEN_URI, 104 revoke_uri=oauth2client.GOOGLE_REVOKE_URI, 105 **kwargs): 106 107 super(ServiceAccountCredentials, self).__init__( 108 None, user_agent=user_agent, token_uri=token_uri, 109 revoke_uri=revoke_uri) 110 111 self._service_account_email = service_account_email 112 self._signer = signer 113 self._scopes = util.scopes_to_string(scopes) 114 self._private_key_id = private_key_id 115 self.client_id = client_id 116 self._user_agent = user_agent 117 self._kwargs = kwargs 118 119 def _to_json(self, strip, to_serialize=None): 120 """Utility function that creates JSON repr. of a credentials object. 121 122 Over-ride is needed since PKCS#12 keys will not in general be JSON 123 serializable. 124 125 Args: 126 strip: array, An array of names of members to exclude from the 127 JSON. 128 to_serialize: dict, (Optional) The properties for this object 129 that will be serialized. This allows callers to 130 modify before serializing. 131 132 Returns: 133 string, a JSON representation of this instance, suitable to pass to 134 from_json(). 135 """ 136 if to_serialize is None: 137 to_serialize = copy.copy(self.__dict__) 138 pkcs12_val = to_serialize.get(_PKCS12_KEY) 139 if pkcs12_val is not None: 140 to_serialize[_PKCS12_KEY] = base64.b64encode(pkcs12_val) 141 return super(ServiceAccountCredentials, self)._to_json( 142 strip, to_serialize=to_serialize) 143 144 @classmethod 145 def _from_parsed_json_keyfile(cls, keyfile_dict, scopes, 146 token_uri=None, revoke_uri=None): 147 """Helper for factory constructors from JSON keyfile. 148 149 Args: 150 keyfile_dict: dict-like object, The parsed dictionary-like object 151 containing the contents of the JSON keyfile. 152 scopes: List or string, Scopes to use when acquiring an 153 access token. 154 token_uri: string, URI for OAuth 2.0 provider token endpoint. 155 If unset and not present in keyfile_dict, defaults 156 to Google's endpoints. 157 revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint. 158 If unset and not present in keyfile_dict, defaults 159 to Google's endpoints. 160 161 Returns: 162 ServiceAccountCredentials, a credentials object created from 163 the keyfile contents. 164 165 Raises: 166 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. 167 KeyError, if one of the expected keys is not present in 168 the keyfile. 169 """ 170 creds_type = keyfile_dict.get('type') 171 if creds_type != client.SERVICE_ACCOUNT: 172 raise ValueError('Unexpected credentials type', creds_type, 173 'Expected', client.SERVICE_ACCOUNT) 174 175 service_account_email = keyfile_dict['client_email'] 176 private_key_pkcs8_pem = keyfile_dict['private_key'] 177 private_key_id = keyfile_dict['private_key_id'] 178 client_id = keyfile_dict['client_id'] 179 if not token_uri: 180 token_uri = keyfile_dict.get('token_uri', 181 oauth2client.GOOGLE_TOKEN_URI) 182 if not revoke_uri: 183 revoke_uri = keyfile_dict.get('revoke_uri', 184 oauth2client.GOOGLE_REVOKE_URI) 185 186 signer = crypt.Signer.from_string(private_key_pkcs8_pem) 187 credentials = cls(service_account_email, signer, scopes=scopes, 188 private_key_id=private_key_id, 189 client_id=client_id, token_uri=token_uri, 190 revoke_uri=revoke_uri) 191 credentials._private_key_pkcs8_pem = private_key_pkcs8_pem 192 return credentials 193 194 @classmethod 195 def from_json_keyfile_name(cls, filename, scopes='', 196 token_uri=None, revoke_uri=None): 197 198 """Factory constructor from JSON keyfile by name. 199 200 Args: 201 filename: string, The location of the keyfile. 202 scopes: List or string, (Optional) Scopes to use when acquiring an 203 access token. 204 token_uri: string, URI for OAuth 2.0 provider token endpoint. 205 If unset and not present in the key file, defaults 206 to Google's endpoints. 207 revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint. 208 If unset and not present in the key file, defaults 209 to Google's endpoints. 210 211 Returns: 212 ServiceAccountCredentials, a credentials object created from 213 the keyfile. 214 215 Raises: 216 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. 217 KeyError, if one of the expected keys is not present in 218 the keyfile. 219 """ 220 with open(filename, 'r') as file_obj: 221 client_credentials = json.load(file_obj) 222 return cls._from_parsed_json_keyfile(client_credentials, scopes, 223 token_uri=token_uri, 224 revoke_uri=revoke_uri) 225 226 @classmethod 227 def from_json_keyfile_dict(cls, keyfile_dict, scopes='', 228 token_uri=None, revoke_uri=None): 229 """Factory constructor from parsed JSON keyfile. 230 231 Args: 232 keyfile_dict: dict-like object, The parsed dictionary-like object 233 containing the contents of the JSON keyfile. 234 scopes: List or string, (Optional) Scopes to use when acquiring an 235 access token. 236 token_uri: string, URI for OAuth 2.0 provider token endpoint. 237 If unset and not present in keyfile_dict, defaults 238 to Google's endpoints. 239 revoke_uri: string, URI for OAuth 2.0 provider revoke endpoint. 240 If unset and not present in keyfile_dict, defaults 241 to Google's endpoints. 242 243 Returns: 244 ServiceAccountCredentials, a credentials object created from 245 the keyfile. 246 247 Raises: 248 ValueError, if the credential type is not :data:`SERVICE_ACCOUNT`. 249 KeyError, if one of the expected keys is not present in 250 the keyfile. 251 """ 252 return cls._from_parsed_json_keyfile(keyfile_dict, scopes, 253 token_uri=token_uri, 254 revoke_uri=revoke_uri) 255 256 @classmethod 257 def _from_p12_keyfile_contents(cls, service_account_email, 258 private_key_pkcs12, 259 private_key_password=None, scopes='', 260 token_uri=oauth2client.GOOGLE_TOKEN_URI, 261 revoke_uri=oauth2client.GOOGLE_REVOKE_URI): 262 """Factory constructor from JSON keyfile. 263 264 Args: 265 service_account_email: string, The email associated with the 266 service account. 267 private_key_pkcs12: string, The contents of a PKCS#12 keyfile. 268 private_key_password: string, (Optional) Password for PKCS#12 269 private key. Defaults to ``notasecret``. 270 scopes: List or string, (Optional) Scopes to use when acquiring an 271 access token. 272 token_uri: string, URI for token endpoint. For convenience defaults 273 to Google's endpoints but any OAuth 2.0 provider can be 274 used. 275 revoke_uri: string, URI for revoke endpoint. For convenience 276 defaults to Google's endpoints but any OAuth 2.0 277 provider can be used. 278 279 Returns: 280 ServiceAccountCredentials, a credentials object created from 281 the keyfile. 282 283 Raises: 284 NotImplementedError if pyOpenSSL is not installed / not the 285 active crypto library. 286 """ 287 if private_key_password is None: 288 private_key_password = _PASSWORD_DEFAULT 289 if crypt.Signer is not crypt.OpenSSLSigner: 290 raise NotImplementedError(_PKCS12_ERROR) 291 signer = crypt.Signer.from_string(private_key_pkcs12, 292 private_key_password) 293 credentials = cls(service_account_email, signer, scopes=scopes, 294 token_uri=token_uri, revoke_uri=revoke_uri) 295 credentials._private_key_pkcs12 = private_key_pkcs12 296 credentials._private_key_password = private_key_password 297 return credentials 298 299 @classmethod 300 def from_p12_keyfile(cls, service_account_email, filename, 301 private_key_password=None, scopes='', 302 token_uri=oauth2client.GOOGLE_TOKEN_URI, 303 revoke_uri=oauth2client.GOOGLE_REVOKE_URI): 304 305 """Factory constructor from JSON keyfile. 306 307 Args: 308 service_account_email: string, The email associated with the 309 service account. 310 filename: string, The location of the PKCS#12 keyfile. 311 private_key_password: string, (Optional) Password for PKCS#12 312 private key. Defaults to ``notasecret``. 313 scopes: List or string, (Optional) Scopes to use when acquiring an 314 access token. 315 token_uri: string, URI for token endpoint. For convenience defaults 316 to Google's endpoints but any OAuth 2.0 provider can be 317 used. 318 revoke_uri: string, URI for revoke endpoint. For convenience 319 defaults to Google's endpoints but any OAuth 2.0 320 provider can be used. 321 322 Returns: 323 ServiceAccountCredentials, a credentials object created from 324 the keyfile. 325 326 Raises: 327 NotImplementedError if pyOpenSSL is not installed / not the 328 active crypto library. 329 """ 330 with open(filename, 'rb') as file_obj: 331 private_key_pkcs12 = file_obj.read() 332 return cls._from_p12_keyfile_contents( 333 service_account_email, private_key_pkcs12, 334 private_key_password=private_key_password, scopes=scopes, 335 token_uri=token_uri, revoke_uri=revoke_uri) 336 337 @classmethod 338 def from_p12_keyfile_buffer(cls, service_account_email, file_buffer, 339 private_key_password=None, scopes='', 340 token_uri=oauth2client.GOOGLE_TOKEN_URI, 341 revoke_uri=oauth2client.GOOGLE_REVOKE_URI): 342 """Factory constructor from JSON keyfile. 343 344 Args: 345 service_account_email: string, The email associated with the 346 service account. 347 file_buffer: stream, A buffer that implements ``read()`` 348 and contains the PKCS#12 key contents. 349 private_key_password: string, (Optional) Password for PKCS#12 350 private key. Defaults to ``notasecret``. 351 scopes: List or string, (Optional) Scopes to use when acquiring an 352 access token. 353 token_uri: string, URI for token endpoint. For convenience defaults 354 to Google's endpoints but any OAuth 2.0 provider can be 355 used. 356 revoke_uri: string, URI for revoke endpoint. For convenience 357 defaults to Google's endpoints but any OAuth 2.0 358 provider can be used. 359 360 Returns: 361 ServiceAccountCredentials, a credentials object created from 362 the keyfile. 363 364 Raises: 365 NotImplementedError if pyOpenSSL is not installed / not the 366 active crypto library. 367 """ 368 private_key_pkcs12 = file_buffer.read() 369 return cls._from_p12_keyfile_contents( 370 service_account_email, private_key_pkcs12, 371 private_key_password=private_key_password, scopes=scopes, 372 token_uri=token_uri, revoke_uri=revoke_uri) 373 374 def _generate_assertion(self): 375 """Generate the assertion that will be used in the request.""" 376 now = int(time.time()) 377 payload = { 378 'aud': self.token_uri, 379 'scope': self._scopes, 380 'iat': now, 381 'exp': now + self.MAX_TOKEN_LIFETIME_SECS, 382 'iss': self._service_account_email, 383 } 384 payload.update(self._kwargs) 385 return crypt.make_signed_jwt(self._signer, payload, 386 key_id=self._private_key_id) 387 388 def sign_blob(self, blob): 389 """Cryptographically sign a blob (of bytes). 390 391 Implements abstract method 392 :meth:`oauth2client.client.AssertionCredentials.sign_blob`. 393 394 Args: 395 blob: bytes, Message to be signed. 396 397 Returns: 398 tuple, A pair of the private key ID used to sign the blob and 399 the signed contents. 400 """ 401 return self._private_key_id, self._signer.sign(blob) 402 403 @property 404 def service_account_email(self): 405 """Get the email for the current service account. 406 407 Returns: 408 string, The email associated with the service account. 409 """ 410 return self._service_account_email 411 412 @property 413 def serialization_data(self): 414 # NOTE: This is only useful for JSON keyfile. 415 return { 416 'type': 'service_account', 417 'client_email': self._service_account_email, 418 'private_key_id': self._private_key_id, 419 'private_key': self._private_key_pkcs8_pem, 420 'client_id': self.client_id, 421 } 422 423 @classmethod 424 def from_json(cls, json_data): 425 """Deserialize a JSON-serialized instance. 426 427 Inverse to :meth:`to_json`. 428 429 Args: 430 json_data: dict or string, Serialized JSON (as a string or an 431 already parsed dictionary) representing a credential. 432 433 Returns: 434 ServiceAccountCredentials from the serialized data. 435 """ 436 if not isinstance(json_data, dict): 437 json_data = json.loads(_helpers._from_bytes(json_data)) 438 439 private_key_pkcs8_pem = None 440 pkcs12_val = json_data.get(_PKCS12_KEY) 441 password = None 442 if pkcs12_val is None: 443 private_key_pkcs8_pem = json_data['_private_key_pkcs8_pem'] 444 signer = crypt.Signer.from_string(private_key_pkcs8_pem) 445 else: 446 # NOTE: This assumes that private_key_pkcs8_pem is not also 447 # in the serialized data. This would be very incorrect 448 # state. 449 pkcs12_val = base64.b64decode(pkcs12_val) 450 password = json_data['_private_key_password'] 451 signer = crypt.Signer.from_string(pkcs12_val, password) 452 453 credentials = cls( 454 json_data['_service_account_email'], 455 signer, 456 scopes=json_data['_scopes'], 457 private_key_id=json_data['_private_key_id'], 458 client_id=json_data['client_id'], 459 user_agent=json_data['_user_agent'], 460 **json_data['_kwargs'] 461 ) 462 if private_key_pkcs8_pem is not None: 463 credentials._private_key_pkcs8_pem = private_key_pkcs8_pem 464 if pkcs12_val is not None: 465 credentials._private_key_pkcs12 = pkcs12_val 466 if password is not None: 467 credentials._private_key_password = password 468 credentials.invalid = json_data['invalid'] 469 credentials.access_token = json_data['access_token'] 470 credentials.token_uri = json_data['token_uri'] 471 credentials.revoke_uri = json_data['revoke_uri'] 472 token_expiry = json_data.get('token_expiry', None) 473 if token_expiry is not None: 474 credentials.token_expiry = datetime.datetime.strptime( 475 token_expiry, client.EXPIRY_FORMAT) 476 return credentials 477 478 def create_scoped_required(self): 479 return not self._scopes 480 481 def create_scoped(self, scopes): 482 result = self.__class__(self._service_account_email, 483 self._signer, 484 scopes=scopes, 485 private_key_id=self._private_key_id, 486 client_id=self.client_id, 487 user_agent=self._user_agent, 488 **self._kwargs) 489 result.token_uri = self.token_uri 490 result.revoke_uri = self.revoke_uri 491 result._private_key_pkcs8_pem = self._private_key_pkcs8_pem 492 result._private_key_pkcs12 = self._private_key_pkcs12 493 result._private_key_password = self._private_key_password 494 return result 495 496 def create_with_claims(self, claims): 497 """Create credentials that specify additional claims. 498 499 Args: 500 claims: dict, key-value pairs for claims. 501 502 Returns: 503 ServiceAccountCredentials, a copy of the current service account 504 credentials with updated claims to use when obtaining access 505 tokens. 506 """ 507 new_kwargs = dict(self._kwargs) 508 new_kwargs.update(claims) 509 result = self.__class__(self._service_account_email, 510 self._signer, 511 scopes=self._scopes, 512 private_key_id=self._private_key_id, 513 client_id=self.client_id, 514 user_agent=self._user_agent, 515 **new_kwargs) 516 result.token_uri = self.token_uri 517 result.revoke_uri = self.revoke_uri 518 result._private_key_pkcs8_pem = self._private_key_pkcs8_pem 519 result._private_key_pkcs12 = self._private_key_pkcs12 520 result._private_key_password = self._private_key_password 521 return result 522 523 def create_delegated(self, sub): 524 """Create credentials that act as domain-wide delegation of authority. 525 526 Use the ``sub`` parameter as the subject to delegate on behalf of 527 that user. 528 529 For example:: 530 531 >>> account_sub = 'foo@email.com' 532 >>> delegate_creds = creds.create_delegated(account_sub) 533 534 Args: 535 sub: string, An email address that this service account will 536 act on behalf of (via domain-wide delegation). 537 538 Returns: 539 ServiceAccountCredentials, a copy of the current service account 540 updated to act on behalf of ``sub``. 541 """ 542 return self.create_with_claims({'sub': sub}) 543 544 545def _datetime_to_secs(utc_time): 546 # TODO(issue 298): use time_delta.total_seconds() 547 # time_delta.total_seconds() not supported in Python 2.6 548 epoch = datetime.datetime(1970, 1, 1) 549 time_delta = utc_time - epoch 550 return time_delta.days * 86400 + time_delta.seconds 551 552 553class _JWTAccessCredentials(ServiceAccountCredentials): 554 """Self signed JWT credentials. 555 556 Makes an assertion to server using a self signed JWT from service account 557 credentials. These credentials do NOT use OAuth 2.0 and instead 558 authenticate directly. 559 """ 560 _MAX_TOKEN_LIFETIME_SECS = 3600 561 """Max lifetime of the token (one hour, in seconds).""" 562 563 def __init__(self, 564 service_account_email, 565 signer, 566 scopes=None, 567 private_key_id=None, 568 client_id=None, 569 user_agent=None, 570 token_uri=oauth2client.GOOGLE_TOKEN_URI, 571 revoke_uri=oauth2client.GOOGLE_REVOKE_URI, 572 additional_claims=None): 573 if additional_claims is None: 574 additional_claims = {} 575 super(_JWTAccessCredentials, self).__init__( 576 service_account_email, 577 signer, 578 private_key_id=private_key_id, 579 client_id=client_id, 580 user_agent=user_agent, 581 token_uri=token_uri, 582 revoke_uri=revoke_uri, 583 **additional_claims) 584 585 def authorize(self, http): 586 """Authorize an httplib2.Http instance with a JWT assertion. 587 588 Unless specified, the 'aud' of the assertion will be the base 589 uri of the request. 590 591 Args: 592 http: An instance of ``httplib2.Http`` or something that acts 593 like it. 594 Returns: 595 A modified instance of http that was passed in. 596 Example:: 597 h = httplib2.Http() 598 h = credentials.authorize(h) 599 """ 600 transport.wrap_http_for_jwt_access(self, http) 601 return http 602 603 def get_access_token(self, http=None, additional_claims=None): 604 """Create a signed jwt. 605 606 Args: 607 http: unused 608 additional_claims: dict, additional claims to add to 609 the payload of the JWT. 610 Returns: 611 An AccessTokenInfo with the signed jwt 612 """ 613 if additional_claims is None: 614 if self.access_token is None or self.access_token_expired: 615 self.refresh(None) 616 return client.AccessTokenInfo( 617 access_token=self.access_token, expires_in=self._expires_in()) 618 else: 619 # Create a 1 time token 620 token, unused_expiry = self._create_token(additional_claims) 621 return client.AccessTokenInfo( 622 access_token=token, expires_in=self._MAX_TOKEN_LIFETIME_SECS) 623 624 def revoke(self, http): 625 """Cannot revoke JWTAccessCredentials tokens.""" 626 pass 627 628 def create_scoped_required(self): 629 # JWTAccessCredentials are unscoped by definition 630 return True 631 632 def create_scoped(self, scopes, token_uri=oauth2client.GOOGLE_TOKEN_URI, 633 revoke_uri=oauth2client.GOOGLE_REVOKE_URI): 634 # Returns an OAuth2 credentials with the given scope 635 result = ServiceAccountCredentials(self._service_account_email, 636 self._signer, 637 scopes=scopes, 638 private_key_id=self._private_key_id, 639 client_id=self.client_id, 640 user_agent=self._user_agent, 641 token_uri=token_uri, 642 revoke_uri=revoke_uri, 643 **self._kwargs) 644 if self._private_key_pkcs8_pem is not None: 645 result._private_key_pkcs8_pem = self._private_key_pkcs8_pem 646 if self._private_key_pkcs12 is not None: 647 result._private_key_pkcs12 = self._private_key_pkcs12 648 if self._private_key_password is not None: 649 result._private_key_password = self._private_key_password 650 return result 651 652 def refresh(self, http): 653 self._refresh(None) 654 655 def _refresh(self, http_request): 656 self.access_token, self.token_expiry = self._create_token() 657 658 def _create_token(self, additional_claims=None): 659 now = client._UTCNOW() 660 lifetime = datetime.timedelta(seconds=self._MAX_TOKEN_LIFETIME_SECS) 661 expiry = now + lifetime 662 payload = { 663 'iat': _datetime_to_secs(now), 664 'exp': _datetime_to_secs(expiry), 665 'iss': self._service_account_email, 666 'sub': self._service_account_email 667 } 668 payload.update(self._kwargs) 669 if additional_claims is not None: 670 payload.update(additional_claims) 671 jwt = crypt.make_signed_jwt(self._signer, payload, 672 key_id=self._private_key_id) 673 return jwt.decode('ascii'), expiry 674