From 8f1e9cfd56dbaae0dff64499e1d0cf55abc5b97e Mon Sep 17 00:00:00 2001 From: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Date: Fri, 29 Oct 2021 11:59:01 -0700 Subject: [PATCH 1/5] fix: add fetch_id_token_credentials (#866) --- google/oauth2/id_token.py | 75 ++++++++++++++++++++++++----- tests/oauth2/test_id_token.py | 88 +++++++++++++++++++++-------------- 2 files changed, 116 insertions(+), 47 deletions(-) diff --git a/google/oauth2/id_token.py b/google/oauth2/id_token.py index 20d3ac1af..74899ae55 100644 --- a/google/oauth2/id_token.py +++ b/google/oauth2/id_token.py @@ -64,6 +64,7 @@ from google.auth import environment_vars from google.auth import exceptions from google.auth import jwt +import google.auth.transport.requests # The URL that provides public certificates for verifying ID tokens issued @@ -201,8 +202,8 @@ def verify_firebase_token(id_token, request, audience=None, clock_skew_in_second ) -def fetch_id_token(request, audience): - """Fetch the ID Token from the current environment. +def fetch_id_token_credentials(audience, request=None): + """Create the ID Token credentials from the current environment. This function acquires ID token from the environment in the following order. See https://google.aip.dev/auth/4110. @@ -224,15 +225,22 @@ def fetch_id_token(request, audience): request = google.auth.transport.requests.Request() target_audience = "https://pubsub.googleapis.com" - id_token = google.oauth2.id_token.fetch_id_token(request, target_audience) + # Create ID token credentials. + credentials = google.oauth2.id_token.fetch_id_token_credentials(target_audience, request=request) + + # Refresh the credential to obtain an ID token. + credentials.refresh(request) + + id_token = credentials.token + id_token_expiry = credentials.expiry Args: - request (google.auth.transport.Request): A callable used to make - HTTP requests. audience (str): The audience that this ID token is intended for. + request (Optional[google.auth.transport.Request]): A callable used to make + HTTP requests. A request object will be created if not provided. Returns: - str: The ID token. + google.auth.credentials.Credentials: The ID token credentials. Raises: ~google.auth.exceptions.DefaultCredentialsError: @@ -257,11 +265,9 @@ def fetch_id_token(request, audience): info = json.load(f) if info.get("type") == "service_account": - credentials = service_account.IDTokenCredentials.from_service_account_info( + return service_account.IDTokenCredentials.from_service_account_info( info, target_audience=audience ) - credentials.refresh(request) - return credentials.token except ValueError as caught_exc: new_exc = exceptions.DefaultCredentialsError( "GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials.", @@ -275,15 +281,60 @@ def fetch_id_token(request, audience): from google.auth import compute_engine from google.auth.compute_engine import _metadata + # Create a request object if not provided. + if not request: + request = google.auth.transport.requests.Request() + if _metadata.ping(request): - credentials = compute_engine.IDTokenCredentials( + return compute_engine.IDTokenCredentials( request, audience, use_metadata_identity_endpoint=True ) - credentials.refresh(request) - return credentials.token except (ImportError, exceptions.TransportError): pass raise exceptions.DefaultCredentialsError( "Neither metadata server or valid service account credentials are found." ) + + +def fetch_id_token(request, audience): + """Fetch the ID Token from the current environment. + + This function acquires ID token from the environment in the following order. + See https://google.aip.dev/auth/4110. + + 1. If the environment variable ``GOOGLE_APPLICATION_CREDENTIALS`` is set + to the path of a valid service account JSON file, then ID token is + acquired using this service account credentials. + 2. If the application is running in Compute Engine, App Engine or Cloud Run, + then the ID token are obtained from the metadata server. + 3. If metadata server doesn't exist and no valid service account credentials + are found, :class:`~google.auth.exceptions.DefaultCredentialsError` will + be raised. + + Example:: + + import google.oauth2.id_token + import google.auth.transport.requests + + request = google.auth.transport.requests.Request() + target_audience = "https://pubsub.googleapis.com" + + id_token = google.oauth2.id_token.fetch_id_token(request, target_audience) + + Args: + request (google.auth.transport.Request): A callable used to make + HTTP requests. + audience (str): The audience that this ID token is intended for. + + Returns: + str: The ID token. + + Raises: + ~google.auth.exceptions.DefaultCredentialsError: + If metadata server doesn't exist and no valid service account + credentials are found. + """ + id_token_credentials = fetch_id_token_credentials(audience, request=request) + id_token_credentials.refresh(request) + return id_token_credentials.token diff --git a/tests/oauth2/test_id_token.py b/tests/oauth2/test_id_token.py index a612c58fe..ccfaaaf8c 100644 --- a/tests/oauth2/test_id_token.py +++ b/tests/oauth2/test_id_token.py @@ -21,13 +21,13 @@ from google.auth import environment_vars from google.auth import exceptions from google.auth import transport -import google.auth.compute_engine._metadata from google.oauth2 import id_token from google.oauth2 import service_account SERVICE_ACCOUNT_FILE = os.path.join( os.path.dirname(__file__), "../data/service_account.json" ) +ID_TOKEN_AUDIENCE = "https://pubsub.googleapis.com" def make_request(status, data=None): @@ -201,37 +201,45 @@ def test_verify_firebase_token_clock_skew(verify_token): ) -def test_fetch_id_token_from_metadata_server(monkeypatch): +def test_fetch_id_token_credentials_optional_request(monkeypatch): monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False) - def mock_init(self, request, audience, use_metadata_identity_endpoint): - assert use_metadata_identity_endpoint - self.token = "id_token" - + # Test a request object is created if not provided with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True): - with mock.patch.multiple( - google.auth.compute_engine.IDTokenCredentials, - __init__=mock_init, - refresh=mock.Mock(), + with mock.patch( + "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None ): - request = mock.Mock() - token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com") - assert token == "id_token" + with mock.patch( + "google.auth.transport.requests.Request.__init__", return_value=None + ) as mock_request: + id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) + mock_request.assert_called() -def test_fetch_id_token_from_explicit_cred_json_file(monkeypatch): - monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE) +def test_fetch_id_token_credentials_from_metadata_server(monkeypatch): + monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False) + + mock_req = mock.Mock() + + with mock.patch("google.auth.compute_engine._metadata.ping", return_value=True): + with mock.patch( + "google.auth.compute_engine.IDTokenCredentials.__init__", return_value=None + ) as mock_init: + id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE, request=mock_req) + mock_init.assert_called_once_with( + mock_req, ID_TOKEN_AUDIENCE, use_metadata_identity_endpoint=True + ) - def mock_refresh(self, request): - self.token = "id_token" - with mock.patch.object(service_account.IDTokenCredentials, "refresh", mock_refresh): - request = mock.Mock() - token = id_token.fetch_id_token(request, "https://pubsub.googleapis.com") - assert token == "id_token" +def test_fetch_id_token_credentials_from_explicit_cred_json_file(monkeypatch): + monkeypatch.setenv(environment_vars.CREDENTIALS, SERVICE_ACCOUNT_FILE) + + cred = id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) + assert isinstance(cred, service_account.IDTokenCredentials) + assert cred._target_audience == ID_TOKEN_AUDIENCE -def test_fetch_id_token_no_cred_exists(monkeypatch): +def test_fetch_id_token_credentials_no_cred_exists(monkeypatch): monkeypatch.delenv(environment_vars.CREDENTIALS, raising=False) with mock.patch( @@ -239,22 +247,20 @@ def test_fetch_id_token_no_cred_exists(monkeypatch): side_effect=exceptions.TransportError(), ): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: - request = mock.Mock() - id_token.fetch_id_token(request, "https://pubsub.googleapis.com") + id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"Neither metadata server or valid service account credentials are found." ) with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: - request = mock.Mock() - id_token.fetch_id_token(request, "https://pubsub.googleapis.com") + id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"Neither metadata server or valid service account credentials are found." ) -def test_fetch_id_token_invalid_cred_file_type(monkeypatch): +def test_fetch_id_token_credentials_invalid_cred_file_type(monkeypatch): user_credentials_file = os.path.join( os.path.dirname(__file__), "../data/authorized_user.json" ) @@ -262,32 +268,44 @@ def test_fetch_id_token_invalid_cred_file_type(monkeypatch): with mock.patch("google.auth.compute_engine._metadata.ping", return_value=False): with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: - request = mock.Mock() - id_token.fetch_id_token(request, "https://pubsub.googleapis.com") + id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"Neither metadata server or valid service account credentials are found." ) -def test_fetch_id_token_invalid_json(monkeypatch): +def test_fetch_id_token_credentials_invalid_json(monkeypatch): not_json_file = os.path.join(os.path.dirname(__file__), "../data/public_cert.pem") monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: - request = mock.Mock() - id_token.fetch_id_token(request, "https://pubsub.googleapis.com") + id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"GOOGLE_APPLICATION_CREDENTIALS is not valid service account credentials." ) -def test_fetch_id_token_invalid_cred_path(monkeypatch): +def test_fetch_id_token_credentials_invalid_cred_path(monkeypatch): not_json_file = os.path.join(os.path.dirname(__file__), "../data/not_exists.json") monkeypatch.setenv(environment_vars.CREDENTIALS, not_json_file) with pytest.raises(exceptions.DefaultCredentialsError) as excinfo: - request = mock.Mock() - id_token.fetch_id_token(request, "https://pubsub.googleapis.com") + id_token.fetch_id_token_credentials(ID_TOKEN_AUDIENCE) assert excinfo.match( r"GOOGLE_APPLICATION_CREDENTIALS path is either not found or invalid." ) + + +def test_fetch_id_token(monkeypatch): + mock_cred = mock.MagicMock() + mock_cred.token = "token" + + mock_req = mock.Mock() + + with mock.patch( + "google.oauth2.id_token.fetch_id_token_credentials", return_value=mock_cred + ) as mock_fetch: + token = id_token.fetch_id_token(mock_req, ID_TOKEN_AUDIENCE) + mock_fetch.assert_called_once_with(ID_TOKEN_AUDIENCE, request=mock_req) + mock_cred.refresh.assert_called_once_with(mock_req) + assert token == "token" From 194c64acbcbed95ffcffe00ec09ca10da1081dd7 Mon Sep 17 00:00:00 2001 From: Bu Sun Kim <8822365+busunkim96@users.noreply.github.com> Date: Mon, 1 Nov 2021 10:51:09 -0600 Subject: [PATCH 2/5] chore: update authorized_user.json (#906) --- system_tests/secrets.tar.enc | Bin 10323 -> 10323 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index 3f42f879f93d60a1afd83a5af45a11fb4833f5e5..5f20b1e4ccdcbe19f2cc6aff26157fee83079100 100644 GIT binary patch literal 10323 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 literal 10323 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 From bd0ccc5fe77d55f7a19f5278d6b60587c393ee3c Mon Sep 17 00:00:00 2001 From: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Date: Mon, 1 Nov 2021 10:22:26 -0700 Subject: [PATCH 3/5] fix: use 'int.to_bytes' and 'int.from_bytes' for py3 (#904) --- google/auth/_helpers.py | 10 ++++++++++ google/auth/crypt/es256.py | 18 +++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py index b239fcd4f..1b08ab87f 100644 --- a/google/auth/_helpers.py +++ b/google/auth/_helpers.py @@ -17,6 +17,7 @@ import base64 import calendar import datetime +import sys import six from six.moves import urllib @@ -233,3 +234,12 @@ def unpadded_urlsafe_b64encode(value): Union[str|bytes]: The encoded value """ return base64.urlsafe_b64encode(value).rstrip(b"=") + + +def is_python_3(): + """Check if the Python interpreter is Python 2 or 3. + + Returns: + bool: True if the Python interpreter is Python 3 and False otherwise. + """ + return sys.version_info > (3, 0) diff --git a/google/auth/crypt/es256.py b/google/auth/crypt/es256.py index c6d617606..42823a7a5 100644 --- a/google/auth/crypt/es256.py +++ b/google/auth/crypt/es256.py @@ -53,8 +53,16 @@ def verify(self, message, signature): sig_bytes = _helpers.to_bytes(signature) if len(sig_bytes) != 64: return False - r = utils.int_from_bytes(sig_bytes[:32], byteorder="big") - s = utils.int_from_bytes(sig_bytes[32:], byteorder="big") + r = ( + int.from_bytes(sig_bytes[:32], byteorder="big") + if _helpers.is_python_3() + else utils.int_from_bytes(sig_bytes[:32], byteorder="big") + ) + s = ( + int.from_bytes(sig_bytes[32:], byteorder="big") + if _helpers.is_python_3() + else utils.int_from_bytes(sig_bytes[32:], byteorder="big") + ) asn1_sig = encode_dss_signature(r, s) message = _helpers.to_bytes(message) @@ -121,7 +129,11 @@ def sign(self, message): # Convert ASN1 encoded signature to (r||s) raw signature. (r, s) = decode_dss_signature(asn1_signature) - return utils.int_to_bytes(r, 32) + utils.int_to_bytes(s, 32) + return ( + (r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big")) + if _helpers.is_python_3() + else (utils.int_to_bytes(r, 32) + utils.int_to_bytes(s, 32)) + ) @classmethod def from_string(cls, key, key_id=None): From ef3128474431b07d1d519209ea61622bc245ce91 Mon Sep 17 00:00:00 2001 From: arithmetic1728 <58957152+arithmetic1728@users.noreply.github.com> Date: Mon, 1 Nov 2021 13:10:17 -0700 Subject: [PATCH 4/5] fix: fix error in sign_bytes (#905) * fix: fix error in sign_bytes * fix test --- .coveragerc | 1 + google/auth/impersonated_credentials.py | 5 +++++ tests/test_impersonated_credentials.py | 13 +++++++++++++ 3 files changed, 19 insertions(+) diff --git a/.coveragerc b/.coveragerc index 494c03f07..9ba3d3fe6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,6 +5,7 @@ branch = True omit = */samples/* */conftest.py + */google-cloud-sdk/lib/* exclude_lines = # Re-enable the standard pragma pragma: NO COVER diff --git a/google/auth/impersonated_credentials.py b/google/auth/impersonated_credentials.py index b8a6c49a1..80d6fdfdc 100644 --- a/google/auth/impersonated_credentials.py +++ b/google/auth/impersonated_credentials.py @@ -290,6 +290,11 @@ def sign_bytes(self, message): url=iam_sign_endpoint, headers=headers, json=body ) + if response.status_code != http_client.OK: + raise exceptions.TransportError( + "Error calling sign_bytes: {}".format(response.json()) + ) + return base64.b64decode(response.json()["signedBlob"]) @property diff --git a/tests/test_impersonated_credentials.py b/tests/test_impersonated_credentials.py index bceaebaa5..bc404e36b 100644 --- a/tests/test_impersonated_credentials.py +++ b/tests/test_impersonated_credentials.py @@ -345,6 +345,19 @@ def test_sign_bytes(self, mock_donor_credentials, mock_authorizedsession_sign): signature = credentials.sign_bytes(b"signed bytes") assert signature == b"signature" + def test_sign_bytes_failure(self): + credentials = self.make_credentials(lifetime=None) + + with mock.patch( + "google.auth.transport.requests.AuthorizedSession.request", autospec=True + ) as auth_session: + data = {"error": {"code": 403, "message": "unauthorized"}} + auth_session.return_value = MockResponse(data, http_client.FORBIDDEN) + + with pytest.raises(exceptions.TransportError) as excinfo: + credentials.sign_bytes(b"foo") + assert excinfo.match("'code': 403") + def test_with_quota_project(self): credentials = self.make_credentials() From e6278a815895e050e57fc516f086b4bc89a0864a Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 1 Nov 2021 20:30:12 +0000 Subject: [PATCH 5/5] chore: release 2.3.3 (#903) :robot: I have created a release \*beep\* \*boop\* --- ### [2.3.3](https://www.github.com/googleapis/google-auth-library-python/compare/v2.3.2...v2.3.3) (2021-11-01) ### Bug Fixes * add fetch_id_token_credentials ([#866](https://www.github.com/googleapis/google-auth-library-python/issues/866)) ([8f1e9cf](https://www.github.com/googleapis/google-auth-library-python/commit/8f1e9cfd56dbaae0dff64499e1d0cf55abc5b97e)) * fix error in sign_bytes ([#905](https://www.github.com/googleapis/google-auth-library-python/issues/905)) ([ef31284](https://www.github.com/googleapis/google-auth-library-python/commit/ef3128474431b07d1d519209ea61622bc245ce91)) * use 'int.to_bytes' and 'int.from_bytes' for py3 ([#904](https://www.github.com/googleapis/google-auth-library-python/issues/904)) ([bd0ccc5](https://www.github.com/googleapis/google-auth-library-python/commit/bd0ccc5fe77d55f7a19f5278d6b60587c393ee3c)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 9 +++++++++ google/auth/version.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcda51152..73440f7e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ [1]: https://pypi.org/project/google-auth/#history +### [2.3.3](https://www.github.com/googleapis/google-auth-library-python/compare/v2.3.2...v2.3.3) (2021-11-01) + + +### Bug Fixes + +* add fetch_id_token_credentials ([#866](https://www.github.com/googleapis/google-auth-library-python/issues/866)) ([8f1e9cf](https://www.github.com/googleapis/google-auth-library-python/commit/8f1e9cfd56dbaae0dff64499e1d0cf55abc5b97e)) +* fix error in sign_bytes ([#905](https://www.github.com/googleapis/google-auth-library-python/issues/905)) ([ef31284](https://www.github.com/googleapis/google-auth-library-python/commit/ef3128474431b07d1d519209ea61622bc245ce91)) +* use 'int.to_bytes' and 'int.from_bytes' for py3 ([#904](https://www.github.com/googleapis/google-auth-library-python/issues/904)) ([bd0ccc5](https://www.github.com/googleapis/google-auth-library-python/commit/bd0ccc5fe77d55f7a19f5278d6b60587c393ee3c)) + ### [2.3.2](https://www.github.com/googleapis/google-auth-library-python/compare/v2.3.1...v2.3.2) (2021-10-26) diff --git a/google/auth/version.py b/google/auth/version.py index cd24dc54a..ad9a0c7a4 100644 --- a/google/auth/version.py +++ b/google/auth/version.py @@ -12,4 +12,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -__version__ = "2.3.2" +__version__ = "2.3.3"