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"""Google Compute Engine credentials. 16 17This module provides authentication for an application running on Google 18Compute Engine using the Compute Engine metadata server. 19 20""" 21 22import datetime 23 24import six 25 26from google.auth import _helpers 27from google.auth import credentials 28from google.auth import exceptions 29from google.auth import iam 30from google.auth import jwt 31from google.auth.compute_engine import _metadata 32from google.oauth2 import _client 33 34 35class Credentials(credentials.Scoped, credentials.CredentialsWithQuotaProject): 36 """Compute Engine Credentials. 37 38 These credentials use the Google Compute Engine metadata server to obtain 39 OAuth 2.0 access tokens associated with the instance's service account, 40 and are also used for Cloud Run, Flex and App Engine (except for the Python 41 2.7 runtime, which is supported only on older versions of this library). 42 43 For more information about Compute Engine authentication, including how 44 to configure scopes, see the `Compute Engine authentication 45 documentation`_. 46 47 .. note:: On Compute Engine the metadata server ignores requested scopes. 48 On Cloud Run, Flex and App Engine the server honours requested scopes. 49 50 .. _Compute Engine authentication documentation: 51 https://cloud.google.com/compute/docs/authentication#using 52 """ 53 54 def __init__( 55 self, 56 service_account_email="default", 57 quota_project_id=None, 58 scopes=None, 59 default_scopes=None, 60 ): 61 """ 62 Args: 63 service_account_email (str): The service account email to use, or 64 'default'. A Compute Engine instance may have multiple service 65 accounts. 66 quota_project_id (Optional[str]): The project ID used for quota and 67 billing. 68 scopes (Optional[Sequence[str]]): The list of scopes for the credentials. 69 default_scopes (Optional[Sequence[str]]): Default scopes passed by a 70 Google client library. Use 'scopes' for user-defined scopes. 71 """ 72 super(Credentials, self).__init__() 73 self._service_account_email = service_account_email 74 self._quota_project_id = quota_project_id 75 self._scopes = scopes 76 self._default_scopes = default_scopes 77 78 def _retrieve_info(self, request): 79 """Retrieve information about the service account. 80 81 Updates the scopes and retrieves the full service account email. 82 83 Args: 84 request (google.auth.transport.Request): The object used to make 85 HTTP requests. 86 """ 87 info = _metadata.get_service_account_info( 88 request, service_account=self._service_account_email 89 ) 90 91 self._service_account_email = info["email"] 92 93 # Don't override scopes requested by the user. 94 if self._scopes is None: 95 self._scopes = info["scopes"] 96 97 def refresh(self, request): 98 """Refresh the access token and scopes. 99 100 Args: 101 request (google.auth.transport.Request): The object used to make 102 HTTP requests. 103 104 Raises: 105 google.auth.exceptions.RefreshError: If the Compute Engine metadata 106 service can't be reached if if the instance has not 107 credentials. 108 """ 109 scopes = self._scopes if self._scopes is not None else self._default_scopes 110 try: 111 self._retrieve_info(request) 112 self.token, self.expiry = _metadata.get_service_account_token( 113 request, service_account=self._service_account_email, scopes=scopes 114 ) 115 except exceptions.TransportError as caught_exc: 116 new_exc = exceptions.RefreshError(caught_exc) 117 six.raise_from(new_exc, caught_exc) 118 119 @property 120 def service_account_email(self): 121 """The service account email. 122 123 .. note:: This is not guaranteed to be set until :meth:`refresh` has been 124 called. 125 """ 126 return self._service_account_email 127 128 @property 129 def requires_scopes(self): 130 return not self._scopes 131 132 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 133 def with_quota_project(self, quota_project_id): 134 return self.__class__( 135 service_account_email=self._service_account_email, 136 quota_project_id=quota_project_id, 137 scopes=self._scopes, 138 ) 139 140 @_helpers.copy_docstring(credentials.Scoped) 141 def with_scopes(self, scopes, default_scopes=None): 142 # Compute Engine credentials can not be scoped (the metadata service 143 # ignores the scopes parameter). App Engine, Cloud Run and Flex support 144 # requesting scopes. 145 return self.__class__( 146 scopes=scopes, 147 default_scopes=default_scopes, 148 service_account_email=self._service_account_email, 149 quota_project_id=self._quota_project_id, 150 ) 151 152 153_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds 154_DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token" 155 156 157class IDTokenCredentials(credentials.CredentialsWithQuotaProject, credentials.Signing): 158 """Open ID Connect ID Token-based service account credentials. 159 160 These credentials relies on the default service account of a GCE instance. 161 162 ID token can be requested from `GCE metadata server identity endpoint`_, IAM 163 token endpoint or other token endpoints you specify. If metadata server 164 identity endpoint is not used, the GCE instance must have been started with 165 a service account that has access to the IAM Cloud API. 166 167 .. _GCE metadata server identity endpoint: 168 https://cloud.google.com/compute/docs/instances/verifying-instance-identity 169 """ 170 171 def __init__( 172 self, 173 request, 174 target_audience, 175 token_uri=None, 176 additional_claims=None, 177 service_account_email=None, 178 signer=None, 179 use_metadata_identity_endpoint=False, 180 quota_project_id=None, 181 ): 182 """ 183 Args: 184 request (google.auth.transport.Request): The object used to make 185 HTTP requests. 186 target_audience (str): The intended audience for these credentials, 187 used when requesting the ID Token. The ID Token's ``aud`` claim 188 will be set to this string. 189 token_uri (str): The OAuth 2.0 Token URI. 190 additional_claims (Mapping[str, str]): Any additional claims for 191 the JWT assertion used in the authorization grant. 192 service_account_email (str): Optional explicit service account to 193 use to sign JWT tokens. 194 By default, this is the default GCE service account. 195 signer (google.auth.crypt.Signer): The signer used to sign JWTs. 196 In case the signer is specified, the request argument will be 197 ignored. 198 use_metadata_identity_endpoint (bool): Whether to use GCE metadata 199 identity endpoint. For backward compatibility the default value 200 is False. If set to True, ``token_uri``, ``additional_claims``, 201 ``service_account_email``, ``signer`` argument should not be set; 202 otherwise ValueError will be raised. 203 quota_project_id (Optional[str]): The project ID used for quota and 204 billing. 205 206 Raises: 207 ValueError: 208 If ``use_metadata_identity_endpoint`` is set to True, and one of 209 ``token_uri``, ``additional_claims``, ``service_account_email``, 210 ``signer`` arguments is set. 211 """ 212 super(IDTokenCredentials, self).__init__() 213 214 self._quota_project_id = quota_project_id 215 self._use_metadata_identity_endpoint = use_metadata_identity_endpoint 216 self._target_audience = target_audience 217 218 if use_metadata_identity_endpoint: 219 if token_uri or additional_claims or service_account_email or signer: 220 raise ValueError( 221 "If use_metadata_identity_endpoint is set, token_uri, " 222 "additional_claims, service_account_email, signer arguments" 223 " must not be set" 224 ) 225 self._token_uri = None 226 self._additional_claims = None 227 self._signer = None 228 229 if service_account_email is None: 230 sa_info = _metadata.get_service_account_info(request) 231 self._service_account_email = sa_info["email"] 232 else: 233 self._service_account_email = service_account_email 234 235 if not use_metadata_identity_endpoint: 236 if signer is None: 237 signer = iam.Signer( 238 request=request, 239 credentials=Credentials(), 240 service_account_email=self._service_account_email, 241 ) 242 self._signer = signer 243 self._token_uri = token_uri or _DEFAULT_TOKEN_URI 244 245 if additional_claims is not None: 246 self._additional_claims = additional_claims 247 else: 248 self._additional_claims = {} 249 250 def with_target_audience(self, target_audience): 251 """Create a copy of these credentials with the specified target 252 audience. 253 Args: 254 target_audience (str): The intended audience for these credentials, 255 used when requesting the ID Token. 256 Returns: 257 google.auth.service_account.IDTokenCredentials: A new credentials 258 instance. 259 """ 260 # since the signer is already instantiated, 261 # the request is not needed 262 if self._use_metadata_identity_endpoint: 263 return self.__class__( 264 None, 265 target_audience=target_audience, 266 use_metadata_identity_endpoint=True, 267 quota_project_id=self._quota_project_id, 268 ) 269 else: 270 return self.__class__( 271 None, 272 service_account_email=self._service_account_email, 273 token_uri=self._token_uri, 274 target_audience=target_audience, 275 additional_claims=self._additional_claims.copy(), 276 signer=self.signer, 277 use_metadata_identity_endpoint=False, 278 quota_project_id=self._quota_project_id, 279 ) 280 281 @_helpers.copy_docstring(credentials.CredentialsWithQuotaProject) 282 def with_quota_project(self, quota_project_id): 283 284 # since the signer is already instantiated, 285 # the request is not needed 286 if self._use_metadata_identity_endpoint: 287 return self.__class__( 288 None, 289 target_audience=self._target_audience, 290 use_metadata_identity_endpoint=True, 291 quota_project_id=quota_project_id, 292 ) 293 else: 294 return self.__class__( 295 None, 296 service_account_email=self._service_account_email, 297 token_uri=self._token_uri, 298 target_audience=self._target_audience, 299 additional_claims=self._additional_claims.copy(), 300 signer=self.signer, 301 use_metadata_identity_endpoint=False, 302 quota_project_id=quota_project_id, 303 ) 304 305 def _make_authorization_grant_assertion(self): 306 """Create the OAuth 2.0 assertion. 307 This assertion is used during the OAuth 2.0 grant to acquire an 308 ID token. 309 Returns: 310 bytes: The authorization grant assertion. 311 """ 312 now = _helpers.utcnow() 313 lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS) 314 expiry = now + lifetime 315 316 payload = { 317 "iat": _helpers.datetime_to_secs(now), 318 "exp": _helpers.datetime_to_secs(expiry), 319 # The issuer must be the service account email. 320 "iss": self.service_account_email, 321 # The audience must be the auth token endpoint's URI 322 "aud": self._token_uri, 323 # The target audience specifies which service the ID token is 324 # intended for. 325 "target_audience": self._target_audience, 326 } 327 328 payload.update(self._additional_claims) 329 330 token = jwt.encode(self._signer, payload) 331 332 return token 333 334 def _call_metadata_identity_endpoint(self, request): 335 """Request ID token from metadata identity endpoint. 336 337 Args: 338 request (google.auth.transport.Request): The object used to make 339 HTTP requests. 340 341 Returns: 342 Tuple[str, datetime.datetime]: The ID token and the expiry of the ID token. 343 344 Raises: 345 google.auth.exceptions.RefreshError: If the Compute Engine metadata 346 service can't be reached or if the instance has no credentials. 347 ValueError: If extracting expiry from the obtained ID token fails. 348 """ 349 try: 350 path = "instance/service-accounts/default/identity" 351 params = {"audience": self._target_audience, "format": "full"} 352 id_token = _metadata.get(request, path, params=params) 353 except exceptions.TransportError as caught_exc: 354 new_exc = exceptions.RefreshError(caught_exc) 355 six.raise_from(new_exc, caught_exc) 356 357 _, payload, _, _ = jwt._unverified_decode(id_token) 358 return id_token, datetime.datetime.fromtimestamp(payload["exp"]) 359 360 def refresh(self, request): 361 """Refreshes the ID token. 362 363 Args: 364 request (google.auth.transport.Request): The object used to make 365 HTTP requests. 366 367 Raises: 368 google.auth.exceptions.RefreshError: If the credentials could 369 not be refreshed. 370 ValueError: If extracting expiry from the obtained ID token fails. 371 """ 372 if self._use_metadata_identity_endpoint: 373 self.token, self.expiry = self._call_metadata_identity_endpoint(request) 374 else: 375 assertion = self._make_authorization_grant_assertion() 376 access_token, expiry, _ = _client.id_token_jwt_grant( 377 request, self._token_uri, assertion 378 ) 379 self.token = access_token 380 self.expiry = expiry 381 382 @property 383 @_helpers.copy_docstring(credentials.Signing) 384 def signer(self): 385 return self._signer 386 387 def sign_bytes(self, message): 388 """Signs the given message. 389 390 Args: 391 message (bytes): The message to sign. 392 393 Returns: 394 bytes: The message's cryptographic signature. 395 396 Raises: 397 ValueError: 398 Signer is not available if metadata identity endpoint is used. 399 """ 400 if self._use_metadata_identity_endpoint: 401 raise ValueError( 402 "Signer is not available if metadata identity endpoint is used" 403 ) 404 return self._signer.sign(message) 405 406 @property 407 def service_account_email(self): 408 """The service account email.""" 409 return self._service_account_email 410 411 @property 412 def signer_email(self): 413 return self._service_account_email 414