1# Copyright 2016 Google LLC 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"""Service Accounts: JSON Web Token (JWT) Profile for OAuth 2.0 16 17This module implements the JWT Profile for OAuth 2.0 Authorization Grants 18as defined by `RFC 7523`_ with particular support for how this RFC is 19implemented in Google's infrastructure. Google refers to these credentials 20as *Service Accounts*. 21 22Service accounts are used for server-to-server communication, such as 23interactions between a web application server and a Google service. The 24service account belongs to your application instead of to an individual end 25user. In contrast to other OAuth 2.0 profiles, no users are involved and your 26application "acts" as the service account. 27 28Typically an application uses a service account when the application uses 29Google APIs to work with its own data rather than a user's data. For example, 30an application that uses Google Cloud Datastore for data persistence would use 31a service account to authenticate its calls to the Google Cloud Datastore API. 32However, an application that needs to access a user's Drive documents would 33use the normal OAuth 2.0 profile. 34 35Additionally, Google Apps domain administrators can grant service accounts 36`domain-wide delegation`_ authority to access user data on behalf of users in 37the domain. 38 39This profile uses a JWT to acquire an OAuth 2.0 access token. The JWT is used 40in place of the usual authorization token returned during the standard 41OAuth 2.0 Authorization Code grant. The JWT is only used for this purpose, as 42the acquired access token is used as the bearer token when making requests 43using these credentials. 44 45This profile differs from normal OAuth 2.0 profile because no user consent 46step is required. The use of the private key allows this profile to assert 47identity directly. 48 49This profile also differs from the :mod:`google.auth.jwt` authentication 50because the JWT credentials use the JWT directly as the bearer token. This 51profile instead only uses the JWT to obtain an OAuth 2.0 access token. The 52obtained OAuth 2.0 access token is used as the bearer token. 53 54Domain-wide delegation 55---------------------- 56 57Domain-wide delegation allows a service account to access user data on 58behalf of any user in a Google Apps domain without consent from the user. 59For example, an application that uses the Google Calendar API to add events to 60the calendars of all users in a Google Apps domain would use a service account 61to access the Google Calendar API on behalf of users. 62 63The Google Apps administrator must explicitly authorize the service account to 64do this. This authorization step is referred to as "delegating domain-wide 65authority" to a service account. 66 67You can use domain-wise delegation by creating a set of credentials with a 68specific subject using :meth:`~Credentials.with_subject`. 69 70.. _RFC 7523: https://tools.ietf.org/html/rfc7523 71""" 72 73import copy 74import datetime 75 76from google.auth import _helpers 77from google.auth import _service_account_info 78from google.auth import credentials 79from google.auth import jwt 80from google.oauth2 import _client 81 82_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 83_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token" 84 85 86class Credentials( 87 credentials.Signing, credentials.Scoped, credentials.CredentialsWithQuotaProject 88): 89 """Service account credentials 90 91 Usually, you'll create these credentials with one of the helper 92 constructors. To create credentials using a Google service account 93 private key JSON file:: 94 95 credentials = service_account.Credentials.from_service_account_file( 96 'service-account.json') 97 98 Or if you already have the service account file loaded:: 99 100 service_account_info = json.load(open('service_account.json')) 101 credentials = service_account.Credentials.from_service_account_info( 102 service_account_info) 103 104 Both helper methods pass on arguments to the constructor, so you can 105 specify additional scopes and a subject if necessary:: 106 107 credentials = service_account.Credentials.from_service_account_file( 108 'service-account.json', 109 scopes=['email'], 110 subject='[email protected]') 111 112 The credentials are considered immutable. If you want to modify the scopes 113 or the subject used for delegation, use :meth:`with_scopes` or 114 :meth:`with_subject`:: 115 116 scoped_credentials = credentials.with_scopes(['email']) 117 delegated_credentials = credentials.with_subject(subject) 118 119 To add a quota project, use :meth:`with_quota_project`:: 120 121 credentials = credentials.with_quota_project('myproject-123') 122 """ 123 124 def __init__( 125 self, 126 signer, 127 service_account_email, 128 token_uri, 129 scopes=None, 130 default_scopes=None, 131 subject=None, 132 project_id=None, 133 quota_project_id=None, 134 additional_claims=None, 135 always_use_jwt_access=False, 136 ): 137 """ 138 Args: 139 signer (google.auth.crypt.Signer): The signer used to sign JWTs. 140 service_account_email (str): The service account's email. 141 scopes (Sequence[str]): User-defined scopes to request during the 142 authorization grant. 143 default_scopes (Sequence[str]): Default scopes passed by a 144 Google client library. Use 'scopes' for user-defined scopes. 145 token_uri (str): The OAuth 2.0 Token URI. 146 subject (str): For domain-wide delegation, the email address of the 147 user to for which to request delegated access. 148 project_id (str): Project ID associated with the service account 149 credential. 150 quota_project_id (Optional[str]): The project ID used for quota and 151 billing. 152 additional_claims (Mapping[str, str]): Any additional claims for 153 the JWT assertion used in the authorization grant. 154 always_use_jwt_access (Optional[bool]): Whether self signed JWT should 155 be always used. 156 157 .. note:: Typically one of the helper constructors 158 :meth:`from_service_account_file` or 159 :meth:`from_service_account_info` are used instead of calling the 160 constructor directly. 161 """ 162 super(Credentials, self).__init__() 163 164 self._scopes = scopes 165 self._default_scopes = default_scopes 166 self._signer = signer 167 self._service_account_email = service_account_email 168 self._subject = subject 169 self._project_id = project_id 170 self._quota_project_id = quota_project_id 171 self._token_uri = token_uri 172 self._always_use_jwt_access = always_use_jwt_access 173 174 self._jwt_credentials = None 175 176 if additional_claims is not None: 177 self._additional_claims = additional_claims 178 else: 179 self._additional_claims = {} 180 181 @classmethod 182 def _from_signer_and_info(cls, signer, info, **kwargs): 183 """Creates a Credentials instance from a signer and service account 184 info. 185 186 Args: 187 signer (google.auth.crypt.Signer): The signer used to sign JWTs. 188 info (Mapping[str, str]): The service account info. 189 kwargs: Additional arguments to pass to the constructor. 190 191 Returns: 192 google.auth.jwt.Credentials: The constructed credentials. 193 194 Raises: 195 ValueError: If the info is not in the expected format. 196 """ 197 return cls( 198 signer, 199 service_account_email=info["client_email"], 200 token_uri=info["token_uri"], 201 project_id=info.get("project_id"), 202 **kwargs 203 ) 204 205 @classmethod 206 def from_service_account_info(cls, info, **kwargs): 207 """Creates a Credentials instance from parsed service account info. 208 209 Args: 210 info (Mapping[str, str]): The service account info in Google 211 format. 212 kwargs: Additional arguments to pass to the constructor. 213 214 Returns: 215 google.auth.service_account.Credentials: The constructed 216 credentials. 217 218 Raises: 219 ValueError: If the info is not in the expected format. 220 """ 221 signer = _service_account_info.from_dict( 222 info, require=["client_email", "token_uri"] 223 ) 224 return cls._from_signer_and_info(signer, info, **kwargs) 225 226 @classmethod 227 def from_service_account_file(cls, filename, **kwargs): 228 """Creates a Credentials instance from a service account json file. 229 230 Args: 231 filename (str): The path to the service account json file. 232 kwargs: Additional arguments to pass to the constructor. 233 234 Returns: 235 google.auth.service_account.Credentials: The constructed 236 credentials. 237 """ 238 info, signer = _service_account_info.from_filename( 239 filename, require=["client_email", "token_uri"] 240 ) 241 return cls._from_signer_and_info(signer, info, **kwargs) 242 243 @property 244 def service_account_email(self): 245 """The service account email.""" 246 return self._service_account_email 247 248 @property 249 def project_id(self): 250 """Project ID associated with this credential.""" 251 return self._project_id 252 253 @property 254 def requires_scopes(self): 255 """Checks if the credentials requires scopes. 256 257 Returns: 258 bool: True if there are no scopes set otherwise False. 259 """ 260 return True if not self._scopes else False 261 262 @_helpers.copy_docstring(credentials.Scoped) 263 def with_scopes(self, scopes, default_scopes=None): 264 return self.__class__( 265 self._signer, 266 service_account_email=self._service_account_email, 267 scopes=scopes, 268 default_scopes=default_scopes, 269 token_uri=self._token_uri, 270 subject=self._subject, 271 project_id=self._project_id, 272 quota_project_id=self._quota_project_id, 273 additional_claims=self._additional_claims.copy(), 274 always_use_jwt_access=self._always_use_jwt_access, 275 ) 276 277 def with_always_use_jwt_access(self, always_use_jwt_access): 278 """Create a copy of these credentials with the specified always_use_jwt_access value. 279 280 Args: 281 always_use_jwt_access (bool): Whether always use self signed JWT or not. 282 283 Returns: 284 google.auth.service_account.Credentials: A new credentials 285 instance. 286 """ 287 return self.__class__( 288 self._signer, 289 service_account_email=self._service_account_email, 290 scopes=self._scopes, 291 default_scopes=self._default_scopes, 292 token_uri=self._token_uri, 293 subject=self._subject, 294 project_id=self._project_id, 295 quota_project_id=self._quota_project_id, 296 additional_claims=self._additional_claims.copy(), 297 always_use_jwt_access=always_use_jwt_access, 298 ) 299 300 def with_subject(self, subject): 301 """Create a copy of these credentials with the specified subject. 302 303 Args: 304 subject (str): The subject claim. 305 306 Returns: 307 google.auth.service_account.Credentials: A new credentials 308 instance. 309 """ 310 return self.__class__( 311 self._signer, 312 service_account_email=self._service_account_email, 313 scopes=self._scopes, 314 default_scopes=self._default_scopes, 315 token_uri=self._token_uri, 316 subject=subject, 317 project_id=self._project_id, 318 quota_project_id=self._quota_project_id, 319 additional_claims=self._additional_claims.copy(), 320 always_use_jwt_access=self._always_use_jwt_access, 321 ) 322 323 def with_claims(self, additional_claims): 324 """Returns a copy of these credentials with modified claims. 325 326 Args: 327 additional_claims (Mapping[str, str]): Any additional claims for 328 the JWT payload. This will be merged with the current 329 additional claims. 330 331 Returns: 332 google.auth.service_account.Credentials: A new credentials 333 instance. 334 """ 335 new_additional_claims = copy.deepcopy(self._additional_claims) 336 new_additional_claims.update(additional_claims or {}) 337 338 return self.__class__( 339 self._signer, 340 service_account_email=self._service_account_email, 341 scopes=self._scopes, 342 default_scopes=self._default_scopes, 343 token_uri=self._token_uri, 344 subject=self._subject, 345 project_id=self._project_id, 346 quota_project_id=self._quota_project_id, 347 additional_claims=new_additional_claims, 348 always_use_jwt_access=self._always_use_jwt_access, 349 ) 350 351 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 352 def with_quota_project(self, quota_project_id): 353 354 return self.__class__( 355 self._signer, 356 service_account_email=self._service_account_email, 357 default_scopes=self._default_scopes, 358 scopes=self._scopes, 359 token_uri=self._token_uri, 360 subject=self._subject, 361 project_id=self._project_id, 362 quota_project_id=quota_project_id, 363 additional_claims=self._additional_claims.copy(), 364 always_use_jwt_access=self._always_use_jwt_access, 365 ) 366 367 def _make_authorization_grant_assertion(self): 368 """Create the OAuth 2.0 assertion. 369 370 This assertion is used during the OAuth 2.0 grant to acquire an 371 access token. 372 373 Returns: 374 bytes: The authorization grant assertion. 375 """ 376 now = _helpers.utcnow() 377 lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS) 378 expiry = now + lifetime 379 380 payload = { 381 "iat": _helpers.datetime_to_secs(now), 382 "exp": _helpers.datetime_to_secs(expiry), 383 # The issuer must be the service account email. 384 "iss": self._service_account_email, 385 # The audience must be the auth token endpoint's URI 386 "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT, 387 "scope": _helpers.scopes_to_string(self._scopes or ()), 388 } 389 390 payload.update(self._additional_claims) 391 392 # The subject can be a user email for domain-wide delegation. 393 if self._subject: 394 payload.setdefault("sub", self._subject) 395 396 token = jwt.encode(self._signer, payload) 397 398 return token 399 400 @_helpers.copy_docstring(credentials.Credentials) 401 def refresh(self, request): 402 # Since domain wide delegation doesn't work with self signed JWT. If 403 # subject exists, then we should not use self signed JWT. 404 if self._subject is None and self._jwt_credentials is not None: 405 self._jwt_credentials.refresh(request) 406 self.token = self._jwt_credentials.token 407 self.expiry = self._jwt_credentials.expiry 408 else: 409 assertion = self._make_authorization_grant_assertion() 410 access_token, expiry, _ = _client.jwt_grant( 411 request, self._token_uri, assertion 412 ) 413 self.token = access_token 414 self.expiry = expiry 415 416 def _create_self_signed_jwt(self, audience): 417 """Create a self-signed JWT from the credentials if requirements are met. 418 419 Args: 420 audience (str): The service URL. ``https://[API_ENDPOINT]/`` 421 """ 422 # https://google.aip.dev/auth/4111 423 if self._always_use_jwt_access: 424 if self._scopes: 425 self._jwt_credentials = jwt.Credentials.from_signing_credentials( 426 self, None, additional_claims={"scope": " ".join(self._scopes)} 427 ) 428 elif audience: 429 self._jwt_credentials = jwt.Credentials.from_signing_credentials( 430 self, audience 431 ) 432 elif self._default_scopes: 433 self._jwt_credentials = jwt.Credentials.from_signing_credentials( 434 self, 435 None, 436 additional_claims={"scope": " ".join(self._default_scopes)}, 437 ) 438 elif not self._scopes and audience: 439 self._jwt_credentials = jwt.Credentials.from_signing_credentials( 440 self, audience 441 ) 442 443 @_helpers.copy_docstring(credentials.Signing) 444 def sign_bytes(self, message): 445 return self._signer.sign(message) 446 447 @property 448 @_helpers.copy_docstring(credentials.Signing) 449 def signer(self): 450 return self._signer 451 452 @property 453 @_helpers.copy_docstring(credentials.Signing) 454 def signer_email(self): 455 return self._service_account_email 456 457 458class IDTokenCredentials(credentials.Signing, credentials.CredentialsWithQuotaProject): 459 """Open ID Connect ID Token-based service account credentials. 460 461 These credentials are largely similar to :class:`.Credentials`, but instead 462 of using an OAuth 2.0 Access Token as the bearer token, they use an Open 463 ID Connect ID Token as the bearer token. These credentials are useful when 464 communicating to services that require ID Tokens and can not accept access 465 tokens. 466 467 Usually, you'll create these credentials with one of the helper 468 constructors. To create credentials using a Google service account 469 private key JSON file:: 470 471 credentials = ( 472 service_account.IDTokenCredentials.from_service_account_file( 473 'service-account.json')) 474 475 476 Or if you already have the service account file loaded:: 477 478 service_account_info = json.load(open('service_account.json')) 479 credentials = ( 480 service_account.IDTokenCredentials.from_service_account_info( 481 service_account_info)) 482 483 484 Both helper methods pass on arguments to the constructor, so you can 485 specify additional scopes and a subject if necessary:: 486 487 credentials = ( 488 service_account.IDTokenCredentials.from_service_account_file( 489 'service-account.json', 490 scopes=['email'], 491 subject='[email protected]')) 492 493 494 The credentials are considered immutable. If you want to modify the scopes 495 or the subject used for delegation, use :meth:`with_scopes` or 496 :meth:`with_subject`:: 497 498 scoped_credentials = credentials.with_scopes(['email']) 499 delegated_credentials = credentials.with_subject(subject) 500 501 """ 502 503 def __init__( 504 self, 505 signer, 506 service_account_email, 507 token_uri, 508 target_audience, 509 additional_claims=None, 510 quota_project_id=None, 511 ): 512 """ 513 Args: 514 signer (google.auth.crypt.Signer): The signer used to sign JWTs. 515 service_account_email (str): The service account's email. 516 token_uri (str): The OAuth 2.0 Token URI. 517 target_audience (str): The intended audience for these credentials, 518 used when requesting the ID Token. The ID Token's ``aud`` claim 519 will be set to this string. 520 additional_claims (Mapping[str, str]): Any additional claims for 521 the JWT assertion used in the authorization grant. 522 quota_project_id (Optional[str]): The project ID used for quota and billing. 523 .. note:: Typically one of the helper constructors 524 :meth:`from_service_account_file` or 525 :meth:`from_service_account_info` are used instead of calling the 526 constructor directly. 527 """ 528 super(IDTokenCredentials, self).__init__() 529 self._signer = signer 530 self._service_account_email = service_account_email 531 self._token_uri = token_uri 532 self._target_audience = target_audience 533 self._quota_project_id = quota_project_id 534 535 if additional_claims is not None: 536 self._additional_claims = additional_claims 537 else: 538 self._additional_claims = {} 539 540 @classmethod 541 def _from_signer_and_info(cls, signer, info, **kwargs): 542 """Creates a credentials instance from a signer and service account 543 info. 544 545 Args: 546 signer (google.auth.crypt.Signer): The signer used to sign JWTs. 547 info (Mapping[str, str]): The service account info. 548 kwargs: Additional arguments to pass to the constructor. 549 550 Returns: 551 google.auth.jwt.IDTokenCredentials: The constructed credentials. 552 553 Raises: 554 ValueError: If the info is not in the expected format. 555 """ 556 kwargs.setdefault("service_account_email", info["client_email"]) 557 kwargs.setdefault("token_uri", info["token_uri"]) 558 return cls(signer, **kwargs) 559 560 @classmethod 561 def from_service_account_info(cls, info, **kwargs): 562 """Creates a credentials instance from parsed service account info. 563 564 Args: 565 info (Mapping[str, str]): The service account info in Google 566 format. 567 kwargs: Additional arguments to pass to the constructor. 568 569 Returns: 570 google.auth.service_account.IDTokenCredentials: The constructed 571 credentials. 572 573 Raises: 574 ValueError: If the info is not in the expected format. 575 """ 576 signer = _service_account_info.from_dict( 577 info, require=["client_email", "token_uri"] 578 ) 579 return cls._from_signer_and_info(signer, info, **kwargs) 580 581 @classmethod 582 def from_service_account_file(cls, filename, **kwargs): 583 """Creates a credentials instance from a service account json file. 584 585 Args: 586 filename (str): The path to the service account json file. 587 kwargs: Additional arguments to pass to the constructor. 588 589 Returns: 590 google.auth.service_account.IDTokenCredentials: The constructed 591 credentials. 592 """ 593 info, signer = _service_account_info.from_filename( 594 filename, require=["client_email", "token_uri"] 595 ) 596 return cls._from_signer_and_info(signer, info, **kwargs) 597 598 def with_target_audience(self, target_audience): 599 """Create a copy of these credentials with the specified target 600 audience. 601 602 Args: 603 target_audience (str): The intended audience for these credentials, 604 used when requesting the ID Token. 605 606 Returns: 607 google.auth.service_account.IDTokenCredentials: A new credentials 608 instance. 609 """ 610 return self.__class__( 611 self._signer, 612 service_account_email=self._service_account_email, 613 token_uri=self._token_uri, 614 target_audience=target_audience, 615 additional_claims=self._additional_claims.copy(), 616 quota_project_id=self.quota_project_id, 617 ) 618 619 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 620 def with_quota_project(self, quota_project_id): 621 return self.__class__( 622 self._signer, 623 service_account_email=self._service_account_email, 624 token_uri=self._token_uri, 625 target_audience=self._target_audience, 626 additional_claims=self._additional_claims.copy(), 627 quota_project_id=quota_project_id, 628 ) 629 630 def _make_authorization_grant_assertion(self): 631 """Create the OAuth 2.0 assertion. 632 633 This assertion is used during the OAuth 2.0 grant to acquire an 634 ID token. 635 636 Returns: 637 bytes: The authorization grant assertion. 638 """ 639 now = _helpers.utcnow() 640 lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS) 641 expiry = now + lifetime 642 643 payload = { 644 "iat": _helpers.datetime_to_secs(now), 645 "exp": _helpers.datetime_to_secs(expiry), 646 # The issuer must be the service account email. 647 "iss": self.service_account_email, 648 # The audience must be the auth token endpoint's URI 649 "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT, 650 # The target audience specifies which service the ID token is 651 # intended for. 652 "target_audience": self._target_audience, 653 } 654 655 payload.update(self._additional_claims) 656 657 token = jwt.encode(self._signer, payload) 658 659 return token 660 661 @_helpers.copy_docstring(credentials.Credentials) 662 def refresh(self, request): 663 assertion = self._make_authorization_grant_assertion() 664 access_token, expiry, _ = _client.id_token_jwt_grant( 665 request, self._token_uri, assertion 666 ) 667 self.token = access_token 668 self.expiry = expiry 669 670 @property 671 def service_account_email(self): 672 """The service account email.""" 673 return self._service_account_email 674 675 @_helpers.copy_docstring(credentials.Signing) 676 def sign_bytes(self, message): 677 return self._signer.sign(message) 678 679 @property 680 @_helpers.copy_docstring(credentials.Signing) 681 def signer(self): 682 return self._signer 683 684 @property 685 @_helpers.copy_docstring(credentials.Signing) 686 def signer_email(self): 687 return self._service_account_email 688