From edf0a3dd771f8ca080c90a41f28801f32656d888 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Wed, 19 Jun 2019 14:01:03 -0700 Subject: [PATCH 1/7] Add support for imersonated_credentials.Sign, IDToken --- google/auth/impersonated_credentials.py | 115 +++++++++++++++++++++++- tests/test_impersonated_credentials.py | 97 +++++++++++++++++++- 2 files changed, 208 insertions(+), 4 deletions(-) diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index 32dfe8309..8b6fd555d 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -25,6 +25,7 @@ https://cloud.google.com/iam/credentials/reference/rest/ """ +import base64 import copy from datetime import datetime import json @@ -35,6 +36,8 @@ from google.auth import _helpers from google.auth import credentials from google.auth import exceptions +from google.auth import jwt +from google.auth.transport.requests import AuthorizedSession _DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds @@ -43,6 +46,12 @@ _IAM_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/projects/-' + '/serviceAccounts/{}:generateAccessToken') +_IAM_SIGN_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/projects/-' + + '/serviceAccounts/{}:signBlob') + +_IAM_IDTOKEN_ENDPOINT = ('https://iamcredentials.googleapis.com/v1/' + + 'projects/-/serviceAccounts/{}:generateIdToken') + _REFRESH_ERROR = 'Unable to acquire impersonated credentials' @@ -94,7 +103,7 @@ def _make_iam_token_request(request, principal, headers, body): six.raise_from(new_exc, caught_exc) -class Credentials(credentials.Credentials): +class Credentials(credentials.Credentials, credentials.Signing): """This module defines impersonated credentials which are essentially impersonated identities. @@ -172,7 +181,8 @@ def __init__(self, source_credentials, target_principal, granted to the prceeding identity. For example, if set to [serviceAccountB, serviceAccountC], the source_credential must have the Token Creator role on serviceAccountB. - serviceAccountB must have the Token Creator on serviceAccountC. + credentials.refresh(request) must have the Token Creator on + serviceAccountC. Finally, C must have Token Creator on target_principal. If left unset, source_credential must have that role on target_principal. @@ -229,3 +239,104 @@ def _update_token(self, request): principal=self._target_principal, headers=headers, body=body) + + def sign_bytes(self, message): + + iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal) + + body = { + "payload": base64.b64encode(message), + "delegates": self._delegates + } + + headers = { + 'Content-Type': 'application/json', + } + + authed_session = AuthorizedSession(self._source_credentials) + + response = authed_session.post( + url=iam_sign_endpoint, + headers=headers, + data=json.dumps(body)) + + return base64.b64decode(response.json()['signedBlob']) + + @property + def signer_email(self): + return self._target_principal + + @property + def service_account_email(self): + return self._target_principal + + @property + def signer(self): + return self + + +_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds +_DEFAULT_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token' + + +class IDTokenCredentials(credentials.Credentials): + """Open ID Connect ID Token-based service account credentials. + + """ + def __init__(self, target_credentials, + target_audience=None): + """ + Args: + targete_credentials (google.auth.Credentials): The target + credential used as to acquire the id tokens for. + target_audience (string): + additional_claims (Sequece[str]): + """ + super(IDTokenCredentials, self).__init__() + + if not isinstance(target_credentials, + Credentials): + raise exceptions.GoogleAuthError("Provided Credential must be " + "impersonated_credentials") + self._target_credentials = target_credentials + self._target_audience = target_audience + + def from_credentials(self, target_credentials, + target_audience=None): + return self.__class__( + target_credentials=self._target_credentials, + target_audience=target_audience) + + def with_target_audience(self, target_audience): + return self.__class__( + target_credentials=self._target_credentials, + target_audience=target_audience) + + @_helpers.copy_docstring(credentials.Credentials) + def refresh(self, request): + + iam_sign_endpoint = _IAM_IDTOKEN_ENDPOINT.format(self. + _target_credentials. + signer_email) + + body = { + "audience": self._target_audience, + "delegates": self._target_credentials._delegates + } + + headers = { + 'Content-Type': 'application/json', + } + + authed_session = AuthorizedSession(self._target_credentials. + _source_credentials) + + response = authed_session.post( + url=iam_sign_endpoint, + headers=headers, + data=json.dumps(body)) + + id_token = response.json()['token'] + self.token = id_token + self.expiry = datetime.fromtimestamp(jwt.decode(id_token, + verify=False)['exp']) diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index 68a2af8f2..bb189f182 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -62,10 +62,13 @@ class TestImpersonatedCredentials(object): SOURCE_CREDENTIALS = service_account.Credentials( SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI) - def make_credentials(self, lifetime=LIFETIME): + def make_credentials(self, lifetime=LIFETIME, target_principal=None): + if target_principal is None: + target_principal = self.TARGET_PRINCIPAL + return Credentials( source_credentials=self.SOURCE_CREDENTIALS, - target_principal=self.TARGET_PRINCIPAL, + target_principal=target_principal, target_scopes=self.TARGET_SCOPES, delegates=self.DELEGATES, lifetime=lifetime) @@ -176,3 +179,93 @@ def test_refresh_failure_http_error(self, mock_donor_credentials): def test_expired(self): credentials = self.make_credentials(lifetime=None) assert credentials.expired + + def test_signer(self): + credentials = self.make_credentials() + assert isinstance(credentials.signer, + impersonated_credentials.Credentials) + + def test_signer_email(self): + credentials = self.make_credentials( + target_principal=self.TARGET_PRINCIPAL) + assert credentials.signer_email == self.TARGET_PRINCIPAL + + def test_service_account_email(self): + credentials = self.make_credentials( + target_principal=self.TARGET_PRINCIPAL) + assert credentials.service_account_email == self.TARGET_PRINCIPAL + + def test_sign_bytes(self, mock_donor_credentials): + credentials = self.make_credentials(lifetime=None) + token = 'token' + + expire_time = ( + _helpers.utcnow().replace(microsecond=0) + + datetime.timedelta(seconds=500)).isoformat('T') + 'Z' + response_body = { + "accessToken": token, + "expireTime": expire_time + } + + request = self.make_request( + data=json.dumps(response_body), + status=http_client.OK) + +# sign_response_body = { +# "keyId": "1", +# "signedBlob": "c2lnbmF0dXJl" +# } +# sign_response = mock.create_autospec(transport.Response, +# instance=True) +# sign_response.data = sign_response_body +# sign_response.status = http_client.OK +# sign_request = self.make_request( +# data=json.dumps(sign_response_body), +# status=http_client.OK, side_effect=[sign_response]) + + credentials.refresh(request) + + assert credentials.valid + assert not credentials.expired + +# signature = credentials.sign_bytes(b"some bytes") +# assert signature == b'signature' + + def test_id_token_success(self, mock_donor_credentials): + credentials = self.make_credentials(lifetime=None) + token = 'token' +# idtoken = 'idtoken' +# target_audience = 'https://foo.bar' + + expire_time = ( + _helpers.utcnow().replace(microsecond=0) + + datetime.timedelta(seconds=500)).isoformat('T') + 'Z' + response_body = { + "accessToken": token, + "expireTime": expire_time + } + + request = self.make_request( + data=json.dumps(response_body), + status=http_client.OK) + +# id_token_response_body = { +# "token": idtoken +# } +# id_token_response = mock.create_autospec(transport.Response, +# instance=True) +# id_token_response.data = id_token_response_body +# id_token_response.status = http_client.OK +# sign_request = self.make_request( +# data=json.dumps(id_token_response_body), +# status=http_client.OK) + + credentials.refresh(request) + + assert credentials.valid + assert not credentials.expired + +# id_creds = impersonated_credentials.IDTokenCredentials( +# credentials, target_audience=target_audience) +# id_creds.refresh(request) +# assert id_creds.token == idtoken From ffa4a346d01c6c6e9cb9c0cb3c41e81907acd197 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Thu, 20 Jun 2019 07:22:20 -0700 Subject: [PATCH 2/7] fix pytype; add docs --- docs/index.rst | 1 + docs/user-guide.rst | 50 +++++++++++++++++++++++++ google/auth/impersonated_credentials.py | 7 ++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1eb3d861a..4287c3db3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,6 +14,7 @@ also provides integration with several HTTP libraries. - Support for Google :func:`Application Default Credentials `. - Support for signing and verifying :mod:`JWTs `. +- Support for creating `Google ID Tokens `__. - Support for verifying and decoding :mod:`ID Tokens `. - Support for Google :mod:`Service Account credentials `. - Support for Google :mod:`Impersonated Credentials `. diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 62836623f..8b442e9da 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -234,6 +234,56 @@ In the example above `source_credentials` does not have direct access to list bu in the target project. Using `ImpersonatedCredentials` will allow the source_credentials to assume the identity of a target_principal that does have access. +Identity Tokens ++++++++++++++++ + +`Google OpenID Connect`_ tokens are avaiable through both ServiceAccount, ImpersonatedCredentials, +and Compute modules. These tokens can be used to authenticate against `Cloud Functions`_, +`Cloud Run`_, a user service behind `Identity Aware Proxy`_ or any other service capable +of verifying a `Google ID Token`_. + +ServiceAccount :: + + from google.oauth2 import service_account + + target_audience = 'https://example.com' + + creds = service_account.IDTokenCredentials.from_service_account_file( + '/path/to/svc.json', + target_audience=target_audience) + + +Compute :: + + from google.auth import compute_engine + import google.auth.transport.requests + + target_audience = 'https://example.com' + + request = google.auth.transport.requests.Request() + creds = compute_engine.IDTokenCredentials(request, + target_audience=target_audience) + +Impersonated :: + + from google.auth import impersonated_credentials + + # get target_credentials from a source_credentials + + target_audience = 'https://example.com' + + creds = impersonated_credentials.IDTokenCredentials( + target_credentials, + target_audience=target_audience) + +IDToken verification can be done for various type of IDTokens using the :class:`google.oauth2.id_token` module + +.. _Cloud Functions: https://cloud.google.com/functions/ +.. _Cloud Run: https://cloud.google.com/run/ +.. _Identity Aware Proxy: https://cloud.google.com/iap/ +.. _Google OpenID Connect: https://developers.google.com/identity/protocols/OpenIDConnect +.. _Google ID Token: https://developers.google.com/identity/protocols/OpenIDConnect#validatinganidtoken + Making authenticated requests ----------------------------- diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index 8b6fd555d..5a03ea806 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -258,7 +258,7 @@ def sign_bytes(self, message): response = authed_session.post( url=iam_sign_endpoint, headers=headers, - data=json.dumps(body)) + data=json.dumps(body).encode('utf-8')) return base64.b64decode(response.json()['signedBlob']) @@ -289,8 +289,7 @@ def __init__(self, target_credentials, Args: targete_credentials (google.auth.Credentials): The target credential used as to acquire the id tokens for. - target_audience (string): - additional_claims (Sequece[str]): + target_audience (string): Audience to issue the token for. """ super(IDTokenCredentials, self).__init__() @@ -334,7 +333,7 @@ def refresh(self, request): response = authed_session.post( url=iam_sign_endpoint, headers=headers, - data=json.dumps(body)) + data=json.dumps(body).encode('utf-8')) id_token = response.json()['token'] self.token = id_token From d35412bebc1960235924323e491e12aad8ee3695 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Fri, 26 Jul 2019 11:29:44 -0700 Subject: [PATCH 3/7] add include_email; fix typos --- google/auth/impersonated_credentials.py | 23 ++++++++++++++++------- tests/test_impersonated_credentials.py | 5 ++--- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index 5a03ea806..e5ac059aa 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -54,6 +54,10 @@ _REFRESH_ERROR = 'Unable to acquire impersonated credentials' +_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds + +_DEFAULT_TOKEN_URI = 'https://oauth2.googleapis.com/token' + def _make_iam_token_request(request, principal, headers, body): """Makes a request to the Google Cloud IAM service for an access token. @@ -275,21 +279,18 @@ def signer(self): return self -_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds -_DEFAULT_TOKEN_URI = 'https://www.googleapis.com/oauth2/v4/token' - - class IDTokenCredentials(credentials.Credentials): """Open ID Connect ID Token-based service account credentials. """ def __init__(self, target_credentials, - target_audience=None): + target_audience=None, include_email=False): """ Args: - targete_credentials (google.auth.Credentials): The target + target_credentials (google.auth.Credentials): The target credential used as to acquire the id tokens for. target_audience (string): Audience to issue the token for. + include_email (bool): Include email in IdToken """ super(IDTokenCredentials, self).__init__() @@ -299,6 +300,7 @@ def __init__(self, target_credentials, "impersonated_credentials") self._target_credentials = target_credentials self._target_audience = target_audience + self._include_email = include_email def from_credentials(self, target_credentials, target_audience=None): @@ -311,6 +313,12 @@ def with_target_audience(self, target_audience): target_credentials=self._target_credentials, target_audience=target_audience) + def with_include_email(self, include_email): + return self.__class__( + target_credentials=self._target_credentials, + target_audience=self._target_audience, + include_email=include_email) + @_helpers.copy_docstring(credentials.Credentials) def refresh(self, request): @@ -320,7 +328,8 @@ def refresh(self, request): body = { "audience": self._target_audience, - "delegates": self._target_credentials._delegates + "delegates": self._target_credentials._delegates, + "includeEmail": self._include_email } headers = { diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index bb189f182..fd689d158 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -62,9 +62,8 @@ class TestImpersonatedCredentials(object): SOURCE_CREDENTIALS = service_account.Credentials( SIGNER, SERVICE_ACCOUNT_EMAIL, TOKEN_URI) - def make_credentials(self, lifetime=LIFETIME, target_principal=None): - if target_principal is None: - target_principal = self.TARGET_PRINCIPAL + def make_credentials(self, lifetime=LIFETIME, + target_principal=TARGET_PRINCIPAL): return Credentials( source_credentials=self.SOURCE_CREDENTIALS, From a43d07af05596038548696472d2b61632d7b25cc Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Tue, 30 Jul 2019 07:08:04 -0700 Subject: [PATCH 4/7] add testcases, coverage --- docs/user-guide.rst | 38 ++++- google/auth/impersonated_credentials.py | 2 +- tests/test_impersonated_credentials.py | 189 ++++++++++++++++++++---- 3 files changed, 192 insertions(+), 37 deletions(-) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 8b442e9da..9eda99f5a 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -237,10 +237,11 @@ to assume the identity of a target_principal that does have access. Identity Tokens +++++++++++++++ -`Google OpenID Connect`_ tokens are avaiable through both ServiceAccount, ImpersonatedCredentials, -and Compute modules. These tokens can be used to authenticate against `Cloud Functions`_, -`Cloud Run`_, a user service behind `Identity Aware Proxy`_ or any other service capable -of verifying a `Google ID Token`_. +`Google OpenID Connect`_ tokens are avaiable through :mod:`Service Account `, +:mod:`Impersonated `, +and :mod:`Compute Engine `. These tokens can be used to +authenticate against `Cloud Functions`_, `Cloud Run`_, a user service behind +`Identity Aware Proxy`_ or any other service capable of verifying a `Google ID Token`_. ServiceAccount :: @@ -268,7 +269,7 @@ Impersonated :: from google.auth import impersonated_credentials - # get target_credentials from a source_credentials + # get target_credentials from a source_credential target_audience = 'https://example.com' @@ -278,6 +279,33 @@ Impersonated :: IDToken verification can be done for various type of IDTokens using the :class:`google.oauth2.id_token` module +A sample end-to-end flow using an ID Token against a Cloud Run endpoint maybe :: + + from google.oauth2 import id_token + from google.oauth2 import service_account + import google.auth + import google.auth.transport.requests + from google.auth.transport.requests import AuthorizedSession + + target_audience = 'https://your-cloud-run-app.a.run.app' + url = 'https://your-cloud-run-app.a.run.app' + + creds = service_account.IDTokenCredentials.from_service_account_file( + '/path/to/svc.json', target_audience=target_audience) + + authed_session = AuthorizedSession(creds) + + # make authenticated request and print the response, status_code + resp = authed_session.get(url) + print resp.status_code + print resp.text + + # to verify an ID Token + request = google.auth.transport.requests.Request() + token = creds.token + print token + print id_token.verify_token(token,request) + .. _Cloud Functions: https://cloud.google.com/functions/ .. _Cloud Run: https://cloud.google.com/run/ .. _Identity Aware Proxy: https://cloud.google.com/iap/ diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index e5ac059aa..a9d3f9489 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -262,7 +262,7 @@ def sign_bytes(self, message): response = authed_session.post( url=iam_sign_endpoint, headers=headers, - data=json.dumps(body).encode('utf-8')) + json=body) return base64.b64decode(response.json()['signedBlob']) diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index fd689d158..9945401d4 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -35,6 +35,14 @@ SERVICE_ACCOUNT_JSON_FILE = os.path.join(DATA_DIR, 'service_account.json') +ID_TOKEN_DATA = ('eyJhbGciOiJSUzI1NiIsImtpZCI6ImRmMzc1ODkwOGI3OTIyOTNhZDk3N2Ew' + 'Yjk5MWQ5OGE3N2Y0ZWVlY2QiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwc' + 'zovL2Zvby5iYXIiLCJhenAiOiIxMDIxMDE1NTA4MzQyMDA3MDg1NjgiLCJle' + 'HAiOjE1NjQ0NzUwNTEsImlhdCI6MTU2NDQ3MTQ1MSwiaXNzIjoiaHR0cHM6L' + 'y9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTAyMTAxNTUwODM0MjAwN' + 'zA4NTY4In0.redacted') +ID_TOKEN_EXPIRY = 1564475051 + with open(SERVICE_ACCOUNT_JSON_FILE, 'r') as fh: SERVICE_ACCOUNT_INFO = json.load(fh) @@ -52,6 +60,38 @@ def mock_donor_credentials(): yield grant +class MockResponse: + def __init__(self, json_data, status_code): + self.json_data = json_data + self.status_code = status_code + + def json(self): + return self.json_data + + +@pytest.fixture +def mock_authorizedsession_sign(): + with mock.patch('google.auth.transport.requests.AuthorizedSession.request', + autospec=True) as auth_session: + data = { + "keyId": "1", + "signedBlob": "c2lnbmF0dXJl" + } + auth_session.return_value = MockResponse(data, http_client.OK) + yield auth_session + + +@pytest.fixture +def mock_authorizedsession_idtoken(): + with mock.patch('google.auth.transport.requests.AuthorizedSession.request', + autospec=True) as auth_session: + data = { + "token": ID_TOKEN_DATA + } + auth_session.return_value = MockResponse(data, http_client.OK) + yield auth_session + + class TestImpersonatedCredentials(object): SERVICE_ACCOUNT_EMAIL = 'service-account@example.com' @@ -194,9 +234,39 @@ def test_service_account_email(self): target_principal=self.TARGET_PRINCIPAL) assert credentials.service_account_email == self.TARGET_PRINCIPAL - def test_sign_bytes(self, mock_donor_credentials): + def test_sign_bytes(self, mock_donor_credentials, + mock_authorizedsession_sign): + credentials = self.make_credentials(lifetime=None) + token = 'token' + + expire_time = ( + _helpers.utcnow().replace(microsecond=0) + + datetime.timedelta(seconds=500)).isoformat('T') + 'Z' + token_response_body = { + "accessToken": token, + "expireTime": expire_time + } + + response = mock.create_autospec(transport.Response, instance=False) + response.status = http_client.OK + response.data = _helpers.to_bytes(json.dumps(token_response_body)) + + request = mock.create_autospec(transport.Request, instance=False) + request.return_value = response + + credentials.refresh(request) + + assert credentials.valid + assert not credentials.expired + + signature = credentials.sign_bytes(b'signed bytes') + assert signature == b'signature' + + def test_id_token_success(self, mock_donor_credentials, + mock_authorizedsession_idtoken): credentials = self.make_credentials(lifetime=None) token = 'token' + target_audience = 'https://foo.bar' expire_time = ( _helpers.utcnow().replace(microsecond=0) + @@ -210,31 +280,54 @@ def test_sign_bytes(self, mock_donor_credentials): data=json.dumps(response_body), status=http_client.OK) -# sign_response_body = { -# "keyId": "1", -# "signedBlob": "c2lnbmF0dXJl" -# } -# sign_response = mock.create_autospec(transport.Response, -# instance=True) -# sign_response.data = sign_response_body -# sign_response.status = http_client.OK -# sign_request = self.make_request( -# data=json.dumps(sign_response_body), -# status=http_client.OK, side_effect=[sign_response]) + credentials.refresh(request) + + assert credentials.valid + assert not credentials.expired + + id_creds = impersonated_credentials.IDTokenCredentials( + credentials, target_audience=target_audience) + id_creds.refresh(request) + + assert id_creds.token == ID_TOKEN_DATA + assert id_creds.expiry == datetime.datetime.fromtimestamp( + ID_TOKEN_EXPIRY) + + def test_id_token_from_credential(self, mock_donor_credentials, + mock_authorizedsession_idtoken): + credentials = self.make_credentials(lifetime=None) + token = 'token' + target_audience = 'https://foo.bar' + + expire_time = ( + _helpers.utcnow().replace(microsecond=0) + + datetime.timedelta(seconds=500)).isoformat('T') + 'Z' + response_body = { + "accessToken": token, + "expireTime": expire_time + } + + request = self.make_request( + data=json.dumps(response_body), + status=http_client.OK) credentials.refresh(request) assert credentials.valid assert not credentials.expired -# signature = credentials.sign_bytes(b"some bytes") -# assert signature == b'signature' + id_creds = impersonated_credentials.IDTokenCredentials( + credentials, target_audience=target_audience) + id_creds = id_creds.from_credentials(target_credentials=credentials) + id_creds.refresh(request) + + assert id_creds.token == ID_TOKEN_DATA - def test_id_token_success(self, mock_donor_credentials): + def test_id_token_with_target_audience(self, mock_donor_credentials, + mock_authorizedsession_idtoken): credentials = self.make_credentials(lifetime=None) token = 'token' -# idtoken = 'idtoken' -# target_audience = 'https://foo.bar' + target_audience = 'https://foo.bar' expire_time = ( _helpers.utcnow().replace(microsecond=0) + @@ -248,23 +341,57 @@ def test_id_token_success(self, mock_donor_credentials): data=json.dumps(response_body), status=http_client.OK) -# id_token_response_body = { -# "token": idtoken -# } -# id_token_response = mock.create_autospec(transport.Response, -# instance=True) -# id_token_response.data = id_token_response_body -# id_token_response.status = http_client.OK -# sign_request = self.make_request( -# data=json.dumps(id_token_response_body), -# status=http_client.OK) + credentials.refresh(request) + + assert credentials.valid + assert not credentials.expired + + id_creds = impersonated_credentials.IDTokenCredentials( + credentials) + id_creds = id_creds.with_target_audience( + target_audience=target_audience) + id_creds.refresh(request) + + assert id_creds.token == ID_TOKEN_DATA + assert id_creds.expiry == datetime.datetime.fromtimestamp( + ID_TOKEN_EXPIRY) + + def test_id_token_invalid_cred(self, mock_donor_credentials, + mock_authorizedsession_idtoken): + credentials = None + + with pytest.raises(exceptions.GoogleAuthError) as excinfo: + impersonated_credentials.IDTokenCredentials(credentials) + + assert excinfo.match('Provided Credential must be' + ' impersonated_credentials') + + def test_id_token_with_include_email(self, mock_donor_credentials, + mock_authorizedsession_idtoken): + credentials = self.make_credentials(lifetime=None) + token = 'token' + target_audience = 'https://foo.bar' + + expire_time = ( + _helpers.utcnow().replace(microsecond=0) + + datetime.timedelta(seconds=500)).isoformat('T') + 'Z' + response_body = { + "accessToken": token, + "expireTime": expire_time + } + + request = self.make_request( + data=json.dumps(response_body), + status=http_client.OK) credentials.refresh(request) assert credentials.valid assert not credentials.expired -# id_creds = impersonated_credentials.IDTokenCredentials( -# credentials, target_audience=target_audience) -# id_creds.refresh(request) -# assert id_creds.token == idtoken + id_creds = impersonated_credentials.IDTokenCredentials( + credentials, target_audience=target_audience) + id_creds = id_creds.with_include_email(True) + id_creds.refresh(request) + + assert id_creds.token == ID_TOKEN_DATA From e7d28721de325ca3b043a3a1141a112dba4a36f9 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Wed, 7 Aug 2019 10:38:34 -0700 Subject: [PATCH 5/7] fix docstring --- google/auth/impersonated_credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index a9d3f9489..09c1f5192 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -185,7 +185,7 @@ def __init__(self, source_credentials, target_principal, granted to the prceeding identity. For example, if set to [serviceAccountB, serviceAccountC], the source_credential must have the Token Creator role on serviceAccountB. - credentials.refresh(request) must have the Token Creator on + serviceAccountB must have the Token Creator on serviceAccountC. Finally, C must have Token Creator on target_principal. If left unset, source_credential must have that role on From 23758aecfee26581d81fe00f8dc3c03ce48c5618 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Wed, 7 Aug 2019 12:13:04 -0700 Subject: [PATCH 6/7] use python3 print --- google/auth/impersonated_credentials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index 09c1f5192..bb2bbf26a 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -166,7 +166,7 @@ class Credentials(credentials.Credentials, credentials.Signing): client = storage.Client(credentials=target_credentials) buckets = client.list_buckets(project='your_project') for bucket in buckets: - print bucket.name + print(bucket.name) """ def __init__(self, source_credentials, target_principal, From e096ca4aeb04f6fc69f062d62d5567880cc823b1 Mon Sep 17 00:00:00 2001 From: salmaan rashid Date: Wed, 7 Aug 2019 12:17:05 -0700 Subject: [PATCH 7/7] update .rst --- docs/user-guide.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/user-guide.rst b/docs/user-guide.rst index 9eda99f5a..c9b3238a7 100644 --- a/docs/user-guide.rst +++ b/docs/user-guide.rst @@ -227,7 +227,7 @@ the "Service Account Token Creator" IAM role. :: client = storage.Client(credentials=target_credentials) buckets = client.list_buckets(project='your_project') for bucket in buckets: - print bucket.name + print(bucket.name) In the example above `source_credentials` does not have direct access to list buckets @@ -297,14 +297,14 @@ A sample end-to-end flow using an ID Token against a Cloud Run endpoint maybe :: # make authenticated request and print the response, status_code resp = authed_session.get(url) - print resp.status_code - print resp.text + print(resp.status_code) + print(resp.text) # to verify an ID Token request = google.auth.transport.requests.Request() token = creds.token - print token - print id_token.verify_token(token,request) + print(token) + print(id_token.verify_token(token,request)) .. _Cloud Functions: https://cloud.google.com/functions/ .. _Cloud Run: https://cloud.google.com/run/