diff --git a/.gitignore b/.gitignore index 88a8b8bc4..598752fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,11 @@ tests/data/user-key.json # PyCharm configuration: .idea +venv/ # Generated files pylintrc pylintrc.test pytype_output/ + +.python-version diff --git a/google/auth/compute_engine/credentials.py b/google/auth/compute_engine/credentials.py index d9c6e26d6..642df594d 100644 --- a/google/auth/compute_engine/credentials.py +++ b/google/auth/compute_engine/credentials.py @@ -131,7 +131,8 @@ class IDTokenCredentials(credentials.Credentials, credentials.Signing): def __init__(self, request, target_audience, token_uri=_DEFAULT_TOKEN_URI, additional_claims=None, - service_account_email=None): + service_account_email=None, + signer=None): """ Args: request (google.auth.transport.Request): The object used to make @@ -145,6 +146,9 @@ def __init__(self, request, target_audience, service_account_email (str): Optional explicit service account to use to sign JWT tokens. By default, this is the default GCE service account. + signer (google.auth.crypt.Signer): The signer used to sign JWTs. + In case the signer is specified, the request argument will be + ignored. """ super(IDTokenCredentials, self).__init__() @@ -153,10 +157,12 @@ def __init__(self, request, target_audience, service_account_email = sa_info['email'] self._service_account_email = service_account_email - self._signer = iam.Signer( - request=request, - credentials=Credentials(), - service_account_email=service_account_email) + if signer is None: + signer = iam.Signer( + request=request, + credentials=Credentials(), + service_account_email=service_account_email) + self._signer = signer self._token_uri = token_uri self._target_audience = target_audience @@ -176,12 +182,15 @@ def with_target_audience(self, target_audience): google.auth.service_account.IDTokenCredentials: A new credentials instance. """ + # since the signer is already instantiated, + # the request is not needed return self.__class__( - self._signer, + None, service_account_email=self._service_account_email, token_uri=self._token_uri, target_audience=target_audience, - additional_claims=self._additional_claims.copy()) + additional_claims=self._additional_claims.copy(), + signer=self.signer) def _make_authorization_grant_assertion(self): """Create the OAuth 2.0 assertion. diff --git a/tests/compute_engine/test_credentials.py b/tests/compute_engine/test_credentials.py index ee415db98..a948a26da 100644 --- a/tests/compute_engine/test_credentials.py +++ b/tests/compute_engine/test_credentials.py @@ -11,17 +11,19 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import base64 import datetime import mock import pytest +import responses from google.auth import _helpers from google.auth import exceptions from google.auth import jwt from google.auth import transport from google.auth.compute_engine import credentials +from google.auth.transport import requests class TestCredentials(object): @@ -257,6 +259,89 @@ def test_with_target_audience(self, sign, get, utcnow): 'iss': 'service-account@example.com', 'target_audience': 'https://actually.not'} + # Check that the signer have been initialized with a Request object + assert isinstance(self.credentials._signer._request, transport.Request) + + @responses.activate + def test_with_target_audience_integration(self): + """ Test that it is possible to refresh credentials + generated from `with_target_audience`. + + Instead of mocking the methods, the HTTP responses + have been mocked. + """ + + # mock information about credentials + responses.add( + responses.GET, + "http://metadata.google.internal/computeMetadata/v1/instance/" + "service-accounts/default/?recursive=true", + status=200, + content_type="application/json", + json={ + "scopes": "email", + "email": "service-account@example.com", + "aliases": ["default"] + } + ) + + # mock token for credentials + responses.add( + responses.GET, + "http://metadata.google.internal/computeMetadata/v1/instance/" + "service-accounts/service-account@example.com/token", + status=200, + content_type="application/json", + json={ + "access_token": "some-token", + "expires_in": 3210, + "token_type": "Bearer" + } + ) + + # mock sign blob endpoint + signature = base64.b64encode(b"some-signature").decode('utf-8') + responses.add( + responses.POST, + "https://iam.googleapis.com/v1/projects/-/serviceAccounts/" + "service-account@example.com:signBlob?alt=json", + status=200, + content_type="application/json", + json={ + "keyId": "some-key-id", + "signature": signature + } + ) + + id_token = "{}.{}.{}".format( + base64.b64encode(b'{"some":"some"}').decode('utf-8'), + base64.b64encode(b'{"exp": 3210}').decode('utf-8'), + base64.b64encode(b"token").decode('utf-8')) + + # mock id token endpoint + responses.add( + responses.POST, + "https://www.googleapis.com/oauth2/v4/token", + status=200, + content_type="application/json", + json={ + "id_token": id_token, + "expiry": 3210 + } + ) + + self.credentials = credentials.IDTokenCredentials( + request=requests.Request(), + service_account_email="service-account@example.com", + target_audience="https://audience.com") + + self.credentials = ( + self.credentials.with_target_audience("https://actually.not")) + + self.credentials.refresh(requests.Request()) + + assert self.credentials.token is not None + @mock.patch( 'google.auth._helpers.utcnow', return_value=datetime.datetime.utcfromtimestamp(0)) diff --git a/tox.ini b/tox.ini index 59fd6aba1..4a9dc887c 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,9 @@ deps = requests-oauthlib urllib3 cryptography + responses grpcio; platform_python_implementation != 'PyPy' + commands = pytest --cov=google.auth --cov=google.oauth2 --cov=tests {posargs:tests}