aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/google-auth
diff options
context:
space:
mode:
authorrobot-piglet <robot-piglet@yandex-team.com>2024-06-21 09:28:26 +0300
committerrobot-piglet <robot-piglet@yandex-team.com>2024-06-21 09:36:40 +0300
commit0cb3f820fac6a243bcb7e4c4388700898660bfd0 (patch)
tree056f1b8bc5f72039fa422aac0af13bab0e966aa7 /contrib/python/google-auth
parent08049311fe5c42a97e8bb47a73fb6cd143c0bdb1 (diff)
downloadydb-0cb3f820fac6a243bcb7e4c4388700898660bfd0.tar.gz
Intermediate changes
Diffstat (limited to 'contrib/python/google-auth')
-rw-r--r--contrib/python/google-auth/py3/.dist-info/METADATA2
-rw-r--r--contrib/python/google-auth/py3/google/auth/external_account.py8
-rw-r--r--contrib/python/google-auth/py3/google/auth/iam.py21
-rw-r--r--contrib/python/google-auth/py3/google/auth/identity_pool.py2
-rw-r--r--contrib/python/google-auth/py3/google/auth/impersonated_credentials.py29
-rw-r--r--contrib/python/google-auth/py3/google/auth/pluggable.py2
-rw-r--r--contrib/python/google-auth/py3/google/auth/py.typed2
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py20
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/requests.py13
-rw-r--r--contrib/python/google-auth/py3/google/auth/version.py2
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/_client.py11
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/challenges.py78
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/py.typed2
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/reauth.py1
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/service_account.py7
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/webauthn_handler.py82
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/webauthn_handler_factory.py16
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/webauthn_types.py156
-rw-r--r--contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py4
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test__client.py13
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_challenges.py153
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_service_account.py29
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py148
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler_factory.py29
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py237
-rw-r--r--contrib/python/google-auth/py3/tests/test_aws.py33
-rw-r--r--contrib/python/google-auth/py3/tests/test_identity_pool.py33
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py2
-rw-r--r--contrib/python/google-auth/py3/tests/transport/test_requests.py35
-rw-r--r--contrib/python/google-auth/py3/ya.make7
30 files changed, 1121 insertions, 56 deletions
diff --git a/contrib/python/google-auth/py3/.dist-info/METADATA b/contrib/python/google-auth/py3/.dist-info/METADATA
index e0785656a4..d8ad54493a 100644
--- a/contrib/python/google-auth/py3/.dist-info/METADATA
+++ b/contrib/python/google-auth/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: google-auth
-Version: 2.29.0
+Version: 2.30.0
Summary: Google Authentication Library
Home-page: https://github.com/googleapis/google-auth-library-python
Author: Google Cloud Platform
diff --git a/contrib/python/google-auth/py3/google/auth/external_account.py b/contrib/python/google-auth/py3/google/auth/external_account.py
index c14001bc2b..3943de2a34 100644
--- a/contrib/python/google-auth/py3/google/auth/external_account.py
+++ b/contrib/python/google-auth/py3/google/auth/external_account.py
@@ -52,7 +52,7 @@ _STS_REQUESTED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token"
# Cloud resource manager URL used to retrieve project information.
_CLOUD_RESOURCE_MANAGER = "https://cloudresourcemanager.googleapis.com/v1/projects/"
# Default Google sts token url.
-_DEFAULT_TOKEN_URL = "https://sts.googleapis.com/v1/token"
+_DEFAULT_TOKEN_URL = "https://sts.{universe_domain}/v1/token"
@dataclass
@@ -147,7 +147,12 @@ class Credentials(
super(Credentials, self).__init__()
self._audience = audience
self._subject_token_type = subject_token_type
+ self._universe_domain = universe_domain
self._token_url = token_url
+ if self._token_url == _DEFAULT_TOKEN_URL:
+ self._token_url = self._token_url.replace(
+ "{universe_domain}", self._universe_domain
+ )
self._token_info_url = token_info_url
self._credential_source = credential_source
self._service_account_impersonation_url = service_account_impersonation_url
@@ -160,7 +165,6 @@ class Credentials(
self._scopes = scopes
self._default_scopes = default_scopes
self._workforce_pool_user_project = workforce_pool_user_project
- self._universe_domain = universe_domain or credentials.DEFAULT_UNIVERSE_DOMAIN
self._trust_boundary = {
"locations": [],
"encoded_locations": "0x0",
diff --git a/contrib/python/google-auth/py3/google/auth/iam.py b/contrib/python/google-auth/py3/google/auth/iam.py
index e9df844178..bba1624c16 100644
--- a/contrib/python/google-auth/py3/google/auth/iam.py
+++ b/contrib/python/google-auth/py3/google/auth/iam.py
@@ -27,8 +27,23 @@ from google.auth import _helpers
from google.auth import crypt
from google.auth import exceptions
-_IAM_API_ROOT_URI = "https://iamcredentials.googleapis.com/v1"
-_SIGN_BLOB_URI = _IAM_API_ROOT_URI + "/projects/-/serviceAccounts/{}:signBlob?alt=json"
+
+_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]
+
+_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"
+)
class Signer(crypt.Signer):
@@ -67,7 +82,7 @@ class Signer(crypt.Signer):
message = _helpers.to_bytes(message)
method = "POST"
- url = _SIGN_BLOB_URI.format(self._service_account_email)
+ url = _IAM_SIGN_ENDPOINT.format(self._service_account_email)
headers = {"Content-Type": "application/json"}
body = json.dumps(
{"payload": base64.b64encode(message).decode("utf-8")}
diff --git a/contrib/python/google-auth/py3/google/auth/identity_pool.py b/contrib/python/google-auth/py3/google/auth/identity_pool.py
index a9ec577334..1c97885a4a 100644
--- a/contrib/python/google-auth/py3/google/auth/identity_pool.py
+++ b/contrib/python/google-auth/py3/google/auth/identity_pool.py
@@ -39,7 +39,7 @@ try:
from collections.abc import Mapping
# Python 2.7 compatibility
except ImportError: # pragma: NO COVER
- from collections import Mapping
+ from collections import Mapping # type: ignore
import abc
import json
import os
diff --git a/contrib/python/google-auth/py3/google/auth/impersonated_credentials.py b/contrib/python/google-auth/py3/google/auth/impersonated_credentials.py
index d32e6eb69a..3c6f8712a9 100644
--- a/contrib/python/google-auth/py3/google/auth/impersonated_credentials.py
+++ b/contrib/python/google-auth/py3/google/auth/impersonated_credentials.py
@@ -34,32 +34,15 @@ import json
from google.auth import _helpers
from google.auth import credentials
from google.auth import exceptions
+from google.auth import iam
from google.auth import jwt
from google.auth import metrics
-_IAM_SCOPE = ["https://www.googleapis.com/auth/iam"]
-
-_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"
_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, iam_endpoint_override=None
@@ -83,7 +66,7 @@ def _make_iam_token_request(
`iamcredentials.googleapis.com` is not enabled or the
`Service Account Token Creator` is not assigned
"""
- iam_endpoint = iam_endpoint_override or _IAM_ENDPOINT.format(principal)
+ iam_endpoint = iam_endpoint_override or iam._IAM_ENDPOINT.format(principal)
body = json.dumps(body).encode("utf-8")
@@ -225,7 +208,9 @@ class Credentials(
# added to refresh correctly. User credentials cannot have
# their original scopes modified.
if isinstance(self._source_credentials, credentials.Scoped):
- self._source_credentials = self._source_credentials.with_scopes(_IAM_SCOPE)
+ self._source_credentials = self._source_credentials.with_scopes(
+ iam._IAM_SCOPE
+ )
# If the source credential is service account and self signed jwt
# is needed, we need to create a jwt credential inside it
if (
@@ -290,7 +275,7 @@ class Credentials(
def sign_bytes(self, message):
from google.auth.transport.requests import AuthorizedSession
- iam_sign_endpoint = _IAM_SIGN_ENDPOINT.format(self._target_principal)
+ iam_sign_endpoint = iam._IAM_SIGN_ENDPOINT.format(self._target_principal)
body = {
"payload": base64.b64encode(message).decode("utf-8"),
@@ -425,7 +410,7 @@ class IDTokenCredentials(credentials.CredentialsWithQuotaProject):
def refresh(self, request):
from google.auth.transport.requests import AuthorizedSession
- iam_sign_endpoint = _IAM_IDTOKEN_ENDPOINT.format(
+ iam_sign_endpoint = iam._IAM_IDTOKEN_ENDPOINT.format(
self._target_credentials.signer_email
)
diff --git a/contrib/python/google-auth/py3/google/auth/pluggable.py b/contrib/python/google-auth/py3/google/auth/pluggable.py
index 53b4eac5b4..d725188f87 100644
--- a/contrib/python/google-auth/py3/google/auth/pluggable.py
+++ b/contrib/python/google-auth/py3/google/auth/pluggable.py
@@ -34,7 +34,7 @@ try:
from collections.abc import Mapping
# Python 2.7 compatibility
except ImportError: # pragma: NO COVER
- from collections import Mapping
+ from collections import Mapping # type: ignore
import json
import os
import subprocess
diff --git a/contrib/python/google-auth/py3/google/auth/py.typed b/contrib/python/google-auth/py3/google/auth/py.typed
new file mode 100644
index 0000000000..e1ab889b3a
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/py.typed
@@ -0,0 +1,2 @@
+# Marker file for PEP 561.
+# The google-auth package uses inline types.
diff --git a/contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py b/contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py
index 57a563d03b..9279158d45 100644
--- a/contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py
+++ b/contrib/python/google-auth/py3/google/auth/transport/_custom_tls_signer.py
@@ -46,10 +46,17 @@ SIGN_CALLBACK_CTYPE = ctypes.CFUNCTYPE(
# Cast SSL_CTX* to void*
-def _cast_ssl_ctx_to_void_p(ssl_ctx):
+def _cast_ssl_ctx_to_void_p_pyopenssl(ssl_ctx):
return ctypes.cast(int(cffi.FFI().cast("intptr_t", ssl_ctx)), ctypes.c_void_p)
+# Cast SSL_CTX* to void*
+def _cast_ssl_ctx_to_void_p_stdlib(context):
+ return ctypes.c_void_p.from_address(
+ id(context) + ctypes.sizeof(ctypes.c_void_p) * 2
+ )
+
+
# Load offload library and set up the function types.
def load_offload_lib(offload_lib_path):
_LOGGER.debug("loading offload library from %s", offload_lib_path)
@@ -249,10 +256,15 @@ class CustomTlsSigner(object):
self._signer_lib, self._enterprise_cert_file_path
)
- def attach_to_ssl_context(self, ctx):
+ def should_use_provider(self):
if self._provider_lib:
+ return True
+ return False
+
+ def attach_to_ssl_context(self, ctx):
+ if self.should_use_provider():
if not self._provider_lib.ECP_attach_to_ctx(
- _cast_ssl_ctx_to_void_p(ctx._ctx._context),
+ _cast_ssl_ctx_to_void_p_stdlib(ctx),
self._enterprise_cert_file_path.encode("ascii"),
):
raise exceptions.MutualTLSChannelError(
@@ -262,7 +274,7 @@ class CustomTlsSigner(object):
if not self._offload_lib.ConfigureSslContext(
self._sign_callback,
ctypes.c_char_p(self._cert),
- _cast_ssl_ctx_to_void_p(ctx._ctx._context),
+ _cast_ssl_ctx_to_void_p_pyopenssl(ctx._ctx._context),
):
raise exceptions.MutualTLSChannelError(
"failed to configure ECP Offload SSL context"
diff --git a/contrib/python/google-auth/py3/google/auth/transport/requests.py b/contrib/python/google-auth/py3/google/auth/transport/requests.py
index aa16113226..63a2b4596c 100644
--- a/contrib/python/google-auth/py3/google/auth/transport/requests.py
+++ b/contrib/python/google-auth/py3/google/auth/transport/requests.py
@@ -262,19 +262,16 @@ class _MutualTlsOffloadAdapter(requests.adapters.HTTPAdapter):
def __init__(self, enterprise_cert_file_path):
import certifi
- import urllib3.contrib.pyopenssl
-
from google.auth.transport import _custom_tls_signer
- # Call inject_into_urllib3 to activate certificate checking. See the
- # following links for more info:
- # (1) doc: https://github.com/urllib3/urllib3/blob/cb9ebf8aac5d75f64c8551820d760b72b619beff/src/urllib3/contrib/pyopenssl.py#L31-L32
- # (2) mTLS example: https://github.com/urllib3/urllib3/issues/474#issuecomment-253168415
- urllib3.contrib.pyopenssl.inject_into_urllib3()
-
self.signer = _custom_tls_signer.CustomTlsSigner(enterprise_cert_file_path)
self.signer.load_libraries()
+ if not self.signer.should_use_provider():
+ import urllib3.contrib.pyopenssl
+
+ urllib3.contrib.pyopenssl.inject_into_urllib3()
+
poolmanager = create_urllib3_context()
poolmanager.load_verify_locations(cafile=certifi.where())
self.signer.attach_to_ssl_context(poolmanager)
diff --git a/contrib/python/google-auth/py3/google/auth/version.py b/contrib/python/google-auth/py3/google/auth/version.py
index f0dd919dca..0800489978 100644
--- a/contrib/python/google-auth/py3/google/auth/version.py
+++ b/contrib/python/google-auth/py3/google/auth/version.py
@@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = "2.29.0"
+__version__ = "2.30.0"
diff --git a/contrib/python/google-auth/py3/google/oauth2/_client.py b/contrib/python/google-auth/py3/google/oauth2/_client.py
index d2af6c8aa8..bce797b88b 100644
--- a/contrib/python/google-auth/py3/google/oauth2/_client.py
+++ b/contrib/python/google-auth/py3/google/oauth2/_client.py
@@ -39,10 +39,6 @@ _URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
_JSON_CONTENT_TYPE = "application/json"
_JWT_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"
_REFRESH_GRANT_TYPE = "refresh_token"
-_IAM_IDTOKEN_ENDPOINT = (
- "https://iamcredentials.googleapis.com/v1/"
- + "projects/-/serviceAccounts/{}:generateIdToken"
-)
def _handle_error_response(response_data, retryable_error):
@@ -328,12 +324,15 @@ def jwt_grant(request, token_uri, assertion, can_retry=True):
return access_token, expiry, response_data
-def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_token):
+def call_iam_generate_id_token_endpoint(
+ request, iam_id_token_endpoint, signer_email, audience, access_token
+):
"""Call iam.generateIdToken endpoint to get ID token.
Args:
request (google.auth.transport.Request): A callable used to make
HTTP requests.
+ iam_id_token_endpoint (str): The IAM ID token endpoint to use.
signer_email (str): The signer email used to form the IAM
generateIdToken endpoint.
audience (str): The audience for the ID token.
@@ -346,7 +345,7 @@ def call_iam_generate_id_token_endpoint(request, signer_email, audience, access_
response_data = _token_endpoint_request(
request,
- _IAM_IDTOKEN_ENDPOINT.format(signer_email),
+ iam_id_token_endpoint.format(signer_email),
body,
access_token=access_token,
use_json=True,
diff --git a/contrib/python/google-auth/py3/google/oauth2/challenges.py b/contrib/python/google-auth/py3/google/oauth2/challenges.py
index c55796323b..6468498bcb 100644
--- a/contrib/python/google-auth/py3/google/oauth2/challenges.py
+++ b/contrib/python/google-auth/py3/google/oauth2/challenges.py
@@ -22,12 +22,19 @@ import sys
from google.auth import _helpers
from google.auth import exceptions
+from google.oauth2 import webauthn_handler_factory
+from google.oauth2.webauthn_types import (
+ AuthenticationExtensionsClientInputs,
+ GetRequest,
+ PublicKeyCredentialDescriptor,
+)
REAUTH_ORIGIN = "https://accounts.google.com"
SAML_CHALLENGE_MESSAGE = (
"Please run `gcloud auth login` to complete reauthentication with SAML."
)
+WEBAUTHN_TIMEOUT_MS = 120000 # Two minute timeout
def get_user_password(text):
@@ -110,6 +117,17 @@ class SecurityKeyChallenge(ReauthChallenge):
@_helpers.copy_docstring(ReauthChallenge)
def obtain_challenge_input(self, metadata):
+ # Check if there is an available Webauthn Handler, if not use pyu2f
+ try:
+ factory = webauthn_handler_factory.WebauthnHandlerFactory()
+ webauthn_handler = factory.get_handler()
+ if webauthn_handler is not None:
+ sys.stderr.write("Please insert and touch your security key\n")
+ return self._obtain_challenge_input_webauthn(metadata, webauthn_handler)
+ except Exception:
+ # Attempt pyu2f if exception in webauthn flow
+ pass
+
try:
import pyu2f.convenience.authenticator # type: ignore
import pyu2f.errors # type: ignore
@@ -173,6 +191,66 @@ class SecurityKeyChallenge(ReauthChallenge):
sys.stderr.write("No security key found.\n")
return None
+ def _obtain_challenge_input_webauthn(self, metadata, webauthn_handler):
+ sk = metadata.get("securityKey")
+ if sk is None:
+ raise exceptions.InvalidValue("securityKey is None")
+ challenges = sk.get("challenges")
+ application_id = sk.get("applicationId")
+ relying_party_id = sk.get("relyingPartyId")
+ if challenges is None or len(challenges) < 1:
+ raise exceptions.InvalidValue("challenges is None or empty")
+ if application_id is None:
+ raise exceptions.InvalidValue("application_id is None")
+ if relying_party_id is None:
+ raise exceptions.InvalidValue("relying_party_id is None")
+
+ allow_credentials = []
+ for challenge in challenges:
+ kh = challenge.get("keyHandle")
+ if kh is None:
+ raise exceptions.InvalidValue("keyHandle is None")
+ key_handle = self._unpadded_urlsafe_b64recode(kh)
+ allow_credentials.append(PublicKeyCredentialDescriptor(id=key_handle))
+
+ extension = AuthenticationExtensionsClientInputs(appid=application_id)
+
+ challenge = challenges[0].get("challenge")
+ if challenge is None:
+ raise exceptions.InvalidValue("challenge is None")
+
+ get_request = GetRequest(
+ origin=REAUTH_ORIGIN,
+ rpid=relying_party_id,
+ challenge=self._unpadded_urlsafe_b64recode(challenge),
+ timeout_ms=WEBAUTHN_TIMEOUT_MS,
+ allow_credentials=allow_credentials,
+ user_verification="required",
+ extensions=extension,
+ )
+
+ try:
+ get_response = webauthn_handler.get(get_request)
+ except Exception as e:
+ sys.stderr.write("Webauthn Error: {}.\n".format(e))
+ raise e
+
+ response = {
+ "clientData": get_response.response.client_data_json,
+ "authenticatorData": get_response.response.authenticator_data,
+ "signatureData": get_response.response.signature,
+ "applicationId": application_id,
+ "keyHandle": get_response.id,
+ "securityKeyReplyType": 2,
+ }
+ return {"securityKey": response}
+
+ def _unpadded_urlsafe_b64recode(self, s):
+ """Converts standard b64 encoded string to url safe b64 encoded string
+ with no padding."""
+ b = base64.urlsafe_b64decode(s)
+ return base64.urlsafe_b64encode(b).decode().rstrip("=")
+
class SamlChallenge(ReauthChallenge):
"""Challenge that asks the users to browse to their ID Providers.
diff --git a/contrib/python/google-auth/py3/google/oauth2/py.typed b/contrib/python/google-auth/py3/google/oauth2/py.typed
new file mode 100644
index 0000000000..aedf18e4b6
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/py.typed
@@ -0,0 +1,2 @@
+# Marker file for PEP 561.
+# The google-oauth2 package uses inline types.
diff --git a/contrib/python/google-auth/py3/google/oauth2/reauth.py b/contrib/python/google-auth/py3/google/oauth2/reauth.py
index 5870347739..1e39e0bc7f 100644
--- a/contrib/python/google-auth/py3/google/oauth2/reauth.py
+++ b/contrib/python/google-auth/py3/google/oauth2/reauth.py
@@ -274,6 +274,7 @@ def get_rapt_token(
# Get rapt token from reauth API.
rapt_token = _obtain_rapt(request, access_token, requested_scopes=scopes)
+ sys.stderr.write("Reauthentication successful.\n")
return rapt_token
diff --git a/contrib/python/google-auth/py3/google/oauth2/service_account.py b/contrib/python/google-auth/py3/google/oauth2/service_account.py
index 04fd7797ad..0e12868f14 100644
--- a/contrib/python/google-auth/py3/google/oauth2/service_account.py
+++ b/contrib/python/google-auth/py3/google/oauth2/service_account.py
@@ -77,6 +77,7 @@ from google.auth import _helpers
from google.auth import _service_account_info
from google.auth import credentials
from google.auth import exceptions
+from google.auth import iam
from google.auth import jwt
from google.auth import metrics
from google.oauth2 import _client
@@ -595,8 +596,11 @@ class IDTokenCredentials(
self._universe_domain = credentials.DEFAULT_UNIVERSE_DOMAIN
else:
self._universe_domain = universe_domain
+ self._iam_id_token_endpoint = iam._IAM_IDTOKEN_ENDPOINT.replace(
+ "googleapis.com", self._universe_domain
+ )
- if universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
+ if self._universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
self._use_iam_endpoint = True
if additional_claims is not None:
@@ -792,6 +796,7 @@ class IDTokenCredentials(
jwt_credentials.refresh(request)
self.token, self.expiry = _client.call_iam_generate_id_token_endpoint(
request,
+ self._iam_id_token_endpoint,
self.signer_email,
self._target_audience,
jwt_credentials.token.decode(),
diff --git a/contrib/python/google-auth/py3/google/oauth2/webauthn_handler.py b/contrib/python/google-auth/py3/google/oauth2/webauthn_handler.py
new file mode 100644
index 0000000000..e27c7e0990
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/webauthn_handler.py
@@ -0,0 +1,82 @@
+import abc
+import os
+import struct
+import subprocess
+
+from google.auth import exceptions
+from google.oauth2.webauthn_types import GetRequest, GetResponse
+
+
+class WebAuthnHandler(abc.ABC):
+ @abc.abstractmethod
+ def is_available(self) -> bool:
+ """Check whether this WebAuthn handler is available"""
+ raise NotImplementedError("is_available method must be implemented")
+
+ @abc.abstractmethod
+ def get(self, get_request: GetRequest) -> GetResponse:
+ """WebAuthn get (assertion)"""
+ raise NotImplementedError("get method must be implemented")
+
+
+class PluginHandler(WebAuthnHandler):
+ """Offloads WebAuthn get reqeust to a pluggable command-line tool.
+
+ Offloads WebAuthn get to a plugin which takes the form of a
+ command-line tool. The command-line tool is configurable via the
+ PluginHandler._ENV_VAR environment variable.
+
+ The WebAuthn plugin should implement the following interface:
+
+ Communication occurs over stdin/stdout, and messages are both sent and
+ received in the form:
+
+ [4 bytes - payload size (little-endian)][variable bytes - json payload]
+ """
+
+ _ENV_VAR = "GOOGLE_AUTH_WEBAUTHN_PLUGIN"
+
+ def is_available(self) -> bool:
+ try:
+ self._find_plugin()
+ except Exception:
+ return False
+ else:
+ return True
+
+ def get(self, get_request: GetRequest) -> GetResponse:
+ request_json = get_request.to_json()
+ cmd = self._find_plugin()
+ response_json = self._call_plugin(cmd, request_json)
+ return GetResponse.from_json(response_json)
+
+ def _call_plugin(self, cmd: str, input_json: str) -> str:
+ # Calculate length of input
+ input_length = len(input_json)
+ length_bytes_le = struct.pack("<I", input_length)
+ request = length_bytes_le + input_json.encode()
+
+ # Call plugin
+ process_result = subprocess.run(
+ [cmd], input=request, capture_output=True, check=True
+ )
+
+ # Check length of response
+ response_len_le = process_result.stdout[:4]
+ response_len = struct.unpack("<I", response_len_le)[0]
+ response = process_result.stdout[4:]
+ if response_len != len(response):
+ raise exceptions.MalformedError(
+ "Plugin response length {} does not match data {}".format(
+ response_len, len(response)
+ )
+ )
+ return response.decode()
+
+ def _find_plugin(self) -> str:
+ plugin_cmd = os.environ.get(PluginHandler._ENV_VAR)
+ if plugin_cmd is None:
+ raise exceptions.InvalidResource(
+ "{} env var is not set".format(PluginHandler._ENV_VAR)
+ )
+ return plugin_cmd
diff --git a/contrib/python/google-auth/py3/google/oauth2/webauthn_handler_factory.py b/contrib/python/google-auth/py3/google/oauth2/webauthn_handler_factory.py
new file mode 100644
index 0000000000..184329fed7
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/webauthn_handler_factory.py
@@ -0,0 +1,16 @@
+from typing import List, Optional
+
+from google.oauth2.webauthn_handler import PluginHandler, WebAuthnHandler
+
+
+class WebauthnHandlerFactory:
+ handlers: List[WebAuthnHandler]
+
+ def __init__(self):
+ self.handlers = [PluginHandler()]
+
+ def get_handler(self) -> Optional[WebAuthnHandler]:
+ for handler in self.handlers:
+ if handler.is_available():
+ return handler
+ return None
diff --git a/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py b/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py
new file mode 100644
index 0000000000..7784e83d0b
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py
@@ -0,0 +1,156 @@
+from dataclasses import dataclass
+import json
+from typing import Any, Dict, List, Optional
+
+from google.auth import exceptions
+
+
+@dataclass(frozen=True)
+class PublicKeyCredentialDescriptor:
+ """Descriptor for a security key based credential.
+
+ https://www.w3.org/TR/webauthn-3/#dictionary-credential-descriptor
+
+ Args:
+ id: <url-safe base64-encoded> credential id (key handle).
+ transports: <'usb'|'nfc'|'ble'|'internal'> List of supported transports.
+ """
+
+ id: str
+ transports: Optional[List[str]] = None
+
+ def to_dict(self):
+ cred = {"type": "public-key", "id": self.id}
+ if self.transports:
+ cred["transports"] = self.transports
+ return cred
+
+
+@dataclass
+class AuthenticationExtensionsClientInputs:
+ """Client extensions inputs for WebAuthn extensions.
+
+ Args:
+ appid: app id that can be asserted with in addition to rpid.
+ https://www.w3.org/TR/webauthn-3/#sctn-appid-extension
+ """
+
+ appid: Optional[str] = None
+
+ def to_dict(self):
+ extensions = {}
+ if self.appid:
+ extensions["appid"] = self.appid
+ return extensions
+
+
+@dataclass
+class GetRequest:
+ """WebAuthn get request
+
+ Args:
+ origin: Origin where the WebAuthn get assertion takes place.
+ rpid: Relying Party ID.
+ challenge: <url-safe base64-encoded> raw challenge.
+ timeout_ms: Timeout number in millisecond.
+ allow_credentials: List of allowed credentials.
+ user_verification: <'required'|'preferred'|'discouraged'> User verification requirement.
+ extensions: WebAuthn authentication extensions inputs.
+ """
+
+ origin: str
+ rpid: str
+ challenge: str
+ timeout_ms: Optional[int] = None
+ allow_credentials: Optional[List[PublicKeyCredentialDescriptor]] = None
+ user_verification: Optional[str] = None
+ extensions: Optional[AuthenticationExtensionsClientInputs] = None
+
+ def to_json(self) -> str:
+ req_options: Dict[str, Any] = {"rpid": self.rpid, "challenge": self.challenge}
+ if self.timeout_ms:
+ req_options["timeout"] = self.timeout_ms
+ if self.allow_credentials:
+ req_options["allowCredentials"] = [
+ c.to_dict() for c in self.allow_credentials
+ ]
+ if self.user_verification:
+ req_options["userVerification"] = self.user_verification
+ if self.extensions:
+ req_options["extensions"] = self.extensions.to_dict()
+ return json.dumps(
+ {"type": "get", "origin": self.origin, "requestData": req_options}
+ )
+
+
+@dataclass(frozen=True)
+class AuthenticatorAssertionResponse:
+ """Authenticator response to a WebAuthn get (assertion) request.
+
+ https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse
+
+ Args:
+ client_data_json: <url-safe base64-encoded> client data JSON.
+ authenticator_data: <url-safe base64-encoded> authenticator data.
+ signature: <url-safe base64-encoded> signature.
+ user_handle: <url-safe base64-encoded> user handle.
+ """
+
+ client_data_json: str
+ authenticator_data: str
+ signature: str
+ user_handle: Optional[str]
+
+
+@dataclass(frozen=True)
+class GetResponse:
+ """WebAuthn get (assertion) response.
+
+ Args:
+ id: <url-safe base64-encoded> credential id (key handle).
+ response: The authenticator assertion response.
+ authenticator_attachment: <'cross-platform'|'platform'> The attachment status of the authenticator.
+ client_extension_results: WebAuthn authentication extensions output results in a dictionary.
+ """
+
+ id: str
+ response: AuthenticatorAssertionResponse
+ authenticator_attachment: Optional[str]
+ client_extension_results: Optional[Dict]
+
+ @staticmethod
+ def from_json(json_str: str):
+ """Verify and construct GetResponse from a JSON string."""
+ try:
+ resp_json = json.loads(json_str)
+ except ValueError:
+ raise exceptions.MalformedError("Invalid Get JSON response")
+ if resp_json.get("type") != "getResponse":
+ raise exceptions.MalformedError(
+ "Invalid Get response type: {}".format(resp_json.get("type"))
+ )
+ pk_cred = resp_json.get("responseData")
+ if pk_cred is None:
+ if resp_json.get("error"):
+ raise exceptions.ReauthFailError(
+ "WebAuthn.get failure: {}".format(resp_json["error"])
+ )
+ else:
+ raise exceptions.MalformedError("Get response is empty")
+ if pk_cred.get("type") != "public-key":
+ raise exceptions.MalformedError(
+ "Invalid credential type: {}".format(pk_cred.get("type"))
+ )
+ assertion_json = pk_cred["response"]
+ assertion_resp = AuthenticatorAssertionResponse(
+ client_data_json=assertion_json["clientDataJSON"],
+ authenticator_data=assertion_json["authenticatorData"],
+ signature=assertion_json["signature"],
+ user_handle=assertion_json.get("userHandle"),
+ )
+ return GetResponse(
+ id=pk_cred["id"],
+ response=assertion_resp,
+ authenticator_attachment=pk_cred.get("authenticatorAttachment"),
+ client_extension_results=pk_cred.get("clientExtensionResults"),
+ )
diff --git a/contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py b/contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py
index 9cca317924..bb29f8c6e2 100644
--- a/contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py
+++ b/contrib/python/google-auth/py3/tests/compute_engine/test_credentials.py
@@ -499,7 +499,7 @@ class TestIDTokenCredentials(object):
responses.add(
responses.POST,
"https://iamcredentials.googleapis.com/v1/projects/-/"
- "serviceAccounts/service-account@example.com:signBlob?alt=json",
+ "serviceAccounts/service-account@example.com:signBlob",
status=200,
content_type="application/json",
json={"keyId": "some-key-id", "signedBlob": signature},
@@ -657,7 +657,7 @@ class TestIDTokenCredentials(object):
responses.add(
responses.POST,
"https://iamcredentials.googleapis.com/v1/projects/-/"
- "serviceAccounts/service-account@example.com:signBlob?alt=json",
+ "serviceAccounts/service-account@example.com:signBlob",
status=200,
content_type="application/json",
json={"keyId": "some-key-id", "signedBlob": signature},
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test__client.py b/contrib/python/google-auth/py3/tests/oauth2/test__client.py
index 444232f396..f9a2d3aff4 100644
--- a/contrib/python/google-auth/py3/tests/oauth2/test__client.py
+++ b/contrib/python/google-auth/py3/tests/oauth2/test__client.py
@@ -24,6 +24,7 @@ import pytest # type: ignore
from google.auth import _helpers
from google.auth import crypt
from google.auth import exceptions
+from google.auth import iam
from google.auth import jwt
from google.auth import transport
from google.oauth2 import _client
@@ -319,7 +320,11 @@ def test_call_iam_generate_id_token_endpoint():
request = make_request({"token": id_token})
token, expiry = _client.call_iam_generate_id_token_endpoint(
- request, "fake_email", "fake_audience", "fake_access_token"
+ request,
+ iam._IAM_IDTOKEN_ENDPOINT,
+ "fake_email",
+ "fake_audience",
+ "fake_access_token",
)
assert (
@@ -352,7 +357,11 @@ def test_call_iam_generate_id_token_endpoint_no_id_token():
with pytest.raises(exceptions.RefreshError) as excinfo:
_client.call_iam_generate_id_token_endpoint(
- request, "fake_email", "fake_audience", "fake_access_token"
+ request,
+ iam._IAM_IDTOKEN_ENDPOINT,
+ "fake_email",
+ "fake_audience",
+ "fake_access_token",
)
assert excinfo.match("No ID token in response")
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_challenges.py b/contrib/python/google-auth/py3/tests/oauth2/test_challenges.py
index a06f552837..4116b913ab 100644
--- a/contrib/python/google-auth/py3/tests/oauth2/test_challenges.py
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_challenges.py
@@ -15,6 +15,7 @@
"""Tests for the reauth module."""
import base64
+import os
import sys
import mock
@@ -23,6 +24,13 @@ import pyu2f # type: ignore
from google.auth import exceptions
from google.oauth2 import challenges
+from google.oauth2.webauthn_types import (
+ AuthenticationExtensionsClientInputs,
+ AuthenticatorAssertionResponse,
+ GetRequest,
+ GetResponse,
+ PublicKeyCredentialDescriptor,
+)
def test_get_user_password():
@@ -54,6 +62,8 @@ def test_security_key():
# Test the case that security key challenge is passed with applicationId and
# relyingPartyId the same.
+ os.environ.pop('"GOOGLE_AUTH_WEBAUTHN_PLUGIN"', None)
+
with mock.patch("pyu2f.model.RegisteredKey", return_value=mock_key):
with mock.patch(
"pyu2f.convenience.authenticator.CompositeAuthenticator.Authenticate"
@@ -70,6 +80,19 @@ def test_security_key():
print_callback=sys.stderr.write,
)
+ # Test the case that webauthn plugin is available
+ os.environ["GOOGLE_AUTH_WEBAUTHN_PLUGIN"] = "plugin"
+
+ with mock.patch(
+ "google.oauth2.challenges.SecurityKeyChallenge._obtain_challenge_input_webauthn",
+ return_value={"securityKey": "security key response"},
+ ):
+
+ assert challenge.obtain_challenge_input(metadata) == {
+ "securityKey": "security key response"
+ }
+ os.environ.pop('"GOOGLE_AUTH_WEBAUTHN_PLUGIN"', None)
+
# Test the case that security key challenge is passed with applicationId and
# relyingPartyId different, first call works.
metadata["securityKey"]["relyingPartyId"] = "security_key_relying_party_id"
@@ -173,6 +196,136 @@ def test_security_key():
assert excinfo.match(r"pyu2f dependency is required")
+def test_security_key_webauthn():
+ metadata = {
+ "status": "READY",
+ "challengeId": 2,
+ "challengeType": "SECURITY_KEY",
+ "securityKey": {
+ "applicationId": "security_key_application_id",
+ "challenges": [
+ {
+ "keyHandle": "some_key",
+ "challenge": base64.urlsafe_b64encode(
+ "some_challenge".encode("ascii")
+ ).decode("ascii"),
+ }
+ ],
+ "relyingPartyId": "security_key_application_id",
+ },
+ }
+
+ challenge = challenges.SecurityKeyChallenge()
+
+ sk = metadata["securityKey"]
+ sk_challenges = sk["challenges"]
+
+ application_id = sk["applicationId"]
+
+ allow_credentials = []
+ for sk_challenge in sk_challenges:
+ allow_credentials.append(
+ PublicKeyCredentialDescriptor(id=sk_challenge["keyHandle"])
+ )
+
+ extension = AuthenticationExtensionsClientInputs(appid=application_id)
+
+ get_request = GetRequest(
+ origin=challenges.REAUTH_ORIGIN,
+ rpid=application_id,
+ challenge=challenge._unpadded_urlsafe_b64recode(sk_challenge["challenge"]),
+ timeout_ms=challenges.WEBAUTHN_TIMEOUT_MS,
+ allow_credentials=allow_credentials,
+ user_verification="required",
+ extensions=extension,
+ )
+
+ assertion_resp = AuthenticatorAssertionResponse(
+ client_data_json="clientDataJSON",
+ authenticator_data="authenticatorData",
+ signature="signature",
+ user_handle="userHandle",
+ )
+ get_response = GetResponse(
+ id="id",
+ response=assertion_resp,
+ authenticator_attachment="authenticatorAttachment",
+ client_extension_results="clientExtensionResults",
+ )
+ response = {
+ "clientData": get_response.response.client_data_json,
+ "authenticatorData": get_response.response.authenticator_data,
+ "signatureData": get_response.response.signature,
+ "applicationId": "security_key_application_id",
+ "keyHandle": get_response.id,
+ "securityKeyReplyType": 2,
+ }
+
+ mock_handler = mock.Mock()
+ mock_handler.get.return_value = get_response
+
+ # Test success case
+ assert challenge._obtain_challenge_input_webauthn(metadata, mock_handler) == {
+ "securityKey": response
+ }
+ mock_handler.get.assert_called_with(get_request)
+
+ # Test exceptions
+
+ # Missing Values
+ sk = metadata["securityKey"]
+ metadata["securityKey"] = None
+ with pytest.raises(exceptions.InvalidValue):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+ metadata["securityKey"] = sk
+
+ c = metadata["securityKey"]["challenges"]
+ metadata["securityKey"]["challenges"] = None
+ with pytest.raises(exceptions.InvalidValue):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+ metadata["securityKey"]["challenges"] = []
+ with pytest.raises(exceptions.InvalidValue):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+ metadata["securityKey"]["challenges"] = c
+
+ aid = metadata["securityKey"]["applicationId"]
+ metadata["securityKey"]["applicationId"] = None
+ with pytest.raises(exceptions.InvalidValue):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+ metadata["securityKey"]["applicationId"] = aid
+
+ rpi = metadata["securityKey"]["relyingPartyId"]
+ metadata["securityKey"]["relyingPartyId"] = None
+ with pytest.raises(exceptions.InvalidValue):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+ metadata["securityKey"]["relyingPartyId"] = rpi
+
+ kh = metadata["securityKey"]["challenges"][0]["keyHandle"]
+ metadata["securityKey"]["challenges"][0]["keyHandle"] = None
+ with pytest.raises(exceptions.InvalidValue):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+ metadata["securityKey"]["challenges"][0]["keyHandle"] = kh
+
+ ch = metadata["securityKey"]["challenges"][0]["challenge"]
+ metadata["securityKey"]["challenges"][0]["challenge"] = None
+ with pytest.raises(exceptions.InvalidValue):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+ metadata["securityKey"]["challenges"][0]["challenge"] = ch
+
+ # Handler Exceptions
+ mock_handler.get.side_effect = exceptions.MalformedError
+ with pytest.raises(exceptions.MalformedError):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+
+ mock_handler.get.side_effect = exceptions.InvalidResource
+ with pytest.raises(exceptions.InvalidResource):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+
+ mock_handler.get.side_effect = exceptions.ReauthFailError
+ with pytest.raises(exceptions.ReauthFailError):
+ challenge._obtain_challenge_input_webauthn(metadata, mock_handler)
+
+
@mock.patch("getpass.getpass", return_value="foo")
def test_password_challenge(getpass_mock):
challenge = challenges.PasswordChallenge()
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_service_account.py b/contrib/python/google-auth/py3/tests/oauth2/test_service_account.py
index ce0c72fa0a..0dbe316a0f 100644
--- a/contrib/python/google-auth/py3/tests/oauth2/test_service_account.py
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_service_account.py
@@ -22,6 +22,7 @@ import pytest # type: ignore
from google.auth import _helpers
from google.auth import crypt
from google.auth import exceptions
+from google.auth import iam
from google.auth import jwt
from google.auth import transport
from google.auth.credentials import DEFAULT_UNIVERSE_DOMAIN
@@ -772,10 +773,36 @@ class TestIDTokenCredentials(object):
)
request = mock.Mock()
credentials.refresh(request)
- req, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[
+ req, iam_endpoint, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[
0
]
assert req == request
+ assert iam_endpoint == iam._IAM_IDTOKEN_ENDPOINT
+ assert signer_email == "service-account@example.com"
+ assert target_audience == "https://example.com"
+ decoded_access_token = jwt.decode(access_token, verify=False)
+ assert decoded_access_token["scope"] == "https://www.googleapis.com/auth/iam"
+
+ @mock.patch(
+ "google.oauth2._client.call_iam_generate_id_token_endpoint", autospec=True
+ )
+ def test_refresh_iam_flow_non_gdu(self, call_iam_generate_id_token_endpoint):
+ credentials = self.make_credentials(universe_domain="fake-universe")
+ token = "id_token"
+ call_iam_generate_id_token_endpoint.return_value = (
+ token,
+ _helpers.utcnow() + datetime.timedelta(seconds=500),
+ )
+ request = mock.Mock()
+ credentials.refresh(request)
+ req, iam_endpoint, signer_email, target_audience, access_token = call_iam_generate_id_token_endpoint.call_args[
+ 0
+ ]
+ assert req == request
+ assert (
+ iam_endpoint
+ == "https://iamcredentials.fake-universe/v1/projects/-/serviceAccounts/{}:generateIdToken"
+ )
assert signer_email == "service-account@example.com"
assert target_audience == "https://example.com"
decoded_access_token = jwt.decode(access_token, verify=False)
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py
new file mode 100644
index 0000000000..454e97cb61
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py
@@ -0,0 +1,148 @@
+import json
+import struct
+
+import mock
+import pytest # type: ignore
+
+from google.auth import exceptions
+from google.oauth2 import webauthn_handler
+from google.oauth2 import webauthn_types
+
+
+@pytest.fixture
+def os_get_stub():
+ with mock.patch.object(
+ webauthn_handler.os.environ,
+ "get",
+ return_value="gcloud_webauthn_plugin",
+ name="fake os.environ.get",
+ ) as mock_os_environ_get:
+ yield mock_os_environ_get
+
+
+@pytest.fixture
+def subprocess_run_stub():
+ with mock.patch.object(
+ webauthn_handler.subprocess, "run", name="fake subprocess.run"
+ ) as mock_subprocess_run:
+ yield mock_subprocess_run
+
+
+def test_PluginHandler_is_available(os_get_stub):
+ test_handler = webauthn_handler.PluginHandler()
+
+ assert test_handler.is_available() is True
+
+ os_get_stub.return_value = None
+ assert test_handler.is_available() is False
+
+
+GET_ASSERTION_REQUEST = webauthn_types.GetRequest(
+ origin="fake_origin",
+ rpid="fake_rpid",
+ challenge="fake_challenge",
+ allow_credentials=[webauthn_types.PublicKeyCredentialDescriptor(id="fake_id_1")],
+)
+
+
+def test_malformated_get_assertion_response(os_get_stub, subprocess_run_stub):
+ response_len = struct.pack("<I", 5)
+ response = "1234567890"
+ mock_response = mock.Mock()
+ mock_response.stdout = response_len + response.encode()
+ subprocess_run_stub.return_value = mock_response
+
+ test_handler = webauthn_handler.PluginHandler()
+ with pytest.raises(exceptions.MalformedError) as excinfo:
+ test_handler.get(GET_ASSERTION_REQUEST)
+ assert "Plugin response length" in str(excinfo.value)
+
+
+def test_failure_get_assertion(os_get_stub, subprocess_run_stub):
+ failure_response = {
+ "type": "getResponse",
+ "error": "fake_plugin_get_assertion_failure",
+ }
+ response_json = json.dumps(failure_response).encode()
+ response_len = struct.pack("<I", len(response_json))
+
+ # process returns get response in json
+ mock_response = mock.Mock()
+ mock_response.stdout = response_len + response_json
+ subprocess_run_stub.return_value = mock_response
+
+ test_handler = webauthn_handler.PluginHandler()
+ with pytest.raises(exceptions.ReauthFailError) as excinfo:
+ test_handler.get(GET_ASSERTION_REQUEST)
+ assert failure_response["error"] in str(excinfo.value)
+
+
+def test_success_get_assertion(os_get_stub, subprocess_run_stub):
+ success_response = {
+ "type": "public-key",
+ "id": "fake-id",
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {"appid": True},
+ "response": {
+ "clientDataJSON": "fake_client_data_json_base64",
+ "authenticatorData": "fake_authenticator_data_base64",
+ "signature": "fake_signature_base64",
+ "userHandle": "fake_user_handle_base64",
+ },
+ }
+ valid_plugin_response = {"type": "getResponse", "responseData": success_response}
+ valid_plugin_response_json = json.dumps(valid_plugin_response).encode()
+ valid_plugin_response_len = struct.pack("<I", len(valid_plugin_response_json))
+
+ # process returns get response in json
+ mock_response = mock.Mock()
+ mock_response.stdout = valid_plugin_response_len + valid_plugin_response_json
+ subprocess_run_stub.return_value = mock_response
+
+ # Call get()
+ test_handler = webauthn_handler.PluginHandler()
+ got_response = test_handler.get(GET_ASSERTION_REQUEST)
+
+ # Validate expected plugin request
+ os_get_stub.assert_called_once()
+ subprocess_run_stub.assert_called_once()
+
+ stdin_input = subprocess_run_stub.call_args.kwargs["input"]
+ input_json_len_le = stdin_input[:4]
+ input_json_len = struct.unpack("<I", input_json_len_le)[0]
+ input_json = stdin_input[4:]
+ assert len(input_json) == input_json_len
+
+ input_dict = json.loads(input_json.decode("utf8"))
+ assert input_dict == {
+ "type": "get",
+ "origin": "fake_origin",
+ "requestData": {
+ "rpid": "fake_rpid",
+ "challenge": "fake_challenge",
+ "allowCredentials": [{"type": "public-key", "id": "fake_id_1"}],
+ },
+ }
+
+ # Validate get assertion response
+ assert got_response.id == success_response["id"]
+ assert (
+ got_response.authenticator_attachment
+ == success_response["authenticatorAttachment"]
+ )
+ assert (
+ got_response.client_extension_results
+ == success_response["clientExtensionResults"]
+ )
+ assert (
+ got_response.response.client_data_json
+ == success_response["response"]["clientDataJSON"]
+ )
+ assert (
+ got_response.response.authenticator_data
+ == success_response["response"]["authenticatorData"]
+ )
+ assert got_response.response.signature == success_response["response"]["signature"]
+ assert (
+ got_response.response.user_handle == success_response["response"]["userHandle"]
+ )
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler_factory.py b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler_factory.py
new file mode 100644
index 0000000000..47890ce4b4
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler_factory.py
@@ -0,0 +1,29 @@
+import mock
+import pytest # type: ignore
+
+from google.oauth2 import webauthn_handler
+from google.oauth2 import webauthn_handler_factory
+
+
+@pytest.fixture
+def os_get_stub():
+ with mock.patch.object(
+ webauthn_handler.os.environ,
+ "get",
+ return_value="gcloud_webauthn_plugin",
+ name="fake os.environ.get",
+ ) as mock_os_environ_get:
+ yield mock_os_environ_get
+
+
+# Check that get_handler returns a value when env is set,
+# that type is PluginHandler, and that no value is returned
+# if env not set.
+def test_WebauthHandlerFactory_get(os_get_stub):
+ factory = webauthn_handler_factory.WebauthnHandlerFactory()
+ assert factory.get_handler() is not None
+
+ assert isinstance(factory.get_handler(), webauthn_handler.PluginHandler)
+
+ os_get_stub.return_value = None
+ assert factory.get_handler() is None
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py
new file mode 100644
index 0000000000..5231d21896
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py
@@ -0,0 +1,237 @@
+import json
+
+import pytest # type: ignore
+
+from google.oauth2 import webauthn_types
+
+
+@pytest.mark.parametrize(
+ "test_pub_key_cred,expected_dict",
+ [
+ (
+ webauthn_types.PublicKeyCredentialDescriptor(
+ id="fake_cred_id_base64", transports=None
+ ),
+ {"type": "public-key", "id": "fake_cred_id_base64"},
+ ),
+ (
+ webauthn_types.PublicKeyCredentialDescriptor(
+ id="fake_cred_id_base64", transports=[]
+ ),
+ {"type": "public-key", "id": "fake_cred_id_base64"},
+ ),
+ (
+ webauthn_types.PublicKeyCredentialDescriptor(
+ id="fake_cred_id_base64", transports=["usb"]
+ ),
+ {"type": "public-key", "id": "fake_cred_id_base64", "transports": ["usb"]},
+ ),
+ (
+ webauthn_types.PublicKeyCredentialDescriptor(
+ id="fake_cred_id_base64", transports=["usb", "internal"]
+ ),
+ {
+ "type": "public-key",
+ "id": "fake_cred_id_base64",
+ "transports": ["usb", "internal"],
+ },
+ ),
+ ],
+)
+def test_PublicKeyCredentialDescriptor(test_pub_key_cred, expected_dict):
+ assert test_pub_key_cred.to_dict() == expected_dict
+
+
+@pytest.mark.parametrize(
+ "test_extension_input,expected_dict",
+ [
+ (webauthn_types.AuthenticationExtensionsClientInputs(), {}),
+ (webauthn_types.AuthenticationExtensionsClientInputs(appid=""), {}),
+ (
+ webauthn_types.AuthenticationExtensionsClientInputs(appid="fake_appid"),
+ {"appid": "fake_appid"},
+ ),
+ ],
+)
+def test_AuthenticationExtensionsClientInputs(test_extension_input, expected_dict):
+ assert test_extension_input.to_dict() == expected_dict
+
+
+@pytest.mark.parametrize("has_allow_credentials", [(False), (True)])
+def test_GetRequest(has_allow_credentials):
+ allow_credentials = [
+ webauthn_types.PublicKeyCredentialDescriptor(id="fake_id_1"),
+ webauthn_types.PublicKeyCredentialDescriptor(id="fake_id_2"),
+ ]
+ test_get_request = webauthn_types.GetRequest(
+ origin="fake_origin",
+ rpid="fake_rpid",
+ challenge="fake_challenge",
+ timeout_ms=123,
+ allow_credentials=allow_credentials if has_allow_credentials else None,
+ user_verification="preferred",
+ extensions=webauthn_types.AuthenticationExtensionsClientInputs(
+ appid="fake_appid"
+ ),
+ )
+ expected_allow_credentials = [
+ {"type": "public-key", "id": "fake_id_1"},
+ {"type": "public-key", "id": "fake_id_2"},
+ ]
+ exepcted_dict = {
+ "type": "get",
+ "origin": "fake_origin",
+ "requestData": {
+ "rpid": "fake_rpid",
+ "timeout": 123,
+ "challenge": "fake_challenge",
+ "userVerification": "preferred",
+ "extensions": {"appid": "fake_appid"},
+ },
+ }
+ if has_allow_credentials:
+ exepcted_dict["requestData"]["allowCredentials"] = expected_allow_credentials
+ assert json.loads(test_get_request.to_json()) == exepcted_dict
+
+
+@pytest.mark.parametrize(
+ "has_user_handle,has_authenticator_attachment,has_client_extension_results",
+ [
+ (False, False, False),
+ (False, False, True),
+ (False, True, False),
+ (False, True, True),
+ (True, False, False),
+ (True, False, True),
+ (True, True, False),
+ (True, True, True),
+ ],
+)
+def test_GetResponse(
+ has_user_handle, has_authenticator_attachment, has_client_extension_results
+):
+ input_response_data = {
+ "type": "public-key",
+ "id": "fake-id",
+ "authenticatorAttachment": "cross-platform",
+ "clientExtensionResults": {"appid": True},
+ "response": {
+ "clientDataJSON": "fake_client_data_json_base64",
+ "authenticatorData": "fake_authenticator_data_base64",
+ "signature": "fake_signature_base64",
+ "userHandle": "fake_user_handle_base64",
+ },
+ }
+ if not has_authenticator_attachment:
+ input_response_data.pop("authenticatorAttachment")
+ if not has_client_extension_results:
+ input_response_data.pop("clientExtensionResults")
+ if not has_user_handle:
+ input_response_data["response"].pop("userHandle")
+
+ response = webauthn_types.GetResponse.from_json(
+ json.dumps({"type": "getResponse", "responseData": input_response_data})
+ )
+
+ assert response.id == input_response_data["id"]
+ assert response.authenticator_attachment == (
+ input_response_data["authenticatorAttachment"]
+ if has_authenticator_attachment
+ else None
+ )
+ assert response.client_extension_results == (
+ input_response_data["clientExtensionResults"]
+ if has_client_extension_results
+ else None
+ )
+ assert (
+ response.response.client_data_json
+ == input_response_data["response"]["clientDataJSON"]
+ )
+ assert (
+ response.response.authenticator_data
+ == input_response_data["response"]["authenticatorData"]
+ )
+ assert response.response.signature == input_response_data["response"]["signature"]
+ assert response.response.user_handle == (
+ input_response_data["response"]["userHandle"] if has_user_handle else None
+ )
+
+
+@pytest.mark.parametrize(
+ "input_dict,expected_error",
+ [
+ ({"xyz_type": "wrong_type"}, "Invalid Get response type"),
+ ({"type": "wrong_type"}, "Invalid Get response type"),
+ ({"type": "getResponse"}, "Get response is empty"),
+ (
+ {"type": "getResponse", "error": "fake_get_response_error"},
+ "WebAuthn.get failure: fake_get_response_error",
+ ),
+ (
+ {"type": "getResponse", "responseData": {"xyz_type": "wrong_type"}},
+ "Invalid credential type",
+ ),
+ (
+ {"type": "getResponse", "responseData": {"type": "wrong_type"}},
+ "Invalid credential type",
+ ),
+ (
+ {
+ "type": "getResponse",
+ "responseData": {"type": "public-key", "response": {}},
+ },
+ "KeyError",
+ ),
+ (
+ {
+ "type": "getResponse",
+ "responseData": {
+ "type": "public-key",
+ "response": {"clientDataJSON": "fake_client_data_json_base64"},
+ },
+ },
+ "KeyError",
+ ),
+ (
+ {
+ "type": "getResponse",
+ "responseData": {
+ "type": "public-key",
+ "response": {
+ "clientDataJSON": "fake_client_data_json_base64",
+ "authenticatorData": "fake_authenticator_data_base64",
+ },
+ },
+ },
+ "KeyError",
+ ),
+ (
+ {
+ "type": "getResponse",
+ "responseData": {
+ "type": "public-key",
+ "response": {
+ "clientDataJSON": "fake_client_data_json_base64",
+ "authenticatorData": "fake_authenticator_data_base64",
+ "signature": "fake_signature_base64",
+ },
+ },
+ },
+ "KeyError",
+ ),
+ ],
+)
+def test_GetResponse_error(input_dict, expected_error):
+ with pytest.raises(Exception) as excinfo:
+ webauthn_types.GetResponse.from_json(json.dumps(input_dict))
+ if expected_error == "KeyError":
+ assert excinfo.type is KeyError
+ else:
+ assert expected_error in str(excinfo.value)
+
+
+def test_MalformatedJsonInput():
+ with pytest.raises(ValueError) as excinfo:
+ webauthn_types.GetResponse.from_json(")]}")
+ assert "Invalid Get JSON response" in str(excinfo.value)
diff --git a/contrib/python/google-auth/py3/tests/test_aws.py b/contrib/python/google-auth/py3/tests/test_aws.py
index 5614820312..df1f02e7d7 100644
--- a/contrib/python/google-auth/py3/tests/test_aws.py
+++ b/contrib/python/google-auth/py3/tests/test_aws.py
@@ -1220,6 +1220,39 @@ class TestCredentials(object):
url + SERVICE_ACCOUNT_IMPERSONATION_URL_ROUTE
)
+ def test_info_with_default_token_url(self):
+ credentials = aws.Credentials(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE.copy(),
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_info_with_default_token_url_with_universe_domain(self):
+ credentials = aws.Credentials(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ credential_source=self.CREDENTIAL_SOURCE.copy(),
+ universe_domain="testdomain.org",
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": "https://sts.testdomain.org/v1/token",
+ "credential_source": self.CREDENTIAL_SOURCE.copy(),
+ "universe_domain": "testdomain.org",
+ }
+
def test_retrieve_subject_token_missing_region_url(self):
# When AWS_REGION envvar is not available, region_url is required for
# determining the current AWS region.
diff --git a/contrib/python/google-auth/py3/tests/test_identity_pool.py b/contrib/python/google-auth/py3/tests/test_identity_pool.py
index 0de711832f..e4efe46c6b 100644
--- a/contrib/python/google-auth/py3/tests/test_identity_pool.py
+++ b/contrib/python/google-auth/py3/tests/test_identity_pool.py
@@ -783,6 +783,39 @@ class TestCredentials(object):
"universe_domain": DEFAULT_UNIVERSE_DOMAIN,
}
+ def test_info_with_default_token_url(self):
+ credentials = identity_pool.Credentials(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy(),
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": TOKEN_URL,
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL,
+ "universe_domain": DEFAULT_UNIVERSE_DOMAIN,
+ }
+
+ def test_info_with_default_token_url_with_universe_domain(self):
+ credentials = identity_pool.Credentials(
+ audience=AUDIENCE,
+ subject_token_type=SUBJECT_TOKEN_TYPE,
+ credential_source=self.CREDENTIAL_SOURCE_TEXT_URL.copy(),
+ universe_domain="testdomain.org",
+ )
+
+ assert credentials.info == {
+ "type": "external_account",
+ "audience": AUDIENCE,
+ "subject_token_type": SUBJECT_TOKEN_TYPE,
+ "token_url": "https://sts.testdomain.org/v1/token",
+ "credential_source": self.CREDENTIAL_SOURCE_TEXT_URL,
+ "universe_domain": "testdomain.org",
+ }
+
def test_retrieve_subject_token_missing_subject_token(self, tmpdir):
# Provide empty text file.
empty_file = tmpdir.join("empty.txt")
diff --git a/contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py b/contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py
index d2907bad29..3a33c2c021 100644
--- a/contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py
+++ b/contrib/python/google-auth/py3/tests/transport/test__custom_tls_signer.py
@@ -195,6 +195,7 @@ def test_custom_tls_signer():
get_cert.assert_called_once()
get_sign_callback.assert_called_once()
offload_lib.ConfigureSslContext.assert_called_once()
+ assert not signer_object.should_use_provider()
assert signer_object._enterprise_cert_file_path == ENTERPRISE_CERT_FILE
assert signer_object._offload_lib == offload_lib
assert signer_object._signer_lib == signer_lib
@@ -216,6 +217,7 @@ def test_custom_tls_signer_provider():
signer_object.load_libraries()
signer_object.attach_to_ssl_context(mock.MagicMock())
+ assert signer_object.should_use_provider()
assert signer_object._enterprise_cert_file_path == ENTERPRISE_CERT_FILE_PROVIDER
assert signer_object._provider_lib == provider_lib
load_provider_lib.assert_called_with("/path/to/provider/lib")
diff --git a/contrib/python/google-auth/py3/tests/transport/test_requests.py b/contrib/python/google-auth/py3/tests/transport/test_requests.py
index aadc1ddbfd..0da3e36d9a 100644
--- a/contrib/python/google-auth/py3/tests/transport/test_requests.py
+++ b/contrib/python/google-auth/py3/tests/transport/test_requests.py
@@ -568,3 +568,38 @@ class TestMutualTlsOffloadAdapter(object):
adapter.proxy_manager_for()
mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager)
+
+ @mock.patch.object(requests.adapters.HTTPAdapter, "init_poolmanager")
+ @mock.patch.object(requests.adapters.HTTPAdapter, "proxy_manager_for")
+ @mock.patch.object(
+ google.auth.transport._custom_tls_signer.CustomTlsSigner, "should_use_provider"
+ )
+ @mock.patch.object(
+ google.auth.transport._custom_tls_signer.CustomTlsSigner, "load_libraries"
+ )
+ @mock.patch.object(
+ google.auth.transport._custom_tls_signer.CustomTlsSigner,
+ "attach_to_ssl_context",
+ )
+ def test_success_should_use_provider(
+ self,
+ mock_attach_to_ssl_context,
+ mock_load_libraries,
+ mock_should_use_provider,
+ mock_proxy_manager_for,
+ mock_init_poolmanager,
+ ):
+ enterprise_cert_file_path = "/path/to/enterprise/cert/json"
+ adapter = google.auth.transport.requests._MutualTlsOffloadAdapter(
+ enterprise_cert_file_path
+ )
+
+ mock_should_use_provider.side_effect = True
+ mock_load_libraries.assert_called_once()
+ assert mock_attach_to_ssl_context.call_count == 2
+
+ adapter.init_poolmanager()
+ mock_init_poolmanager.assert_called_with(ssl_context=adapter._ctx_poolmanager)
+
+ adapter.proxy_manager_for()
+ mock_proxy_manager_for.assert_called_with(ssl_context=adapter._ctx_proxymanager)
diff --git a/contrib/python/google-auth/py3/ya.make b/contrib/python/google-auth/py3/ya.make
index 952d1ebdd3..3216499980 100644
--- a/contrib/python/google-auth/py3/ya.make
+++ b/contrib/python/google-auth/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(2.29.0)
+VERSION(2.30.0)
LICENSE(Apache-2.0)
@@ -86,12 +86,17 @@ PY_SRCS(
google/oauth2/service_account.py
google/oauth2/sts.py
google/oauth2/utils.py
+ google/oauth2/webauthn_handler.py
+ google/oauth2/webauthn_handler_factory.py
+ google/oauth2/webauthn_types.py
)
RESOURCE_FILES(
PREFIX contrib/python/google-auth/py3/
.dist-info/METADATA
.dist-info/top_level.txt
+ google/auth/py.typed
+ google/oauth2/py.typed
)
END()